Reworked the file transfer system, added a channel file browser and fixed some minor bugs
13
ChangeLog.md
|
@ -1,4 +1,17 @@
|
||||||
# Changelog:
|
# Changelog:
|
||||||
|
* **10.06.20**
|
||||||
|
- Finalize the channel file explorer
|
||||||
|
- Reworked the file transfer system
|
||||||
|
- Using an appropriate hash function for the avatar id generation
|
||||||
|
- Fixed icon over clipping for the channel tree and favorites
|
||||||
|
|
||||||
|
* **21.05.20**
|
||||||
|
- Updated the volume adjustment bar
|
||||||
|
|
||||||
|
* **18.05.20**
|
||||||
|
- Fixed client name change does not update the name in the channel tree
|
||||||
|
- Fixed hostbanner height
|
||||||
|
|
||||||
* **03.05.20**
|
* **03.05.20**
|
||||||
- Splitup the file transfer & management part
|
- Splitup the file transfer & management part
|
||||||
- Added the ability to register a custom file transfer provider (required for the native client)
|
- Added the ability to register a custom file transfer provider (required for the native client)
|
||||||
|
|
|
@ -5,7 +5,8 @@ import * as template_loader from "./template_loader";
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
tr(message: string) : string;
|
tr(message: string) : string;
|
||||||
tra(message: string, ...args: any[]);
|
tra(message: string, ...args: (string | number | boolean)[]) : string;
|
||||||
|
tra(message: string, ...args: any[]) : JQuery[];
|
||||||
|
|
||||||
log: any;
|
log: any;
|
||||||
StaticSettings: any;
|
StaticSettings: any;
|
||||||
|
|
|
@ -1641,6 +1641,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"can-use-dom": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/can-use-dom/-/can-use-dom-0.1.0.tgz",
|
||||||
|
"integrity": "sha1-IsxKNKCrxDlQ9CxkEQJKP2NmtFo="
|
||||||
|
},
|
||||||
"capture-stack-trace": {
|
"capture-stack-trace": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz",
|
||||||
|
@ -2094,6 +2099,11 @@
|
||||||
"is-plain-object": "^2.0.1"
|
"is-plain-object": "^2.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"core-js": {
|
||||||
|
"version": "3.6.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz",
|
||||||
|
"integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA=="
|
||||||
|
},
|
||||||
"core-util-is": {
|
"core-util-is": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||||
|
@ -6696,12 +6706,27 @@
|
||||||
"integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=",
|
"integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"lodash.debounce": {
|
||||||
|
"version": "4.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||||
|
"integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168="
|
||||||
|
},
|
||||||
"lodash.has": {
|
"lodash.has": {
|
||||||
"version": "4.5.2",
|
"version": "4.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz",
|
||||||
"integrity": "sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI=",
|
"integrity": "sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"lodash.memoize": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
||||||
|
"integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4="
|
||||||
|
},
|
||||||
|
"lodash.throttle": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
|
||||||
|
"integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ="
|
||||||
|
},
|
||||||
"log-symbols": {
|
"log-symbols": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz",
|
||||||
|
@ -9897,6 +9922,34 @@
|
||||||
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
|
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"simple-html-tokenizer": {
|
||||||
|
"version": "0.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/simple-html-tokenizer/-/simple-html-tokenizer-0.1.1.tgz",
|
||||||
|
"integrity": "sha1-BcLuxXn//+FFoDCsJs/qYbmA+r4=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"simplebar": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/simplebar/-/simplebar-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-CpVSINCQ/XAYABUdUAnVWHyjkBYoFu+s12IUrZgVNfXzILNXP0MP+5OaIBjylzjYxIE/rsuC1K50/xJldPGGpQ==",
|
||||||
|
"requires": {
|
||||||
|
"can-use-dom": "^0.1.0",
|
||||||
|
"core-js": "^3.0.1",
|
||||||
|
"lodash.debounce": "^4.0.8",
|
||||||
|
"lodash.memoize": "^4.1.2",
|
||||||
|
"lodash.throttle": "^4.1.1",
|
||||||
|
"resize-observer-polyfill": "^1.5.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"simplebar-react": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/simplebar-react/-/simplebar-react-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-MOhBo2RabguVxUG4b8khO+zhOhktGUt3BjRsbTs8EpA0DzeCtAxq6RzdtDqGeBUj2c2+/CpF2vQmuvRCARln/A==",
|
||||||
|
"requires": {
|
||||||
|
"prop-types": "^15.6.1",
|
||||||
|
"simplebar": "^5.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"slash": {
|
"slash": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
|
||||||
|
@ -10376,6 +10429,17 @@
|
||||||
"es6-symbol": "^3.1.1"
|
"es6-symbol": "^3.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"svg-inline-loader": {
|
||||||
|
"version": "0.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg-inline-loader/-/svg-inline-loader-0.8.2.tgz",
|
||||||
|
"integrity": "sha512-kbrcEh5n5JkypaSC152eGfGcnT4lkR0eSfvefaUJkLqgGjRQJyKDvvEE/CCv5aTSdfXuc+N98w16iAojhShI3g==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"loader-utils": "^1.1.0",
|
||||||
|
"object-assign": "^4.0.1",
|
||||||
|
"simple-html-tokenizer": "^0.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"tapable": {
|
"tapable": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz",
|
||||||
|
|
|
@ -60,16 +60,17 @@
|
||||||
"sass-loader": "^8.0.2",
|
"sass-loader": "^8.0.2",
|
||||||
"sha256": "^0.2.0",
|
"sha256": "^0.2.0",
|
||||||
"style-loader": "^1.1.3",
|
"style-loader": "^1.1.3",
|
||||||
|
"svg-inline-loader": "^0.8.2",
|
||||||
"terser": "^4.2.1",
|
"terser": "^4.2.1",
|
||||||
"terser-webpack-plugin": "latest",
|
"terser-webpack-plugin": "latest",
|
||||||
"ts-loader": "^6.2.2",
|
"ts-loader": "^6.2.2",
|
||||||
|
"tsd": "latest",
|
||||||
"typescript": "^3.7.0",
|
"typescript": "^3.7.0",
|
||||||
"wabt": "^1.0.13",
|
"wabt": "^1.0.13",
|
||||||
"webpack": "^4.42.1",
|
"webpack": "^4.42.1",
|
||||||
"webpack-bundle-analyzer": "^3.6.1",
|
"webpack-bundle-analyzer": "^3.6.1",
|
||||||
"webpack-cli": "^3.3.11",
|
"webpack-cli": "^3.3.11",
|
||||||
"worker-plugin": "^4.0.2",
|
"worker-plugin": "^4.0.2"
|
||||||
"tsd": "latest"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -85,6 +86,7 @@
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
"resize-observer-polyfill": "^1.5.1",
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
|
"simplebar-react": "^2.2.0",
|
||||||
"webrtc-adapter": "^7.5.1"
|
"webrtc-adapter": "^7.5.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -842,45 +842,6 @@ $tooltip_height: 1.8em;
|
||||||
|
|
||||||
.tooltip {
|
.tooltip {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
/*
|
|
||||||
position: absolute;
|
|
||||||
top: -($tooltip_height + .6em);
|
|
||||||
left: -($tooltip_width - $thumb_width) / 2;
|
|
||||||
|
|
||||||
line-height: 1em;
|
|
||||||
|
|
||||||
height: $tooltip_height;
|
|
||||||
width: $tooltip_width;
|
|
||||||
|
|
||||||
background-color: #232222;
|
|
||||||
border-radius: $border_radius_middle;
|
|
||||||
|
|
||||||
text-align: center;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-around;
|
|
||||||
|
|
||||||
opacity: 0;
|
|
||||||
@include transition(opacity .5s ease-in-out);
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
content: '';
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
left: ($tooltip_width - $thumb_width) / 2 - .25em;
|
|
||||||
right: 0;
|
|
||||||
bottom: -.4em;
|
|
||||||
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
|
|
||||||
border-style: solid;
|
|
||||||
border-width: .5em .5em 0 .5em;
|
|
||||||
border-color: #232222 transparent transparent transparent;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
<svg version="1.1" fill="#7289da" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
width="438.533px" height="438.533px" viewBox="0 0 438.533 438.533" style="enable-background:new 0 0 438.533 438.533;"
|
||||||
|
xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M396.283,130.188c-3.806-9.135-8.371-16.365-13.703-21.695l-89.078-89.081c-5.332-5.325-12.56-9.895-21.697-13.704
|
||||||
|
C262.672,1.903,254.297,0,246.687,0H63.953C56.341,0,49.869,2.663,44.54,7.993c-5.33,5.327-7.994,11.799-7.994,19.414v383.719
|
||||||
|
c0,7.617,2.664,14.089,7.994,19.417c5.33,5.325,11.801,7.991,19.414,7.991h310.633c7.611,0,14.079-2.666,19.407-7.991
|
||||||
|
c5.328-5.332,7.994-11.8,7.994-19.417V155.313C401.991,147.699,400.088,139.323,396.283,130.188z M255.816,38.826
|
||||||
|
c5.517,1.903,9.418,3.999,11.704,6.28l89.366,89.366c2.279,2.286,4.374,6.186,6.276,11.706H255.816V38.826z M365.449,401.991
|
||||||
|
H73.089V36.545h146.178v118.771c0,7.614,2.662,14.084,7.992,19.414c5.332,5.327,11.8,7.994,19.417,7.994h118.773V401.991z"/>
|
||||||
|
<path d="M319.77,292.355h-201c-2.663,0-4.853,0.855-6.567,2.566c-1.709,1.711-2.568,3.901-2.568,6.563v18.274
|
||||||
|
c0,2.67,0.856,4.859,2.568,6.57c1.715,1.711,3.905,2.567,6.567,2.567h201c2.663,0,4.854-0.856,6.564-2.567s2.566-3.9,2.566-6.57
|
||||||
|
v-18.274c0-2.662-0.855-4.853-2.566-6.563C324.619,293.214,322.429,292.355,319.77,292.355z"/>
|
||||||
|
<path d="M112.202,221.831c-1.709,1.712-2.568,3.901-2.568,6.571v18.271c0,2.666,0.856,4.856,2.568,6.567
|
||||||
|
c1.715,1.711,3.905,2.566,6.567,2.566h201c2.663,0,4.854-0.855,6.564-2.566s2.566-3.901,2.566-6.567v-18.271
|
||||||
|
c0-2.663-0.855-4.854-2.566-6.571c-1.715-1.709-3.905-2.564-6.564-2.564h-201C116.107,219.267,113.917,220.122,112.202,221.831z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 18 KiB |
|
@ -0,0 +1,7 @@
|
||||||
|
<svg version="1.1" fill="#7289da" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
width="16px" height="16px" viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;"
|
||||||
|
xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<path fill="#7289da" d="M13.801 3.864h-5.693v-0.752c0-0.593-0.481-1.074-1.074-1.074h-4.834c-0.593 0-1.074 0.481-1.074 1.074v9.775c0 0.593 0.481 1.074 1.074 1.074h11.602c0.593 0 1.074-0.481 1.074-1.074v-7.949c-0-0.593-0.481-1.074-1.074-1.074z"></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 518 B |
|
@ -0,0 +1,5 @@
|
||||||
|
<svg fill="#7289da" style="enable-background:new 0 0 16 16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m13.801 3.864h-5.693v-.752c0-.593-.481-1.074-1.074-1.074h-4.834c-.593 0-1.074.481-1.074 1.074v9.775c0 .593.481 1.074 1.074 1.074h11.602c.593 0 1.074-.481 1.074-1.074v-7.949c-0-.593-.481-1.074-1.074-1.074z" fill="#7289da"/>
|
||||||
|
<path d="m6.751 7.5653c0-.352.123-.651.368-.896s.54-.369.885-.369.638.123.882.369c.243.246.364.545.364.896v.952h.832v-.952c0-.383-.093-.735-.28-1.058s-.439-.579-.757-.768c-.319-.189-.667-.284-1.044-.284s-.725.095-1.044.284-.571.445-.758.768-.28.676-.28 1.058v.952h.833v-.952z" fill="#f2f2f2"/>
|
||||||
|
<path d="m5.409 8.4193h5.185c.076.017.143.054.2.112.08.081.12.181.12.3v2.953c0 .118-.04.219-.12.3s-.178.122-.296.122h-4.997c-.113 0-.21-.04-.293-.122s-.124-.182-.124-.3v-2.953c0-.119.041-.218.124-.3.059-.058.125-.095.2-.112zm3.047 1.149c0-.169-.137-.307-.307-.307h-.298c-.169 0-.307.138-.307.307v1.586c0 .169.138.307.307.307h.298c.169 0 .307-.137.307-.307z" fill="#f2f2f2"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
|
@ -6,6 +6,7 @@ import {ServerSettings, Settings, settings, StaticSettings} from "tc-shared/sett
|
||||||
import {Sound, SoundManager} from "tc-shared/sound/Sounds";
|
import {Sound, SoundManager} from "tc-shared/sound/Sounds";
|
||||||
import {LocalClientEntry} from "tc-shared/ui/client";
|
import {LocalClientEntry} from "tc-shared/ui/client";
|
||||||
import * as server_log from "tc-shared/ui/frames/server_log";
|
import * as server_log from "tc-shared/ui/frames/server_log";
|
||||||
|
import {ServerLog} from "tc-shared/ui/frames/server_log";
|
||||||
import {ConnectionProfile, default_profile, find_profile} from "tc-shared/profiles/ConnectionProfile";
|
import {ConnectionProfile, default_profile, find_profile} from "tc-shared/profiles/ConnectionProfile";
|
||||||
import {ServerAddress} from "tc-shared/ui/server";
|
import {ServerAddress} from "tc-shared/ui/server";
|
||||||
import * as log from "tc-shared/log";
|
import * as log from "tc-shared/log";
|
||||||
|
@ -16,10 +17,8 @@ import {HandshakeHandler} from "tc-shared/connection/HandshakeHandler";
|
||||||
import * as htmltags from "./ui/htmltags";
|
import * as htmltags from "./ui/htmltags";
|
||||||
import {ChannelEntry} from "tc-shared/ui/channel";
|
import {ChannelEntry} from "tc-shared/ui/channel";
|
||||||
import {InputStartResult, InputState} from "tc-shared/voice/RecorderBase";
|
import {InputStartResult, InputState} from "tc-shared/voice/RecorderBase";
|
||||||
import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration";
|
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||||
import {guid} from "tc-shared/crypto/uid";
|
|
||||||
import * as bipc from "./BrowserIPC";
|
import * as bipc from "./BrowserIPC";
|
||||||
import {FileManager, transfer_provider, UploadKey} from "tc-shared/file/FileManager";
|
|
||||||
import {RecorderProfile} from "tc-shared/voice/RecorderProfile";
|
import {RecorderProfile} from "tc-shared/voice/RecorderProfile";
|
||||||
import {Frame} from "tc-shared/ui/frames/chat_frame";
|
import {Frame} from "tc-shared/ui/frames/chat_frame";
|
||||||
import {Hostbanner} from "tc-shared/ui/frames/hostbanner";
|
import {Hostbanner} from "tc-shared/ui/frames/hostbanner";
|
||||||
|
@ -31,7 +30,11 @@ import * as connection from "tc-backend/connection";
|
||||||
import * as dns from "tc-backend/dns";
|
import * as dns from "tc-backend/dns";
|
||||||
import * as top_menu from "tc-shared/ui/frames/MenuBar";
|
import * as top_menu from "tc-shared/ui/frames/MenuBar";
|
||||||
import {EventHandler, Registry} from "tc-shared/events";
|
import {EventHandler, Registry} from "tc-shared/events";
|
||||||
import {ServerLog} from "tc-shared/ui/frames/server_log";
|
import {FileManager} from "tc-shared/file/FileManager";
|
||||||
|
import {FileTransferState, TransferProvider} from "tc-shared/file/Transfer";
|
||||||
|
import {guid} from "tc-shared/crypto/uid";
|
||||||
|
import {traj} from "tc-shared/i18n/localize";
|
||||||
|
import {md5} from "tc-shared/crypto/md5";
|
||||||
|
|
||||||
export enum DisconnectReason {
|
export enum DisconnectReason {
|
||||||
HANDLER_DESTROYED,
|
HANDLER_DESTROYED,
|
||||||
|
@ -871,51 +874,35 @@ export class ConnectionHandler {
|
||||||
} else {
|
} else {
|
||||||
log.info(LogCategory.CLIENT, tr("Uploading new avatar"));
|
log.info(LogCategory.CLIENT, tr("Uploading new avatar"));
|
||||||
(async () => {
|
(async () => {
|
||||||
let key: UploadKey;
|
const transfer = this.fileManager.initializeFileUpload({
|
||||||
try {
|
name: "/avatar",
|
||||||
key = await this.fileManager.upload_file({
|
path: "",
|
||||||
size: data.byteLength,
|
|
||||||
path: '',
|
channel: 0,
|
||||||
name: '/avatar',
|
channelPassword: undefined,
|
||||||
overwrite: true,
|
|
||||||
channel: undefined,
|
source: async () => await TransferProvider.provider().createBufferSource(data)
|
||||||
channel_password: undefined
|
});
|
||||||
});
|
|
||||||
} catch(error) {
|
await transfer.awaitFinished();
|
||||||
log.error(LogCategory.GENERAL, tr("Failed to initialize avatar upload: %o"), error);
|
|
||||||
let message;
|
if(transfer.transferState() !== FileTransferState.FINISHED) {
|
||||||
if(error instanceof CommandResult) {
|
if(transfer.transferState() === FileTransferState.ERRORED) {
|
||||||
//TODO: Resolve permission name
|
log.warn(LogCategory.FILE_TRANSFER, tr("Failed to upload clients avatar: %o"), transfer.currentError());
|
||||||
//i_client_max_avatar_filesize
|
createErrorModal(tr("Failed to upload avatar"), traj("Failed to upload avatar:{:br:}{0}", transfer.currentErrorMessage())).open();
|
||||||
if(error.id == ErrorID.PERMISSION_ERROR) {
|
return;
|
||||||
message = formatMessage(tr("Failed to initialize avatar upload.{:br:}Missing permission {0}"), error["failed_permid"]);
|
} else if(transfer.transferState() === FileTransferState.CANCELED) {
|
||||||
} else {
|
createErrorModal(tr("Failed to upload avatar"), tr("Your avatar upload has been canceled.")).open();
|
||||||
message = formatMessage(tr("Failed to initialize avatar upload.{:br:}Error: {0}"), error.extra_message || error.message);
|
return;
|
||||||
}
|
} else {
|
||||||
|
createErrorModal(tr("Failed to upload avatar"), tr("Avatar upload finished with an unknown finished state.")).open();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if(!message)
|
|
||||||
message = formatMessage(tr("Failed to initialize avatar upload.{:br:}Lookup the console for more details"));
|
|
||||||
createErrorModal(tr("Failed to upload avatar"), message).open();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
await transfer_provider().spawn_upload_transfer(key).put_data(data);
|
|
||||||
} catch(error) {
|
|
||||||
log.error(LogCategory.GENERAL, tr("Failed to upload avatar: %o"), error);
|
|
||||||
|
|
||||||
let message;
|
|
||||||
if(typeof(error) === "string")
|
|
||||||
message = formatMessage(tr("Failed to upload avatar.{:br:}Error: {0}"), error);
|
|
||||||
|
|
||||||
if(!message)
|
|
||||||
message = formatMessage(tr("Failed to initialize avatar upload.{:br:}Lookup the console for more details"));
|
|
||||||
createErrorModal(tr("Failed to upload avatar"), message).open();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
await this.serverConnection.send_command('clientupdate', {
|
await this.serverConnection.send_command('clientupdate', {
|
||||||
client_flag_avatar: guid()
|
client_flag_avatar: md5(new Uint8Array(data))
|
||||||
});
|
});
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
log.error(LogCategory.GENERAL, tr("Failed to update avatar flag: %o"), error);
|
log.error(LogCategory.GENERAL, tr("Failed to update avatar flag: %o"), error);
|
||||||
|
|
|
@ -161,9 +161,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCommandResult(json) {
|
handleCommandResult(json) {
|
||||||
json = json[0]; //Only one bulk
|
let code : string = json[0]["return_code"];
|
||||||
|
|
||||||
let code : string = json["return_code"];
|
|
||||||
if(!code || code.length == 0) {
|
if(!code || code.length == 0) {
|
||||||
log.warn(LogCategory.NETWORKING, tr("Invalid return code! (%o)"), json);
|
log.warn(LogCategory.NETWORKING, tr("Invalid return code! (%o)"), json);
|
||||||
return;
|
return;
|
||||||
|
@ -512,7 +510,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
||||||
attach: true
|
attach: true
|
||||||
});
|
});
|
||||||
if(conversation)
|
if(conversation)
|
||||||
client.flag_text_unread = conversation.is_unread();
|
client.setUnread(conversation.is_unread());
|
||||||
}
|
}
|
||||||
|
|
||||||
if(client instanceof LocalClientEntry) {
|
if(client instanceof LocalClientEntry) {
|
||||||
|
|
|
@ -18,6 +18,38 @@ export enum ErrorID {
|
||||||
CONVERSATION_IS_PRIVATE = 0x2202
|
CONVERSATION_IS_PRIVATE = 0x2202
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ErrorCode {
|
||||||
|
FILE_INVALID_NAME = 0X800,
|
||||||
|
FILE_INVALID_PERMISSIONS = 0X801,
|
||||||
|
FILE_ALREADY_EXISTS = 0X802,
|
||||||
|
FILE_NOT_FOUND = 0X803,
|
||||||
|
FILE_IO_ERROR = 0X804,
|
||||||
|
FILE_INVALID_TRANSFER_ID = 0X805,
|
||||||
|
FILE_INVALID_PATH = 0X806,
|
||||||
|
FILE_NO_FILES_AVAILABLE = 0X807,
|
||||||
|
FILE_OVERWRITE_EXCLUDES_RESUME = 0X808,
|
||||||
|
FILE_INVALID_SIZE = 0X809,
|
||||||
|
FILE_ALREADY_IN_USE = 0X80A,
|
||||||
|
FILE_COULD_NOT_OPEN_CONNECTION = 0X80B,
|
||||||
|
FILE_NO_SPACE_LEFT_ON_DEVICE = 0X80C,
|
||||||
|
FILE_EXCEEDS_FILE_SYSTEM_MAXIMUM_SIZE = 0X80D,
|
||||||
|
FILE_TRANSFER_CONNECTION_TIMEOUT = 0X80E,
|
||||||
|
FILE_CONNECTION_LOST = 0X80F,
|
||||||
|
FILE_EXCEEDS_SUPPLIED_SIZE = 0X810,
|
||||||
|
FILE_TRANSFER_COMPLETE = 0X811,
|
||||||
|
FILE_TRANSFER_CANCELED = 0X812,
|
||||||
|
FILE_TRANSFER_INTERRUPTED = 0X813,
|
||||||
|
FILE_TRANSFER_SERVER_QUOTA_EXCEEDED = 0X814,
|
||||||
|
FILE_TRANSFER_CLIENT_QUOTA_EXCEEDED = 0X815,
|
||||||
|
FILE_TRANSFER_RESET = 0X816,
|
||||||
|
FILE_TRANSFER_LIMIT_REACHED = 0X817,
|
||||||
|
|
||||||
|
FILE_API_TIMEOUT = 0X820,
|
||||||
|
FILE_VIRTUAL_SERVER_NOT_REGISTERED = 0X821,
|
||||||
|
FILE_SERVER_TRANSFER_LIMIT_REACHED = 0X822,
|
||||||
|
FILE_CLIENT_TRANSFER_LIMIT_REACHED = 0X823,
|
||||||
|
}
|
||||||
|
|
||||||
export class CommandResult {
|
export class CommandResult {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -26,16 +58,26 @@ export class CommandResult {
|
||||||
|
|
||||||
json: any;
|
json: any;
|
||||||
|
|
||||||
constructor(json) {
|
bulks: any[];
|
||||||
this.json = json;
|
|
||||||
this.id = parseInt(json["id"]);
|
|
||||||
this.message = json["msg"];
|
|
||||||
|
|
||||||
this.extra_message = "";
|
constructor(bulks) {
|
||||||
if(json["extra_msg"]) this.extra_message = json["extra_msg"];
|
this.bulks = bulks;
|
||||||
|
|
||||||
|
this.json = bulks[0];
|
||||||
|
this.id = parseInt(this.json["id"]);
|
||||||
|
this.message = this.json["msg"];
|
||||||
|
|
||||||
|
this.extra_message = this.json["extra_msg"] || "";
|
||||||
this.success = this.id == 0;
|
this.success = this.id == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getBulks() : CommandResult[] {
|
||||||
|
return this.bulks.map(e => new CommandResult([e]));
|
||||||
|
}
|
||||||
|
|
||||||
|
formattedMessage() {
|
||||||
|
return this.extra_message ? this.message + " (" + this.extra_message + ")" : this.message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClientNameInfo {
|
export interface ClientNameInfo {
|
||||||
|
|
|
@ -0,0 +1,172 @@
|
||||||
|
export function md5(uint8Array: Uint8Array) : string {
|
||||||
|
function md5cycle(x, k) {
|
||||||
|
var a = x[0], b = x[1], c = x[2], d = x[3];
|
||||||
|
|
||||||
|
a = ff(a, b, c, d, k[0], 7, -680876936);
|
||||||
|
d = ff(d, a, b, c, k[1], 12, -389564586);
|
||||||
|
c = ff(c, d, a, b, k[2], 17, 606105819);
|
||||||
|
b = ff(b, c, d, a, k[3], 22, -1044525330);
|
||||||
|
a = ff(a, b, c, d, k[4], 7, -176418897);
|
||||||
|
d = ff(d, a, b, c, k[5], 12, 1200080426);
|
||||||
|
c = ff(c, d, a, b, k[6], 17, -1473231341);
|
||||||
|
b = ff(b, c, d, a, k[7], 22, -45705983);
|
||||||
|
a = ff(a, b, c, d, k[8], 7, 1770035416);
|
||||||
|
d = ff(d, a, b, c, k[9], 12, -1958414417);
|
||||||
|
c = ff(c, d, a, b, k[10], 17, -42063);
|
||||||
|
b = ff(b, c, d, a, k[11], 22, -1990404162);
|
||||||
|
a = ff(a, b, c, d, k[12], 7, 1804603682);
|
||||||
|
d = ff(d, a, b, c, k[13], 12, -40341101);
|
||||||
|
c = ff(c, d, a, b, k[14], 17, -1502002290);
|
||||||
|
b = ff(b, c, d, a, k[15], 22, 1236535329);
|
||||||
|
|
||||||
|
a = gg(a, b, c, d, k[1], 5, -165796510);
|
||||||
|
d = gg(d, a, b, c, k[6], 9, -1069501632);
|
||||||
|
c = gg(c, d, a, b, k[11], 14, 643717713);
|
||||||
|
b = gg(b, c, d, a, k[0], 20, -373897302);
|
||||||
|
a = gg(a, b, c, d, k[5], 5, -701558691);
|
||||||
|
d = gg(d, a, b, c, k[10], 9, 38016083);
|
||||||
|
c = gg(c, d, a, b, k[15], 14, -660478335);
|
||||||
|
b = gg(b, c, d, a, k[4], 20, -405537848);
|
||||||
|
a = gg(a, b, c, d, k[9], 5, 568446438);
|
||||||
|
d = gg(d, a, b, c, k[14], 9, -1019803690);
|
||||||
|
c = gg(c, d, a, b, k[3], 14, -187363961);
|
||||||
|
b = gg(b, c, d, a, k[8], 20, 1163531501);
|
||||||
|
a = gg(a, b, c, d, k[13], 5, -1444681467);
|
||||||
|
d = gg(d, a, b, c, k[2], 9, -51403784);
|
||||||
|
c = gg(c, d, a, b, k[7], 14, 1735328473);
|
||||||
|
b = gg(b, c, d, a, k[12], 20, -1926607734);
|
||||||
|
|
||||||
|
a = hh(a, b, c, d, k[5], 4, -378558);
|
||||||
|
d = hh(d, a, b, c, k[8], 11, -2022574463);
|
||||||
|
c = hh(c, d, a, b, k[11], 16, 1839030562);
|
||||||
|
b = hh(b, c, d, a, k[14], 23, -35309556);
|
||||||
|
a = hh(a, b, c, d, k[1], 4, -1530992060);
|
||||||
|
d = hh(d, a, b, c, k[4], 11, 1272893353);
|
||||||
|
c = hh(c, d, a, b, k[7], 16, -155497632);
|
||||||
|
b = hh(b, c, d, a, k[10], 23, -1094730640);
|
||||||
|
a = hh(a, b, c, d, k[13], 4, 681279174);
|
||||||
|
d = hh(d, a, b, c, k[0], 11, -358537222);
|
||||||
|
c = hh(c, d, a, b, k[3], 16, -722521979);
|
||||||
|
b = hh(b, c, d, a, k[6], 23, 76029189);
|
||||||
|
a = hh(a, b, c, d, k[9], 4, -640364487);
|
||||||
|
d = hh(d, a, b, c, k[12], 11, -421815835);
|
||||||
|
c = hh(c, d, a, b, k[15], 16, 530742520);
|
||||||
|
b = hh(b, c, d, a, k[2], 23, -995338651);
|
||||||
|
|
||||||
|
a = ii(a, b, c, d, k[0], 6, -198630844);
|
||||||
|
d = ii(d, a, b, c, k[7], 10, 1126891415);
|
||||||
|
c = ii(c, d, a, b, k[14], 15, -1416354905);
|
||||||
|
b = ii(b, c, d, a, k[5], 21, -57434055);
|
||||||
|
a = ii(a, b, c, d, k[12], 6, 1700485571);
|
||||||
|
d = ii(d, a, b, c, k[3], 10, -1894986606);
|
||||||
|
c = ii(c, d, a, b, k[10], 15, -1051523);
|
||||||
|
b = ii(b, c, d, a, k[1], 21, -2054922799);
|
||||||
|
a = ii(a, b, c, d, k[8], 6, 1873313359);
|
||||||
|
d = ii(d, a, b, c, k[15], 10, -30611744);
|
||||||
|
c = ii(c, d, a, b, k[6], 15, -1560198380);
|
||||||
|
b = ii(b, c, d, a, k[13], 21, 1309151649);
|
||||||
|
a = ii(a, b, c, d, k[4], 6, -145523070);
|
||||||
|
d = ii(d, a, b, c, k[11], 10, -1120210379);
|
||||||
|
c = ii(c, d, a, b, k[2], 15, 718787259);
|
||||||
|
b = ii(b, c, d, a, k[9], 21, -343485551);
|
||||||
|
|
||||||
|
x[0] = add32(a, x[0]);
|
||||||
|
x[1] = add32(b, x[1]);
|
||||||
|
x[2] = add32(c, x[2]);
|
||||||
|
x[3] = add32(d, x[3]);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmn(q, a, b, x, s, t) {
|
||||||
|
a = add32(add32(a, q), add32(x, t));
|
||||||
|
return add32((a << s) | (a >>> (32 - s)), b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ff(a, b, c, d, x, s, t) {
|
||||||
|
return cmn((b & c) | ((~b) & d), a, b, x, s, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
function gg(a, b, c, d, x, s, t) {
|
||||||
|
return cmn((b & d) | (c & (~d)), a, b, x, s, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hh(a, b, c, d, x, s, t) {
|
||||||
|
return cmn(b ^ c ^ d, a, b, x, s, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ii(a, b, c, d, x, s, t) {
|
||||||
|
return cmn(c ^ (b | (~d)), a, b, x, s, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
function md51(s) {
|
||||||
|
var n = s.length,
|
||||||
|
state = [1732584193, -271733879, -1732584194, 271733878], i;
|
||||||
|
for (i = 64; i <= s.length; i += 64) {
|
||||||
|
md5cycle(state, md5blk(s.subarray(i - 64, i)));
|
||||||
|
}
|
||||||
|
s = s.subarray(i - 64);
|
||||||
|
var tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||||
|
for (i = 0; i < s.length; i++)
|
||||||
|
tail[i >> 2] |= s[i] << ((i % 4) << 3);
|
||||||
|
tail[i >> 2] |= 0x80 << ((i % 4) << 3);
|
||||||
|
if (i > 55) {
|
||||||
|
md5cycle(state, tail);
|
||||||
|
for (i = 0; i < 16; i++) tail[i] = 0;
|
||||||
|
}
|
||||||
|
tail[14] = n * 8;
|
||||||
|
md5cycle(state, tail);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* there needs to be support for Unicode here,
|
||||||
|
* unless we pretend that we can redefine the MD-5
|
||||||
|
* algorithm for multi-byte characters (perhaps
|
||||||
|
* by adding every four 16-bit characters and
|
||||||
|
* shortening the sum to 32 bits). Otherwise
|
||||||
|
* I suggest performing MD-5 as if every character
|
||||||
|
* was two bytes--e.g., 0040 0025 = @%--but then
|
||||||
|
* how will an ordinary MD-5 sum be matched?
|
||||||
|
* There is no way to standardize text to something
|
||||||
|
* like UTF-8 before transformation; speed cost is
|
||||||
|
* utterly prohibitive. The JavaScript standard
|
||||||
|
* itself needs to look at this: it should start
|
||||||
|
* providing access to strings as preformed UTF-8
|
||||||
|
* 8-bit unsigned value arrays.
|
||||||
|
*/
|
||||||
|
function md5blk(s) { /* I figured global was faster. */
|
||||||
|
var md5blks = [], i; /* Andy King said do it this way. */
|
||||||
|
for (i = 0; i < 64; i += 4) {
|
||||||
|
md5blks[i >> 2] = s[i]
|
||||||
|
+ (s[i + 1] << 8)
|
||||||
|
+ (s[i + 2] << 16)
|
||||||
|
+ (s[i + 3] << 24);
|
||||||
|
}
|
||||||
|
return md5blks;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hex_chr = '0123456789abcdef'.split('');
|
||||||
|
|
||||||
|
function rhex(n) {
|
||||||
|
var s = '', j = 0;
|
||||||
|
for (; j < 4; j++)
|
||||||
|
s += hex_chr[(n >> (j * 8 + 4)) & 0x0F]
|
||||||
|
+ hex_chr[(n >> (j * 8)) & 0x0F];
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hex(x) {
|
||||||
|
for (var i = 0; i < x.length; i++)
|
||||||
|
x[i] = rhex(x[i]);
|
||||||
|
return x.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function md5(s) {
|
||||||
|
return hex(md51(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
function add32(a, b) {
|
||||||
|
return (a + b) & 0xFFFFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
return md5(uint8Array);
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import {ClientEvents, MusicClientEntry, SongInfo} from "tc-shared/ui/client";
|
import {ClientEvents, MusicClientEntry, SongInfo} from "tc-shared/ui/client";
|
||||||
import {guid} from "tc-shared/crypto/uid";
|
import {guid} from "tc-shared/crypto/uid";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import {useEffect} from "react";
|
||||||
|
|
||||||
export interface Event<Events, T = keyof Events> {
|
export interface Event<Events, T = keyof Events> {
|
||||||
readonly type: T;
|
readonly type: T;
|
||||||
|
@ -91,6 +92,20 @@ export class Registry<Events> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* special helper methods for react components */
|
||||||
|
reactUse<T extends keyof Events>(event: T, handler: (event?: Events[T] & Event<Events, T>) => void, condition?: boolean) {
|
||||||
|
if(typeof condition === "boolean" && !condition) {
|
||||||
|
useEffect(() => {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const handlers = this.handler[event as any] || (this.handler[event as any] = []);
|
||||||
|
useEffect(() => {
|
||||||
|
handlers.push(handler);
|
||||||
|
return () => handlers.remove(handler);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
connect<EOther, T extends keyof Events & keyof EOther>(events: T | T[], target: Registry<EOther>) {
|
connect<EOther, T extends keyof Events & keyof EOther>(events: T | T[], target: Registry<EOther>) {
|
||||||
for(const event of Array.isArray(events) ? events : [events])
|
for(const event of Array.isArray(events) ? events : [events])
|
||||||
(this.connections[event as string] || (this.connections[event as string] = [])).push(target as any);
|
(this.connections[event as string] || (this.connections[event as string] = [])).push(target as any);
|
||||||
|
@ -178,8 +193,10 @@ export class Registry<Events> {
|
||||||
this.on(event, ev_handler);
|
this.on(event, ev_handler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(Object.keys(registered_events).length === 0)
|
if(Object.keys(registered_events).length === 0) {
|
||||||
throw "no events found in event handler";
|
console.warn(tr("no events found in event handler"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.event_handler_objects.push({
|
this.event_handler_objects.push({
|
||||||
handlers: registered_events,
|
handlers: registered_events,
|
||||||
|
@ -189,7 +206,8 @@ export class Registry<Events> {
|
||||||
|
|
||||||
unregister_handler(handler: any) {
|
unregister_handler(handler: any) {
|
||||||
const data = this.event_handler_objects.find(e => e.object === handler);
|
const data = this.event_handler_objects.find(e => e.object === handler);
|
||||||
if(!data) throw "unknown event handler";
|
if(!data) return;
|
||||||
|
|
||||||
this.event_handler_objects.remove(data);
|
this.event_handler_objects.remove(data);
|
||||||
|
|
||||||
for(const key of Object.keys(data.handlers)) {
|
for(const key of Object.keys(data.handlers)) {
|
||||||
|
|
|
@ -2,11 +2,18 @@ import * as log from "tc-shared/log";
|
||||||
import {LogCategory} from "tc-shared/log";
|
import {LogCategory} from "tc-shared/log";
|
||||||
import {ClientEntry} from "tc-shared/ui/client";
|
import {ClientEntry} from "tc-shared/ui/client";
|
||||||
import * as hex from "tc-shared/crypto/hex";
|
import * as hex from "tc-shared/crypto/hex";
|
||||||
import {
|
|
||||||
DownloadKey,
|
|
||||||
FileManager, transfer_provider
|
|
||||||
} from "tc-shared/file/FileManager";
|
|
||||||
import {image_type, ImageCache, ImageType, media_image_type} from "tc-shared/file/ImageCache";
|
import {image_type, ImageCache, ImageType, media_image_type} from "tc-shared/file/ImageCache";
|
||||||
|
import {FileManager} from "tc-shared/file/FileManager";
|
||||||
|
import {
|
||||||
|
FileDownloadTransfer,
|
||||||
|
FileTransferState,
|
||||||
|
ResponseTransferTarget, TransferProvider,
|
||||||
|
TransferTargetType
|
||||||
|
} from "tc-shared/file/Transfer";
|
||||||
|
import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||||
|
import {tra} from "tc-shared/i18n/localize";
|
||||||
|
import {server_connections} from "tc-shared/ui/frames/connection_handlers";
|
||||||
|
import {icon_cache_loader} from "tc-shared/file/Icons";
|
||||||
|
|
||||||
export class Avatar {
|
export class Avatar {
|
||||||
client_avatar_id: string; /* the base64 uid thing from a-m */
|
client_avatar_id: string; /* the base64 uid thing from a-m */
|
||||||
|
@ -47,11 +54,11 @@ export class AvatarManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
async resolved_cached?(client_avatar_id: string, avatar_version?: string) : Promise<Avatar> {
|
async resolved_cached?(client_avatar_id: string, avatar_version?: string) : Promise<Avatar> {
|
||||||
let avatar: Avatar = this._cached_avatars[avatar_version];
|
let cachedAvatar: Avatar = this._cached_avatars[avatar_version];
|
||||||
if(avatar) {
|
if(cachedAvatar) {
|
||||||
if(typeof(avatar_version) !== "string" || avatar.avatar_id == avatar_version)
|
if(typeof(avatar_version) !== "string" || cachedAvatar.avatar_id == avatar_version)
|
||||||
return avatar;
|
return cachedAvatar;
|
||||||
avatar = undefined;
|
delete this._cached_avatars[avatar_version];
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!AvatarManager.cache.setupped())
|
if(!AvatarManager.cache.setupped())
|
||||||
|
@ -74,37 +81,66 @@ export class AvatarManager {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
create_avatar_download(client_avatar_id: string) : Promise<DownloadKey> {
|
create_avatar_download(client_avatar_id: string) : FileDownloadTransfer {
|
||||||
log.debug(LogCategory.GENERAL, "Requesting download for avatar %s", client_avatar_id);
|
log.debug(LogCategory.GENERAL, "Requesting download for avatar %s", client_avatar_id);
|
||||||
return this.handle.download_file("", "/avatar_" + client_avatar_id);
|
|
||||||
|
return this.handle.initializeFileDownload({
|
||||||
|
path: "",
|
||||||
|
name: "/avatar_" + client_avatar_id,
|
||||||
|
targetSupplier: async () => await TransferProvider.provider().createResponseTarget()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _load_avatar(client_avatar_id: string, avatar_version: string) {
|
private async _load_avatar(client_avatar_id: string, avatar_version: string) {
|
||||||
try {
|
try {
|
||||||
let download_key: DownloadKey;
|
let transfer = this.create_avatar_download(client_avatar_id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
download_key = await this.create_avatar_download(client_avatar_id);
|
await transfer.awaitFinished();
|
||||||
|
|
||||||
|
if(transfer.transferState() === FileTransferState.CANCELED) {
|
||||||
|
throw tr("download canceled");
|
||||||
|
} else if(transfer.transferState() === FileTransferState.ERRORED) {
|
||||||
|
throw transfer.currentError();
|
||||||
|
} else if(transfer.transferState() === FileTransferState.FINISHED) {
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw tr("Unknown transfer finished state");
|
||||||
|
}
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
log.error(LogCategory.GENERAL, tr("Could not request download for avatar %s: %o"), client_avatar_id, error);
|
if(typeof error === "object" && 'error' in error && error.error === "initialize") {
|
||||||
throw "failed to request avatar download";
|
const commandResult = error.commandResult;
|
||||||
|
if(commandResult instanceof CommandResult) {
|
||||||
|
if(commandResult.id === ErrorID.FILE_NOT_FOUND)
|
||||||
|
throw tr("Avatar could not be found");
|
||||||
|
else if(commandResult.id === ErrorID.PERMISSION_ERROR)
|
||||||
|
throw tr("No permissions to download avatar");
|
||||||
|
else
|
||||||
|
throw commandResult.message + (commandResult.extra_message ? " (" + commandResult.extra_message + ")" : "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.error(LogCategory.CLIENT, tr("Could not request download for avatar %s: %o"), client_avatar_id, error);
|
||||||
|
if(error === transfer.currentError())
|
||||||
|
throw transfer.currentErrorMessage();
|
||||||
|
throw typeof error === "string" ? error : tr("Avatar download failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
const downloader = transfer_provider().spawn_download_transfer(download_key);
|
/* could only be tested here, because before we don't know which target we have */
|
||||||
let response: Response;
|
if(transfer.target.type !== TransferTargetType.RESPONSE)
|
||||||
try {
|
throw "unsupported transfer target";
|
||||||
response = await downloader.request_file();
|
|
||||||
} catch(error) {
|
|
||||||
log.error(LogCategory.GENERAL, tr("Could not download avatar %s: %o"), client_avatar_id, error);
|
|
||||||
throw "failed to download avatar";
|
|
||||||
}
|
|
||||||
|
|
||||||
const type = image_type(response.headers.get('X-media-bytes'));
|
const response = transfer.target as ResponseTransferTarget;
|
||||||
|
if(!response.hasResponse())
|
||||||
|
throw tr("Transfer has no response");
|
||||||
|
|
||||||
|
const type = image_type(response.getResponse().headers.get('X-media-bytes'));
|
||||||
const media = media_image_type(type);
|
const media = media_image_type(type);
|
||||||
|
|
||||||
await AvatarManager.cache.put_cache('avatar_' + client_avatar_id, response.clone(), "image/" + media, {
|
await AvatarManager.cache.put_cache('avatar_' + client_avatar_id, response.getResponse().clone(), "image/" + media, {
|
||||||
"X-avatar-version": avatar_version
|
"X-avatar-version": avatar_version
|
||||||
});
|
});
|
||||||
const url = await this._response_url(response.clone(), type);
|
const url = await this._response_url(response.getResponse().clone(), type);
|
||||||
|
|
||||||
return this._cached_avatars[client_avatar_id] = {
|
return this._cached_avatars[client_avatar_id] = {
|
||||||
client_avatar_id: client_avatar_id,
|
client_avatar_id: client_avatar_id,
|
||||||
|
@ -249,9 +285,9 @@ export class AvatarManager {
|
||||||
generate_chat_tag(client: { id?: number; database_id?: number; }, client_unique_id: string, callback_loaded?: (successfully: boolean, error?: any) => any) : JQuery {
|
generate_chat_tag(client: { id?: number; database_id?: number; }, client_unique_id: string, callback_loaded?: (successfully: boolean, error?: any) => any) : JQuery {
|
||||||
let client_handle;
|
let client_handle;
|
||||||
if(typeof(client.id) == "number")
|
if(typeof(client.id) == "number")
|
||||||
client_handle = this.handle.handle.channelTree.findClient(client.id);
|
client_handle = this.handle.connectionHandler.channelTree.findClient(client.id);
|
||||||
if(!client_handle && typeof(client.id) == "number") {
|
if(!client_handle && typeof(client.id) == "number") {
|
||||||
client_handle = this.handle.handle.channelTree.find_client_by_dbid(client.database_id);
|
client_handle = this.handle.connectionHandler.channelTree.find_client_by_dbid(client.database_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(client_handle && client_handle.clientUid() !== client_unique_id)
|
if(client_handle && client_handle.clientUid() !== client_unique_id)
|
||||||
|
@ -314,4 +350,14 @@ export class AvatarManager {
|
||||||
|
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flush_cache() {
|
||||||
|
this._cached_avatars = undefined;
|
||||||
|
this._loading_promises = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
(window as any).flush_avatar_cache = async () => {
|
||||||
|
server_connections.all_connections().forEach(e => {
|
||||||
|
e.fileManager.avatars.flush_cache();
|
||||||
|
});
|
||||||
|
};
|
|
@ -3,12 +3,14 @@ import {LogCategory} from "tc-shared/log";
|
||||||
import {Registry} from "tc-shared/events";
|
import {Registry} from "tc-shared/events";
|
||||||
import {format_time} from "tc-shared/ui/frames/chat";
|
import {format_time} from "tc-shared/ui/frames/chat";
|
||||||
import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration";
|
import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||||
import {
|
|
||||||
DownloadKey,
|
|
||||||
FileEntry,
|
|
||||||
FileManager, transfer_provider
|
|
||||||
} from "tc-shared/file/FileManager";
|
|
||||||
import {image_type, ImageCache, ImageType, media_image_type} from "tc-shared/file/ImageCache";
|
import {image_type, ImageCache, ImageType, media_image_type} from "tc-shared/file/ImageCache";
|
||||||
|
import {FileInfo, FileManager} from "tc-shared/file/FileManager";
|
||||||
|
import {
|
||||||
|
FileDownloadTransfer,
|
||||||
|
FileTransferState, ResponseTransferTarget, TransferProvider,
|
||||||
|
TransferTargetType
|
||||||
|
} from "tc-shared/file/Transfer";
|
||||||
|
import {server_connections} from "tc-shared/ui/frames/connection_handlers";
|
||||||
|
|
||||||
const icon_cache: ImageCache = new ImageCache("icons");
|
const icon_cache: ImageCache = new ImageCache("icons");
|
||||||
export interface IconManagerEvents {
|
export interface IconManagerEvents {
|
||||||
|
@ -215,6 +217,15 @@ window.addEventListener("beforeunload", () => {
|
||||||
icon_cache_loader.clear_memory_cache();
|
icon_cache_loader.clear_memory_cache();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
(window as any).flush_icon_cache = async () => {
|
||||||
|
icon_cache_loader.clear_memory_cache();
|
||||||
|
await icon_cache_loader.clear_cache();
|
||||||
|
|
||||||
|
server_connections.all_connections().forEach(e => {
|
||||||
|
e.fileManager.icons.flush_cache();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
type IconManagerLoadingData = {
|
type IconManagerLoadingData = {
|
||||||
result: "success" | "error" | "unset";
|
result: "success" | "error" | "unset";
|
||||||
next_retry?: number;
|
next_retry?: number;
|
||||||
|
@ -238,17 +249,21 @@ export class IconManager {
|
||||||
if(id <= 1000)
|
if(id <= 1000)
|
||||||
throw "invalid id!";
|
throw "invalid id!";
|
||||||
|
|
||||||
await this.handle.delete_file({
|
await this.handle.deleteFile({
|
||||||
name: '/icon_' + id
|
name: '/icon_' + id
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
iconList() : Promise<FileEntry[]> {
|
iconList() : Promise<FileInfo[]> {
|
||||||
return this.handle.requestFileList("/icons");
|
return this.handle.requestFileList("/icons");
|
||||||
}
|
}
|
||||||
|
|
||||||
create_icon_download(id: number) : Promise<DownloadKey> {
|
createIconDownload(id: number) : FileDownloadTransfer {
|
||||||
return this.handle.download_file("", "/icon_" + id);
|
return this.handle.initializeFileDownload({
|
||||||
|
path: "",
|
||||||
|
name: "/icon_" + id,
|
||||||
|
targetSupplier: async () => await TransferProvider.provider().createResponseTarget()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async server_icon_loader(icon: LocalIcon) : Promise<Response> {
|
private async server_icon_loader(icon: LocalIcon) : Promise<Response> {
|
||||||
|
@ -262,9 +277,20 @@ export class IconManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let download_key: DownloadKey;
|
let transfer = this.createIconDownload(icon.icon_id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
download_key = await this.create_icon_download(icon.icon_id);
|
await transfer.awaitFinished();
|
||||||
|
|
||||||
|
if(transfer.transferState() === FileTransferState.CANCELED) {
|
||||||
|
throw tr("download canceled");
|
||||||
|
} else if(transfer.transferState() === FileTransferState.ERRORED) {
|
||||||
|
throw transfer.currentError();
|
||||||
|
} else if(transfer.transferState() === FileTransferState.FINISHED) {
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw tr("Unknown transfer finished state");
|
||||||
|
}
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
if(error instanceof CommandResult) {
|
if(error instanceof CommandResult) {
|
||||||
if(error.id === ErrorID.FILE_NOT_FOUND)
|
if(error.id === ErrorID.FILE_NOT_FOUND)
|
||||||
|
@ -275,20 +301,21 @@ export class IconManager {
|
||||||
throw error.extra_message || error.message;
|
throw error.extra_message || error.message;
|
||||||
}
|
}
|
||||||
log.error(LogCategory.CLIENT, tr("Could not request download for icon %d: %o"), icon.icon_id, error);
|
log.error(LogCategory.CLIENT, tr("Could not request download for icon %d: %o"), icon.icon_id, error);
|
||||||
|
if(error === transfer.currentError())
|
||||||
|
throw transfer.currentErrorMessage();
|
||||||
throw typeof error === "string" ? error : tr("Failed to initialize icon download");
|
throw typeof error === "string" ? error : tr("Failed to initialize icon download");
|
||||||
}
|
}
|
||||||
|
|
||||||
const downloader = transfer_provider().spawn_download_transfer(download_key);
|
/* could only be tested here, because before we don't know which target we have */
|
||||||
let response: Response;
|
if(transfer.target.type !== TransferTargetType.RESPONSE)
|
||||||
try {
|
throw "unsupported transfer target";
|
||||||
response = await downloader.request_file();
|
|
||||||
} catch(error) {
|
const response = transfer.target as ResponseTransferTarget;
|
||||||
log.error(LogCategory.CLIENT, tr("Could not download icon %d: %o"), icon.icon_id, error);
|
if(!response.hasResponse())
|
||||||
throw "failed to download icon";
|
throw tr("Transfer has no response");
|
||||||
}
|
|
||||||
|
|
||||||
loading_data.result = "success";
|
loading_data.result = "success";
|
||||||
return response;
|
return response.getResponse();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
loading_data.result = "error";
|
loading_data.result = "error";
|
||||||
loading_data.error = error as string;
|
loading_data.error = error as string;
|
||||||
|
@ -365,7 +392,7 @@ export class IconManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
load_icon(id: number) : LocalIcon {
|
load_icon(id: number) : LocalIcon {
|
||||||
const server_uid = this.handle.handle.channelTree.server.properties.virtualserver_unique_identifier;
|
const server_uid = this.handle.connectionHandler.channelTree.server.properties.virtualserver_unique_identifier;
|
||||||
let icon = icon_cache_loader.load_icon(id, server_uid, this.server_icon_loader.bind(this));
|
let icon = icon_cache_loader.load_icon(id, server_uid, this.server_icon_loader.bind(this));
|
||||||
if(icon.status !== "loading" && icon.status !== "loaded") {
|
if(icon.status !== "loading" && icon.status !== "loaded") {
|
||||||
this.server_icon_loader(icon).then(response => {
|
this.server_icon_loader(icon).then(response => {
|
||||||
|
@ -376,4 +403,8 @@ export class IconManager {
|
||||||
}
|
}
|
||||||
return icon;
|
return icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flush_cache() {
|
||||||
|
this.loading_timestamps = {};
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,417 @@
|
||||||
|
import {Registry} from "tc-shared/events";
|
||||||
|
import {CommandResult, ErrorCode} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||||
|
|
||||||
|
/* Transfer source types */
|
||||||
|
export enum TransferSourceType {
|
||||||
|
BROWSER_FILE,
|
||||||
|
BUFFER,
|
||||||
|
TEXT
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class TransferSource {
|
||||||
|
readonly type: TransferSourceType;
|
||||||
|
|
||||||
|
protected constructor(type: TransferSourceType) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fileSize() : Promise<number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export abstract class BrowserFileTransferSource extends TransferSource {
|
||||||
|
protected constructor() {
|
||||||
|
super(TransferSourceType.BROWSER_FILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract getFile() : File;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class BufferTransferSource extends TransferSource {
|
||||||
|
protected constructor() {
|
||||||
|
super(TransferSourceType.BUFFER);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract getBuffer() : ArrayBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class TextTransferSource extends TransferSource {
|
||||||
|
protected constructor() {
|
||||||
|
super(TransferSourceType.TEXT);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract getText() : string;
|
||||||
|
}
|
||||||
|
export type TransferSourceSupplier = (transfer: FileUploadTransfer) => Promise<TransferSource>;
|
||||||
|
|
||||||
|
/* Transfer target types */
|
||||||
|
export enum TransferTargetType {
|
||||||
|
RESPONSE,
|
||||||
|
DOWNLOAD
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class TransferTarget {
|
||||||
|
readonly type: TransferTargetType;
|
||||||
|
|
||||||
|
protected constructor(type: TransferTargetType) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class DownloadTransferTarget extends TransferTarget {
|
||||||
|
protected constructor() {
|
||||||
|
super(TransferTargetType.DOWNLOAD);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class ResponseTransferTarget extends TransferTarget {
|
||||||
|
protected constructor() {
|
||||||
|
super(TransferTargetType.RESPONSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract hasResponse() : boolean;
|
||||||
|
abstract getResponse() : Response;
|
||||||
|
}
|
||||||
|
export type TransferTargetSupplier = (transfer: FileDownloadTransfer) => Promise<TransferTarget>;
|
||||||
|
|
||||||
|
export enum FileTransferState {
|
||||||
|
PENDING, /* bending because other transfers already going on */
|
||||||
|
INITIALIZING,
|
||||||
|
CONNECTING,
|
||||||
|
RUNNING,
|
||||||
|
|
||||||
|
FINISHED,
|
||||||
|
ERRORED,
|
||||||
|
CANCELED
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CancelReason {
|
||||||
|
USER_ACTION,
|
||||||
|
SERVER_DISCONNECTED
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum FileTransferDirection {
|
||||||
|
UPLOAD,
|
||||||
|
DOWNLOAD
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileTransferEvents {
|
||||||
|
"notify_state_updated": { oldState: FileTransferState, newState: FileTransferState },
|
||||||
|
"notify_progress": { progress: TransferProgress },
|
||||||
|
|
||||||
|
"action_request_cancel": { reason: CancelReason },
|
||||||
|
"notify_transfer_canceled": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransferProperties {
|
||||||
|
channel_id: number | 0;
|
||||||
|
path: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InitializedTransferProperties {
|
||||||
|
serverTransferId: number;
|
||||||
|
transferKey: string;
|
||||||
|
|
||||||
|
addresses: {
|
||||||
|
serverAddress: string;
|
||||||
|
serverPort: number;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
protocol: number; /* should be constant 1 */
|
||||||
|
|
||||||
|
seekOffset: number;
|
||||||
|
fileSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface TransferInitializeError {
|
||||||
|
error: "initialize";
|
||||||
|
|
||||||
|
commandResult: string | CommandResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransferConnectError {
|
||||||
|
error: "connection";
|
||||||
|
|
||||||
|
reason: "missing-provider" | "provider-initialize-error" | "network-error";
|
||||||
|
extraMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransferIOError {
|
||||||
|
error: "io";
|
||||||
|
|
||||||
|
reason: "unsupported-target" | "failed-to-initialize-target" | "buffer-transfer-failed";
|
||||||
|
extraMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransferErrorStatus {
|
||||||
|
error: "status";
|
||||||
|
|
||||||
|
status: ErrorCode;
|
||||||
|
extraMessage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransferErrorTimeout {
|
||||||
|
error: "timeout";
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TransferErrorType = TransferInitializeError | TransferConnectError | TransferIOError | TransferErrorStatus | TransferErrorTimeout;
|
||||||
|
|
||||||
|
export interface TransferProgress {
|
||||||
|
timestamp: number;
|
||||||
|
|
||||||
|
file_bytes_transferred: number;
|
||||||
|
file_current_offset: number;
|
||||||
|
file_start_offset: number;
|
||||||
|
file_total_size: number;
|
||||||
|
network_bytes_received: number;
|
||||||
|
network_bytes_send: number;
|
||||||
|
|
||||||
|
|
||||||
|
network_current_speed: number;
|
||||||
|
network_average_speed: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransferTimings {
|
||||||
|
timestampScheduled: number;
|
||||||
|
timestampExecuted: number;
|
||||||
|
timestampTransferBegin: number;
|
||||||
|
timestampEnd: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FinishedFileTransfer {
|
||||||
|
readonly clientTransferId: number;
|
||||||
|
readonly timings: TransferTimings;
|
||||||
|
|
||||||
|
readonly properties: TransferProperties;
|
||||||
|
readonly direction: FileTransferDirection;
|
||||||
|
|
||||||
|
readonly state: FileTransferState.CANCELED | FileTransferState.FINISHED | FileTransferState.ERRORED;
|
||||||
|
|
||||||
|
/* only set if state is ERRORED */
|
||||||
|
readonly transferError?: TransferErrorType;
|
||||||
|
readonly transferErrorMessage?: string;
|
||||||
|
|
||||||
|
readonly bytesTransferred: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FileTransfer {
|
||||||
|
readonly events: Registry<FileTransferEvents>;
|
||||||
|
readonly clientTransferId: number;
|
||||||
|
readonly direction: FileTransferDirection;
|
||||||
|
readonly properties: TransferProperties;
|
||||||
|
readonly timings: TransferTimings;
|
||||||
|
|
||||||
|
lastStateUpdate: number;
|
||||||
|
private cancelReason: CancelReason;
|
||||||
|
private transferProperties_: InitializedTransferProperties;
|
||||||
|
private transferError_: TransferErrorType;
|
||||||
|
private transferErrorMessage_: string;
|
||||||
|
private transferState_: FileTransferState;
|
||||||
|
private progress_: TransferProgress;
|
||||||
|
|
||||||
|
protected constructor(direction, clientTransferId, properties) {
|
||||||
|
this.direction = direction;
|
||||||
|
this.clientTransferId = clientTransferId;
|
||||||
|
this.properties = properties;
|
||||||
|
this.timings = {
|
||||||
|
timestampExecuted: 0,
|
||||||
|
timestampTransferBegin: 0,
|
||||||
|
timestampEnd: 0,
|
||||||
|
timestampScheduled: Date.now()
|
||||||
|
};
|
||||||
|
this.setTransferState(FileTransferState.PENDING);
|
||||||
|
|
||||||
|
this.events = new Registry<FileTransferEvents>();
|
||||||
|
this.events.on("notify_transfer_canceled", event => {
|
||||||
|
this.setTransferState(FileTransferState.CANCELED);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isRunning() {
|
||||||
|
return this.transferState_ === FileTransferState.CONNECTING || this.transferState_ === FileTransferState.RUNNING || this.transferState_ === FileTransferState.INITIALIZING;
|
||||||
|
}
|
||||||
|
|
||||||
|
isPending() {
|
||||||
|
return this.transferState_ === FileTransferState.PENDING;
|
||||||
|
}
|
||||||
|
|
||||||
|
isFinished() {
|
||||||
|
return this.transferState() === FileTransferState.FINISHED || this.transferState() === FileTransferState.ERRORED || this.transferState() === FileTransferState.CANCELED;
|
||||||
|
}
|
||||||
|
|
||||||
|
transferState() {
|
||||||
|
return this.transferState_;
|
||||||
|
}
|
||||||
|
|
||||||
|
transferProperties() : InitializedTransferProperties | undefined {
|
||||||
|
return this.transferProperties_;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentError() : TransferErrorType | undefined {
|
||||||
|
return this.transferError_;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentErrorMessage() : string | undefined {
|
||||||
|
return this.transferErrorMessage_;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastProgressInfo() : TransferProgress | undefined {
|
||||||
|
return this.progress_;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFailed(error: TransferErrorType, asMessage: string) {
|
||||||
|
if(this.isFinished())
|
||||||
|
throw tr("invalid transfer state");
|
||||||
|
|
||||||
|
if(typeof asMessage !== "string")
|
||||||
|
debugger;
|
||||||
|
|
||||||
|
this.transferErrorMessage_ = asMessage;
|
||||||
|
this.transferError_ = error;
|
||||||
|
this.setTransferState(FileTransferState.ERRORED);
|
||||||
|
}
|
||||||
|
|
||||||
|
setProperties(properties: InitializedTransferProperties) {
|
||||||
|
if(this.transferState() !== FileTransferState.INITIALIZING)
|
||||||
|
throw tr("invalid transfer state");
|
||||||
|
|
||||||
|
this.transferProperties_ = properties;
|
||||||
|
this.setTransferState(FileTransferState.CONNECTING);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestCancel(reason: CancelReason) {
|
||||||
|
if(this.isFinished())
|
||||||
|
throw tr("invalid transfer state");
|
||||||
|
|
||||||
|
this.cancelReason = reason;
|
||||||
|
this.events.fire("action_request_cancel");
|
||||||
|
}
|
||||||
|
|
||||||
|
setTransferState(newState: FileTransferState) {
|
||||||
|
if(this.transferState_ === newState)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const newIsFinishedState = newState === FileTransferState.CANCELED || newState === FileTransferState.ERRORED || newState === FileTransferState.FINISHED;
|
||||||
|
try {
|
||||||
|
switch (this.transferState_) {
|
||||||
|
case undefined:
|
||||||
|
if(newState !== FileTransferState.PENDING)
|
||||||
|
throw void 0;
|
||||||
|
this.timings.timestampScheduled = Date.now();
|
||||||
|
break;
|
||||||
|
case FileTransferState.PENDING:
|
||||||
|
if(newState !== FileTransferState.INITIALIZING && !newIsFinishedState)
|
||||||
|
throw void 0;
|
||||||
|
break;
|
||||||
|
case FileTransferState.INITIALIZING:
|
||||||
|
if(newState !== FileTransferState.CONNECTING && !newIsFinishedState)
|
||||||
|
throw void 0;
|
||||||
|
break;
|
||||||
|
case FileTransferState.CONNECTING:
|
||||||
|
if(newState !== FileTransferState.RUNNING && !newIsFinishedState)
|
||||||
|
throw void 0;
|
||||||
|
break;
|
||||||
|
case FileTransferState.RUNNING:
|
||||||
|
if(!newIsFinishedState)
|
||||||
|
throw void 0;
|
||||||
|
break;
|
||||||
|
case FileTransferState.FINISHED:
|
||||||
|
case FileTransferState.CANCELED:
|
||||||
|
case FileTransferState.ERRORED:
|
||||||
|
if(this.isFinished())
|
||||||
|
throw void 0;
|
||||||
|
this.timings.timestampEnd = Date.now();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (newState) {
|
||||||
|
case FileTransferState.INITIALIZING:
|
||||||
|
this.timings.timestampExecuted = Date.now();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FileTransferState.RUNNING:
|
||||||
|
this.timings.timestampTransferBegin = Date.now();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FileTransferState.FINISHED:
|
||||||
|
case FileTransferState.CANCELED:
|
||||||
|
case FileTransferState.ERRORED:
|
||||||
|
this.timings.timestampEnd = Date.now();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw "invalid transfer state transform from " + this.transferState_ + " to " + newState;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldState = this.transferState_;
|
||||||
|
this.transferState_ = newState;
|
||||||
|
this.events?.fire("notify_state_updated", { oldState: oldState, newState: newState });
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProgress(progress: TransferProgress) {
|
||||||
|
this.progress_ = progress;
|
||||||
|
this.events.fire_async("notify_progress", { progress: progress });
|
||||||
|
}
|
||||||
|
|
||||||
|
awaitFinished() : Promise<void> {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
if(this.isFinished()) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const listenerStatus = () => {
|
||||||
|
if(this.isFinished()) {
|
||||||
|
this.events.off("notify_state_updated", listenerStatus);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.events.on("notify_state_updated", listenerStatus);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FileDownloadTransfer extends FileTransfer {
|
||||||
|
public readonly targetSupplier: TransferTargetSupplier;
|
||||||
|
public target: TransferTarget;
|
||||||
|
|
||||||
|
constructor(direction, clientTransferId, properties: TransferProperties, targetSupplier) {
|
||||||
|
super(direction, clientTransferId, properties);
|
||||||
|
this.targetSupplier = targetSupplier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FileUploadTransfer extends FileTransfer {
|
||||||
|
public readonly sourceSupplier: TransferSourceSupplier;
|
||||||
|
public source: TransferSource;
|
||||||
|
public fileSize: number;
|
||||||
|
|
||||||
|
constructor(direction, clientTransferId, properties: TransferProperties, sourceSupplier) {
|
||||||
|
super(direction, clientTransferId, properties);
|
||||||
|
this.sourceSupplier = sourceSupplier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class TransferProvider {
|
||||||
|
private static instance_;
|
||||||
|
public static provider() : TransferProvider { return this.instance_; }
|
||||||
|
public static setProvider(provider: TransferProvider) {
|
||||||
|
this.instance_ = provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract executeFileDownload(transfer: FileDownloadTransfer);
|
||||||
|
abstract executeFileUpload(transfer: FileUploadTransfer);
|
||||||
|
|
||||||
|
abstract targetSupported(type: TransferTargetType);
|
||||||
|
abstract sourceSupported(type: TransferSourceType);
|
||||||
|
|
||||||
|
async createResponseTarget() : Promise<ResponseTransferTarget> { throw tr("response target isn't supported"); }
|
||||||
|
async createDownloadTarget(filename?: string) : Promise<DownloadTransferTarget> { throw tr("download target isn't supported"); }
|
||||||
|
|
||||||
|
async createBufferSource(buffer: ArrayBuffer) : Promise<BufferTransferSource> { throw tr("buffer source isn't supported"); }
|
||||||
|
async createTextSource(text: string) : Promise<TextTransferSource> { throw tr("text source isn't supported"); };
|
||||||
|
async createBrowserFileSource(file: File) : Promise<BrowserFileTransferSource> { throw tr("browser file source isn't supported"); }
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ import {guid} from "tc-shared/crypto/uid";
|
||||||
import {StaticSettings} from "tc-shared/settings";
|
import {StaticSettings} from "tc-shared/settings";
|
||||||
import {createErrorModal} from "tc-shared/ui/elements/Modal";
|
import {createErrorModal} from "tc-shared/ui/elements/Modal";
|
||||||
import * as loader from "tc-loader";
|
import * as loader from "tc-loader";
|
||||||
import {formatMessage} from "tc-shared/ui/frames/chat";
|
import {formatMessage, formatMessageString} from "tc-shared/ui/frames/chat";
|
||||||
|
|
||||||
export interface TranslationKey {
|
export interface TranslationKey {
|
||||||
message: string;
|
message: string;
|
||||||
|
@ -69,9 +69,21 @@ export function tr(message: string, key?: string) {
|
||||||
return translated;
|
return translated;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tra(message: string, ...args: any[]) {
|
export function tra(message: string, ...args: (string | number | boolean)[]) : string;
|
||||||
|
export function tra(message: string, ...args: any[]) : JQuery[];
|
||||||
|
export function tra(message: string, ...args: any[]) : any {
|
||||||
message = /* @tr-ignore */ tr(message);
|
message = /* @tr-ignore */ tr(message);
|
||||||
return formatMessage(message, ...args);
|
for(const element of args) {
|
||||||
|
if(typeof element !== "string" && typeof element !== "number" && typeof element !== "boolean")
|
||||||
|
return formatMessage(message, ...args);
|
||||||
|
}
|
||||||
|
if(message.indexOf("{:") !== -1)
|
||||||
|
return formatMessage(message, ...args);
|
||||||
|
return formatMessageString(message, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function traj(message: string, ...args: any[]) : JQuery[] {
|
||||||
|
return tra(message, ...args, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function load_translation_file(url: string, path: string) : Promise<TranslationFile> {
|
async function load_translation_file(url: string, path: string) : Promise<TranslationFile> {
|
||||||
|
|
|
@ -18,7 +18,8 @@ export enum LogCategory {
|
||||||
IPC,
|
IPC,
|
||||||
IDENTITIES,
|
IDENTITIES,
|
||||||
STATISTICS,
|
STATISTICS,
|
||||||
DNS
|
DNS,
|
||||||
|
FILE_TRANSFER
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum LogType {
|
export enum LogType {
|
||||||
|
@ -30,22 +31,23 @@ export enum LogType {
|
||||||
}
|
}
|
||||||
|
|
||||||
let category_mapping = new Map<number, string>([
|
let category_mapping = new Map<number, string>([
|
||||||
[LogCategory.CHANNEL, "Channel "],
|
[LogCategory.CHANNEL, "Channel "],
|
||||||
[LogCategory.CHANNEL_PROPERTIES, "Channel "],
|
[LogCategory.CHANNEL_PROPERTIES, "Channel "],
|
||||||
[LogCategory.CLIENT, "Client "],
|
[LogCategory.CLIENT, "Client "],
|
||||||
[LogCategory.SERVER, "Server "],
|
[LogCategory.SERVER, "Server "],
|
||||||
[LogCategory.BOOKMARKS, "Bookmark "],
|
[LogCategory.BOOKMARKS, "Bookmark "],
|
||||||
[LogCategory.PERMISSIONS, "Permission "],
|
[LogCategory.PERMISSIONS, "Permission "],
|
||||||
[LogCategory.GENERAL, "General "],
|
[LogCategory.GENERAL, "General "],
|
||||||
[LogCategory.NETWORKING, "Network "],
|
[LogCategory.NETWORKING, "Network "],
|
||||||
[LogCategory.VOICE, "Voice "],
|
[LogCategory.VOICE, "Voice "],
|
||||||
[LogCategory.AUDIO, "Audio "],
|
[LogCategory.AUDIO, "Audio "],
|
||||||
[LogCategory.CHANNEL, "Chat "],
|
[LogCategory.CHANNEL, "Chat "],
|
||||||
[LogCategory.I18N, "I18N "],
|
[LogCategory.I18N, "I18N "],
|
||||||
[LogCategory.IDENTITIES, "Identities "],
|
[LogCategory.IDENTITIES, "Identities "],
|
||||||
[LogCategory.IPC, "IPC "],
|
[LogCategory.IPC, "IPC "],
|
||||||
[LogCategory.STATISTICS, "Statistics "],
|
[LogCategory.STATISTICS, "Statistics "],
|
||||||
[LogCategory.DNS, "DNS "]
|
[LogCategory.DNS, "DNS "],
|
||||||
|
[LogCategory.FILE_TRANSFER, "FILE_TRANSFER"]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export let enabled_mapping = new Map<number, boolean>([
|
export let enabled_mapping = new Map<number, boolean>([
|
||||||
|
@ -64,7 +66,8 @@ export let enabled_mapping = new Map<number, boolean>([
|
||||||
[LogCategory.IDENTITIES, true],
|
[LogCategory.IDENTITIES, true],
|
||||||
[LogCategory.IPC, true],
|
[LogCategory.IPC, true],
|
||||||
[LogCategory.STATISTICS, true],
|
[LogCategory.STATISTICS, true],
|
||||||
[LogCategory.DNS, true]
|
[LogCategory.DNS, true],
|
||||||
|
[LogCategory.FILE_TRANSFER, true]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
//Values will be overridden by initialize()
|
//Values will be overridden by initialize()
|
||||||
|
|
|
@ -10,7 +10,6 @@ import * as i18n from "./i18n/localize";
|
||||||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||||
import {createInfoModal} from "tc-shared/ui/elements/Modal";
|
import {createInfoModal} from "tc-shared/ui/elements/Modal";
|
||||||
import {tra} from "./i18n/localize";
|
import {tra} from "./i18n/localize";
|
||||||
import {RequestFileUpload} from "tc-shared/file/FileManager";
|
|
||||||
import * as stats from "./stats";
|
import * as stats from "./stats";
|
||||||
import * as fidentity from "./profiles/identities/TeaForumIdentity";
|
import * as fidentity from "./profiles/identities/TeaForumIdentity";
|
||||||
import {default_recorder, RecorderProfile, set_default_recorder} from "tc-shared/voice/RecorderProfile";
|
import {default_recorder, RecorderProfile, set_default_recorder} from "tc-shared/voice/RecorderProfile";
|
||||||
|
@ -30,6 +29,11 @@ import * as ReactDOM from "react-dom";
|
||||||
import * as cbar from "./ui/frames/control-bar";
|
import * as cbar from "./ui/frames/control-bar";
|
||||||
import * as global_ev_handler from "./events/ClientGlobalControlHandler";
|
import * as global_ev_handler from "./events/ClientGlobalControlHandler";
|
||||||
import {global_client_actions} from "tc-shared/events/GlobalEvents";
|
import {global_client_actions} from "tc-shared/events/GlobalEvents";
|
||||||
|
import {
|
||||||
|
FileTransferState,
|
||||||
|
TransferProvider,
|
||||||
|
} from "tc-shared/file/Transfer";
|
||||||
|
import {spawnFileTransferModal} from "tc-shared/ui/modal/transfer/ModalFileTransfer";
|
||||||
|
|
||||||
/* required import for init */
|
/* required import for init */
|
||||||
require("./proto").initialize();
|
require("./proto").initialize();
|
||||||
|
@ -351,6 +355,7 @@ function main() {
|
||||||
server_connections.set_active_connection(server_connections.all_connections()[0]);
|
server_connections.set_active_connection(server_connections.all_connections()[0]);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
(window as any).test_upload = (message?: string) => {
|
(window as any).test_upload = (message?: string) => {
|
||||||
message = message || "Hello World";
|
message = message || "Hello World";
|
||||||
|
|
||||||
|
@ -375,6 +380,38 @@ function main() {
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
*/
|
||||||
|
(window as any).test_download = async () => {
|
||||||
|
const connection = server_connections.active_connection();
|
||||||
|
const download = connection.fileManager.initializeFileDownload({
|
||||||
|
targetSupplier: async () => await TransferProvider.provider().createDownloadTarget(),
|
||||||
|
name: "HomeStudent2019Retail.img",
|
||||||
|
path: "/",
|
||||||
|
channel: 4
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Download stated");
|
||||||
|
await download.awaitFinished();
|
||||||
|
console.log("Download finished (%s)", FileTransferState[download.transferState()]);
|
||||||
|
//console.log(await (download.target as ResponseTransferTarget).getResponse().blob());
|
||||||
|
console.log("Have buffer");
|
||||||
|
};
|
||||||
|
|
||||||
|
(window as any).test_upload = async () => {
|
||||||
|
const connection = server_connections.active_connection();
|
||||||
|
const download = connection.fileManager.initializeFileUpload({
|
||||||
|
source: async () => await TransferProvider.provider().createTextSource("Hello my lovely world...."),
|
||||||
|
name: "test-upload.txt",
|
||||||
|
path: "/",
|
||||||
|
channel: 4
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Download stated");
|
||||||
|
await download.awaitFinished();
|
||||||
|
console.log("Download finished (%s)", FileTransferState[download.transferState()]);
|
||||||
|
//console.log(await (download.target as ResponseTransferTarget).getResponse().blob());
|
||||||
|
console.log("Have buffer");
|
||||||
|
};
|
||||||
|
|
||||||
/* schedule it a bit later then the main because the main function is still within the loader */
|
/* schedule it a bit later then the main because the main function is still within the loader */
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -436,11 +473,13 @@ function main() {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
/* for testing */
|
|
||||||
if(settings.static_global(Settings.KEY_USER_IS_NEW)) {
|
if(settings.static_global(Settings.KEY_USER_IS_NEW)) {
|
||||||
const modal = openModalNewcomer();
|
const modal = openModalNewcomer();
|
||||||
modal.close_listener.push(() => settings.changeGlobal(Settings.KEY_USER_IS_NEW, false));
|
modal.close_listener.push(() => settings.changeGlobal(Settings.KEY_USER_IS_NEW, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
(window as any).spawnFileTransferModal = spawnFileTransferModal;
|
||||||
|
spawnFileTransferModal(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const task_teaweb_starter: loader.Task = {
|
const task_teaweb_starter: loader.Task = {
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import {settings, Settings} from "tc-shared/settings";
|
import {settings, Settings} from "tc-shared/settings";
|
||||||
import * as loader from "tc-loader";
|
import * as loader from "tc-loader";
|
||||||
import * as fidentity from "./TeaForumIdentity";
|
import * as fidentity from "./TeaForumIdentity";
|
||||||
|
import * as log from "../../log";
|
||||||
|
import {LogCategory} from "../../log";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
@ -366,5 +368,25 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
if(_data && _data.is_expired()) {
|
if(_data && _data.is_expired()) {
|
||||||
console.error(tr("TeaForo data is expired. TeaForo connection isn't available!"));
|
console.error(tr("TeaForo data is expired. TeaForo connection isn't available!"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
/* if we don't have any _data object set we could not renew anything */
|
||||||
|
if(_data) {
|
||||||
|
log.info(LogCategory.IDENTITIES, tr("Renewing TeaForo data."));
|
||||||
|
renew_data().then(status => {
|
||||||
|
if(status === "success") {
|
||||||
|
log.info(LogCategory.IDENTITIES,tr("TeaForo data has been successfully renewed."));
|
||||||
|
} else {
|
||||||
|
log.warn(LogCategory.IDENTITIES,tr("Failed to renew TeaForo data. New login required."));
|
||||||
|
localStorage.removeItem("teaspeak-forum-data");
|
||||||
|
localStorage.removeItem("teaspeak-forum-sign");
|
||||||
|
localStorage.removeItem("teaspeak-forum-auth");
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.warn(tr("Failed to renew TeaForo data. An error occurred: %o"), error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 24 * 60 * 60 * 1000);
|
||||||
}
|
}
|
||||||
})
|
});
|
|
@ -6,6 +6,7 @@ declare global {
|
||||||
last?(): T;
|
last?(): T;
|
||||||
|
|
||||||
pop_front(): T | undefined;
|
pop_front(): T | undefined;
|
||||||
|
toggle(entry: T) : boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface JSON {
|
interface JSON {
|
||||||
|
@ -172,6 +173,18 @@ if (!Array.prototype.pop_front) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!Array.prototype.toggle) {
|
||||||
|
Array.prototype.toggle = function<T>(element: T): boolean {
|
||||||
|
const index = this.findIndex(e => e === element);
|
||||||
|
if(index === -1) {
|
||||||
|
this.push(element);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
this.splice(index, 1);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!Array.prototype.last){
|
if (!Array.prototype.last){
|
||||||
Array.prototype.last = function(){
|
Array.prototype.last = function(){
|
||||||
|
|
|
@ -354,6 +354,12 @@ export class Settings extends StaticSettings {
|
||||||
default_value: "tea-web"
|
default_value: "tea-web"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static readonly KEY_TRANSFERS_SHOW_FINISHED: SettingsKey<boolean> = {
|
||||||
|
key: 'transfers_show_finished',
|
||||||
|
default_value: true,
|
||||||
|
description: "Show finished file transfers in the file transfer list"
|
||||||
|
};
|
||||||
|
|
||||||
static readonly FN_INVITE_LINK_SETTING: (name: string) => SettingsKey<string> = name => {
|
static readonly FN_INVITE_LINK_SETTING: (name: string) => SettingsKey<string> = name => {
|
||||||
return {
|
return {
|
||||||
key: 'invite_link_setting_' + name
|
key: 'invite_link_setting_' + name
|
||||||
|
|
|
@ -20,6 +20,7 @@ import {Registry} from "tc-shared/events";
|
||||||
import {ChannelTreeEntry, ChannelTreeEntryEvents} from "tc-shared/ui/TreeEntry";
|
import {ChannelTreeEntry, ChannelTreeEntryEvents} from "tc-shared/ui/TreeEntry";
|
||||||
import { ChannelEntryView as ChannelEntryView } from "./tree/Channel";
|
import { ChannelEntryView as ChannelEntryView } from "./tree/Channel";
|
||||||
import {MenuEntryType} from "tc-shared/ui/elements/ContextMenu";
|
import {MenuEntryType} from "tc-shared/ui/elements/ContextMenu";
|
||||||
|
import {spawnFileTransferModal} from "tc-shared/ui/modal/transfer/ModalFileTransfer";
|
||||||
|
|
||||||
export enum ChannelType {
|
export enum ChannelType {
|
||||||
PERMANENT,
|
PERMANENT,
|
||||||
|
@ -170,7 +171,7 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
||||||
//HTML DOM elements
|
//HTML DOM elements
|
||||||
private _destroyed = false;
|
private _destroyed = false;
|
||||||
|
|
||||||
private _cachedPassword: string;
|
private cachedPasswordHash: string;
|
||||||
private _cached_channel_description: string = undefined;
|
private _cached_channel_description: string = undefined;
|
||||||
private _cached_channel_description_promise: Promise<string> = undefined;
|
private _cached_channel_description_promise: Promise<string> = undefined;
|
||||||
private _cached_channel_description_promise_resolve: any = undefined;
|
private _cached_channel_description_promise_resolve: any = undefined;
|
||||||
|
@ -390,6 +391,12 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
||||||
icon_class: "client-channel_switch",
|
icon_class: "client-channel_switch",
|
||||||
name: bold(tr("Switch to channel")),
|
name: bold(tr("Switch to channel")),
|
||||||
callback: () => this.joinChannel()
|
callback: () => this.joinChannel()
|
||||||
|
},{
|
||||||
|
type: contextmenu.MenuEntryType.ENTRY,
|
||||||
|
icon_class: "client-filetransfer",
|
||||||
|
name: bold(tr("Open channel file browser")),
|
||||||
|
callback: () => spawnFileTransferModal(this.getChannelId()),
|
||||||
|
visible: false /* FIXME: Enable this */
|
||||||
}, {
|
}, {
|
||||||
type: contextmenu.MenuEntryType.ENTRY,
|
type: contextmenu.MenuEntryType.ENTRY,
|
||||||
icon_class: "client-channel_switch",
|
icon_class: "client-channel_switch",
|
||||||
|
@ -623,32 +630,47 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
||||||
}
|
}
|
||||||
|
|
||||||
joinChannel() {
|
joinChannel() {
|
||||||
if(this.properties.channel_flag_password == true &&
|
if(this.properties.channel_flag_password === true && !this.cachedPasswordHash) {
|
||||||
!this._cachedPassword &&
|
this.requestChannelPassword(PermissionType.B_CHANNEL_JOIN_IGNORE_PASSWORD).then(password => {
|
||||||
!this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_JOIN_IGNORE_PASSWORD).granted(1)) {
|
this.joinChannel();
|
||||||
createInputModal(tr("Channel password"), tr("Channel password:"), () => true, text => {
|
|
||||||
if(typeof(text) !== "string") return;
|
|
||||||
|
|
||||||
hashPassword(text).then(result => {
|
|
||||||
this._cachedPassword = result;
|
|
||||||
this.events.fire("notify_cached_password_updated", { reason: "password-entered", new_hash: result });
|
|
||||||
this.joinChannel();
|
|
||||||
});
|
|
||||||
}).open();
|
|
||||||
} else if(this.channelTree.client.getClient().currentChannel() != this)
|
|
||||||
this.channelTree.client.getServerConnection().command_helper.joinChannel(this, this._cachedPassword).then(() => {
|
|
||||||
this.channelTree.client.sound.play(Sound.CHANNEL_JOINED);
|
|
||||||
}).catch(error => {
|
|
||||||
if(error instanceof CommandResult) {
|
|
||||||
if(error.id == 781) { //Invalid password
|
|
||||||
this._cachedPassword = undefined;
|
|
||||||
this.events.fire("notify_cached_password_updated", { reason: "password-miss-match" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.channelTree.client.getServerConnection().command_helper.joinChannel(this, this.cachedPasswordHash).then(() => {
|
||||||
|
this.channelTree.client.sound.play(Sound.CHANNEL_JOINED);
|
||||||
|
}).catch(error => {
|
||||||
|
if(error instanceof CommandResult) {
|
||||||
|
if(error.id == 781) { //Invalid password
|
||||||
|
this.invalidateCachedPassword();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
cached_password() { return this._cachedPassword; }
|
async requestChannelPassword(ignorePermission: PermissionType) : Promise<{ hash: string } | undefined> {
|
||||||
|
if(this.cachedPasswordHash)
|
||||||
|
return { hash: this.cachedPasswordHash };
|
||||||
|
|
||||||
|
if(this.channelTree.client.permissions.neededPermission(ignorePermission).granted(1))
|
||||||
|
return { hash: "having ignore permission" };
|
||||||
|
|
||||||
|
const password = await new Promise(resolve => createInputModal(tr("Channel password"), tr("Channel password:"), () => true, resolve).open())
|
||||||
|
if(typeof(password) !== "string" || !password)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const hash = await hashPassword(password);
|
||||||
|
this.cachedPasswordHash = hash;
|
||||||
|
this.events.fire("notify_cached_password_updated", { reason: "password-entered", new_hash: hash });
|
||||||
|
return { hash: this.cachedPasswordHash };
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidateCachedPassword() {
|
||||||
|
this.cachedPasswordHash = undefined;
|
||||||
|
this.events.fire("notify_cached_password_updated", { reason: "password-miss-match" });
|
||||||
|
}
|
||||||
|
|
||||||
|
cached_password() { return this.cachedPasswordHash; }
|
||||||
|
|
||||||
async subscribe() : Promise<void> {
|
async subscribe() : Promise<void> {
|
||||||
if(this.subscribe_mode == ChannelSubscribeMode.SUBSCRIBED)
|
if(this.subscribe_mode == ChannelSubscribeMode.SUBSCRIBED)
|
||||||
|
|
|
@ -19,7 +19,6 @@ import {spawnPermissionEdit} from "tc-shared/ui/modal/permission/ModalPermission
|
||||||
import {createServerGroupAssignmentModal} from "tc-shared/ui/modal/ModalGroupAssignment";
|
import {createServerGroupAssignmentModal} from "tc-shared/ui/modal/ModalGroupAssignment";
|
||||||
import {openClientInfo} from "tc-shared/ui/modal/ModalClientInfo";
|
import {openClientInfo} from "tc-shared/ui/modal/ModalClientInfo";
|
||||||
import {spawnBanClient} from "tc-shared/ui/modal/ModalBanClient";
|
import {spawnBanClient} from "tc-shared/ui/modal/ModalBanClient";
|
||||||
import {spawnChangeVolume} from "tc-shared/ui/modal/ModalChangeVolume";
|
|
||||||
import {spawnChangeLatency} from "tc-shared/ui/modal/ModalChangeLatency";
|
import {spawnChangeLatency} from "tc-shared/ui/modal/ModalChangeLatency";
|
||||||
import {spawnPlaylistEdit} from "tc-shared/ui/modal/ModalPlaylistEdit";
|
import {spawnPlaylistEdit} from "tc-shared/ui/modal/ModalPlaylistEdit";
|
||||||
import {formatMessage} from "tc-shared/ui/frames/chat";
|
import {formatMessage} from "tc-shared/ui/frames/chat";
|
||||||
|
|
|
@ -95,6 +95,46 @@ export function formatMessage(pattern: string, ...objects: any[]) : JQuery[] {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatMessageString(pattern: string, ...args: string[]) : string {
|
||||||
|
let begin = 0, found = 0;
|
||||||
|
|
||||||
|
let result: string[] = [];
|
||||||
|
do {
|
||||||
|
found = pattern.indexOf('{', found);
|
||||||
|
if(found == -1 || pattern.length <= found + 1) {
|
||||||
|
result.push(pattern.substr(begin));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(found > 0 && pattern[found - 1] == '\\') {
|
||||||
|
//TODO remove the escape!
|
||||||
|
found++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(pattern.substr(begin, found - begin)); //Append the text
|
||||||
|
|
||||||
|
let offset = 0;
|
||||||
|
let number;
|
||||||
|
while ("0123456789".includes(pattern[found + 1 + offset])) offset++;
|
||||||
|
number = parseInt(offset > 0 ? pattern.substr(found + 1, offset) : "0");
|
||||||
|
if(pattern[found + offset + 1] != '}') {
|
||||||
|
found++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(args.length < number)
|
||||||
|
log.warn(LogCategory.GENERAL, tr("Message to format contains invalid index (%o)"), number);
|
||||||
|
|
||||||
|
result.push(args[number]);
|
||||||
|
|
||||||
|
found = found + 1 + offset;
|
||||||
|
begin = found + 1;
|
||||||
|
} while(found++);
|
||||||
|
|
||||||
|
return result.join("");
|
||||||
|
}
|
||||||
|
|
||||||
//TODO: Remove this (only legacy)
|
//TODO: Remove this (only legacy)
|
||||||
export function bbcode_chat(message: string) : JQuery[] {
|
export function bbcode_chat(message: string) : JQuery[] {
|
||||||
return bbcode.format(message, {
|
return bbcode.format(message, {
|
||||||
|
|
|
@ -39,7 +39,7 @@ interface VolumeChangeModalState {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactEventHandler(e => e.props.events)
|
@ReactEventHandler(e => e.props.events)
|
||||||
class VolumeChangeModal extends React.Component<{ clientName: string, remote: boolean, events: Registry<VolumeChangeEvents> }, VolumeChangeModalState> {
|
class VolumeChangeModal extends React.Component<{ clientName: string, maxVolume?: number, remote: boolean, events: Registry<VolumeChangeEvents> }, VolumeChangeModalState> {
|
||||||
private readonly refSlider = React.createRef<Slider>();
|
private readonly refSlider = React.createRef<Slider>();
|
||||||
|
|
||||||
private originalValue: number;
|
private originalValue: number;
|
||||||
|
@ -141,7 +141,7 @@ class VolumeChangeModal extends React.Component<{ clientName: string, remote: bo
|
||||||
|
|
||||||
private onApplyClick() {
|
private onApplyClick() {
|
||||||
this.props.events.fire("apply-volume", {
|
this.props.events.fire("apply-volume", {
|
||||||
newValue: this.state.volumeModifier,
|
newValue: this.state.volumeModifier,
|
||||||
origin: "user-input"
|
origin: "user-input"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -275,7 +275,7 @@ export function spawnMusicBotVolumeChange(client: MusicClientEntry, maxValue: nu
|
||||||
|
|
||||||
const modal = spawnReactModal(class extends Modal {
|
const modal = spawnReactModal(class extends Modal {
|
||||||
renderBody() {
|
renderBody() {
|
||||||
return <VolumeChangeModal remote={true} clientName={client.clientNickName()} events={events} />;
|
return <VolumeChangeModal remote={true} clientName={client.clientNickName()} maxVolume={maxValue} events={events} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
title(): string {
|
title(): string {
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||||
import PermissionType from "tc-shared/permission/PermissionType";
|
import PermissionType from "tc-shared/permission/PermissionType";
|
||||||
import {createErrorModal, createModal} from "tc-shared/ui/elements/Modal";
|
import {createErrorModal, createModal} from "tc-shared/ui/elements/Modal";
|
||||||
import {FileEntry, UploadKey} from "tc-shared/file/FileManager";
|
|
||||||
import {LogCategory} from "tc-shared/log";
|
|
||||||
import * as log from "tc-shared/log";
|
import * as log from "tc-shared/log";
|
||||||
|
import {LogCategory} from "tc-shared/log";
|
||||||
import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration";
|
import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||||
import {tra} from "tc-shared/i18n/localize";
|
import {tra, traj} from "tc-shared/i18n/localize";
|
||||||
import {arrayBufferBase64} from "tc-shared/utils/buffers";
|
import {arrayBufferBase64} from "tc-shared/utils/buffers";
|
||||||
import {Settings, settings} from "tc-shared/settings";
|
import {Settings, settings} from "tc-shared/settings";
|
||||||
import * as crc32 from "tc-shared/crypto/crc32";
|
import * as crc32 from "tc-shared/crypto/crc32";
|
||||||
import {transfer_provider} from "tc-shared/file/FileManager";
|
import {FileInfo} from "tc-shared/file/FileManager";
|
||||||
|
import {FileTransferState, TransferProvider} from "tc-shared/file/Transfer";
|
||||||
|
|
||||||
export function spawnIconSelect(client: ConnectionHandler, callback_icon?: (id: number) => any, selected_icon?: number) {
|
export function spawnIconSelect(client: ConnectionHandler, callback_icon?: (id: number) => any, selected_icon?: number) {
|
||||||
selected_icon = selected_icon || 0;
|
selected_icon = selected_icon || 0;
|
||||||
|
@ -89,7 +89,7 @@ export function spawnIconSelect(client: ConnectionHandler, callback_icon?: (id:
|
||||||
container_icons_remote.detach().empty();
|
container_icons_remote.detach().empty();
|
||||||
|
|
||||||
const chunk_size = 50;
|
const chunk_size = 50;
|
||||||
const icon_chunks: FileEntry[][] = [];
|
const icon_chunks: FileInfo[][] = [];
|
||||||
let index = 0;
|
let index = 0;
|
||||||
while(icons.length > index) {
|
while(icons.length > index) {
|
||||||
icon_chunks.push(icons.slice(index, index + chunk_size));
|
icon_chunks.push(icons.slice(index, index + chunk_size));
|
||||||
|
@ -388,54 +388,53 @@ function handle_icon_upload(file: File, client: ConnectionHandler) : UploadingIc
|
||||||
bar.set_value(25);
|
bar.set_value(25);
|
||||||
bar.set_message(tr("initializing"));
|
bar.set_message(tr("initializing"));
|
||||||
|
|
||||||
let upload_key: UploadKey;
|
const transfer = client.fileManager.initializeFileUpload({
|
||||||
try {
|
channel: 0,
|
||||||
upload_key = await client.fileManager.upload_file({
|
channelPassword: undefined,
|
||||||
channel: undefined,
|
|
||||||
channel_password: undefined,
|
path: "",
|
||||||
name: '/icon_' + icon.icon_id,
|
name: "/icon_" + icon.icon_id,
|
||||||
overwrite: false,
|
|
||||||
path: '',
|
source: async () => await TransferProvider.provider().createBrowserFileSource(icon.file)
|
||||||
size: icon.file.size
|
});
|
||||||
})
|
|
||||||
} catch(error) {
|
transfer.events.on("notify_state_updated", event => {
|
||||||
if(error instanceof CommandResult && error.id == ErrorID.FILE_ALREADY_EXISTS) {
|
switch (event.newState) {
|
||||||
if(!settings.static_global(Settings.KEY_DISABLE_COSMETIC_SLOWDOWN, false))
|
case FileTransferState.PENDING:
|
||||||
await new Promise(resolve => setTimeout(resolve, 500 + Math.floor(Math.random() * 500)));
|
bar.set_value(10);
|
||||||
bar.set_message(tr("icon already exists"));
|
bar.set_message(tr("pending"));
|
||||||
bar.set_value(100);
|
break;
|
||||||
icon.upload_state = "uploaded";
|
case FileTransferState.INITIALIZING:
|
||||||
return;
|
case FileTransferState.CONNECTING:
|
||||||
|
bar.set_value(30);
|
||||||
|
bar.set_message(tr("connecting"));
|
||||||
|
break;
|
||||||
|
case FileTransferState.RUNNING:
|
||||||
|
bar.set_value(50);
|
||||||
|
bar.set_message(tr("uploading"));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FileTransferState.FINISHED:
|
||||||
|
bar.set_value(100);
|
||||||
|
bar.set_message(tr("upload completed"));
|
||||||
|
icon.upload_state = "uploaded";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FileTransferState.ERRORED:
|
||||||
|
log.warn(LogCategory.FILE_TRANSFER, tr("Failed to upload icon %s: %o"), icon.file.name, transfer.currentError());
|
||||||
|
bar.set_value(100);
|
||||||
|
bar.set_error(tr("upload failed: ") + transfer.currentErrorMessage());
|
||||||
|
icon.upload_state = "error";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FileTransferState.CANCELED:
|
||||||
|
bar.set_value(100);
|
||||||
|
bar.set_error(tr("upload canceled"));
|
||||||
|
icon.upload_state = "error";
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
console.error(tr("Failed to initialize upload: %o"), error);
|
});
|
||||||
bar.set_error(tr("failed to initialize upload"));
|
await transfer.awaitFinished();
|
||||||
icon.upload_state = "error";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
bar.set_value(50);
|
|
||||||
bar.set_message(tr("uploading"));
|
|
||||||
|
|
||||||
const connection = transfer_provider().spawn_upload_transfer(upload_key);
|
|
||||||
try {
|
|
||||||
await connection.put_data(icon.file)
|
|
||||||
} catch(error) {
|
|
||||||
console.error(tr("Icon upload failed for icon %s: %o"), icon.file.name, error);
|
|
||||||
if(typeof(error) === "string")
|
|
||||||
bar.set_error(tr("upload failed: ") + error);
|
|
||||||
else if(typeof(error.message) === "string")
|
|
||||||
bar.set_error(tr("upload failed: ") + error.message);
|
|
||||||
else
|
|
||||||
bar.set_error(tr("upload failed"));
|
|
||||||
icon.upload_state = "error";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const time_end = Date.now();
|
|
||||||
if(!settings.static_global(Settings.KEY_DISABLE_COSMETIC_SLOWDOWN, false))
|
|
||||||
await new Promise(resolve => setTimeout(resolve, Math.max(0, 1000 - (time_end - time_begin))));
|
|
||||||
bar.set_value(100);
|
|
||||||
bar.set_message(tr("upload completed"));
|
|
||||||
icon.upload_state = "uploaded";
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -467,7 +466,7 @@ export function spawnIconUpload(client: ConnectionHandler) {
|
||||||
const update_upload_button = () => {
|
const update_upload_button = () => {
|
||||||
const icon_count = icons.filter(e => e.state === "valid").length;
|
const icon_count = icons.filter(e => e.state === "valid").length;
|
||||||
button_upload.empty();
|
button_upload.empty();
|
||||||
tra("Upload icons ({})", icon_count).forEach(e => e.appendTo(button_upload));
|
traj("Upload icons ({})", icon_count).forEach(e => e.appendTo(button_upload));
|
||||||
button_upload.prop("disabled", icon_count == 0);
|
button_upload.prop("disabled", icon_count == 0);
|
||||||
};
|
};
|
||||||
update_upload_button();
|
update_upload_button();
|
||||||
|
|
|
@ -0,0 +1,339 @@
|
||||||
|
@import "../../../../css/static/mixin";
|
||||||
|
@import "../../../../css/static/properties";
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 1em;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
padding-bottom: 4em; /* for the transfer info */
|
||||||
|
|
||||||
|
.navigation {
|
||||||
|
.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: #ffffff0e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: #E6E6E6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.fileTable {
|
||||||
|
min-height: 5em;
|
||||||
|
max-height: 40em;
|
||||||
|
height: 400px;
|
||||||
|
|
||||||
|
margin-top: 1em;
|
||||||
|
|
||||||
|
border: 1px #161616 solid;
|
||||||
|
border-radius: 0.2em;
|
||||||
|
background-color: #28292b;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
z-index: 1;
|
||||||
|
padding-top: .2em;
|
||||||
|
padding-bottom: .2em;
|
||||||
|
|
||||||
|
background-color: #28292b;
|
||||||
|
|
||||||
|
.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: #999999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: #9e9494;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlayEmptyFolder {
|
||||||
|
align-self: center;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.directoryEntry {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #2c2d2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background-color: #1a1a1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* drag hovered overrides selected */
|
||||||
|
&.hovered {
|
||||||
|
background-color: #2c2d2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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, #28292b, #28292b);
|
||||||
|
@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, #28292b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.red {
|
||||||
|
@include define-indicator(#a10000, #e60000);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.blue {
|
||||||
|
@include define-indicator(#005fa1, #007acc);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.green {
|
||||||
|
@include define-indicator(#389738, #4ecc4e);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
@include define-indicator(#28292b00, #28292b00);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 #999999;
|
||||||
|
|
||||||
|
border-width: 0 0.125em 0.125em 0;
|
||||||
|
padding: 0.15em;
|
||||||
|
|
||||||
|
height: 0.15em;
|
||||||
|
width: .15em;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,226 @@
|
||||||
|
import {Modal, 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 {server_connections} from "tc-shared/ui/frames/connection_handlers";
|
||||||
|
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/ui/channel";
|
||||||
|
import {initializeTransferInfoController} from "tc-shared/ui/modal/transfer/TransferInfoController";
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FileTransferModal extends Modal {
|
||||||
|
readonly remoteBrowseEvents = new Registry<FileBrowserEvents>();
|
||||||
|
readonly transferInfoEvents = new Registry<TransferInfoEvents>();
|
||||||
|
|
||||||
|
private readonly defaultChannelId;
|
||||||
|
|
||||||
|
constructor(defaultChannelId: number) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.defaultChannelId = defaultChannelId;
|
||||||
|
|
||||||
|
this.remoteBrowseEvents.enable_debug("remote-file-browser");
|
||||||
|
this.transferInfoEvents.enable_debug("transfer-info");
|
||||||
|
|
||||||
|
initializeRemoteFileBrowserController(server_connections.active_connection(), this.remoteBrowseEvents);
|
||||||
|
initializeTransferInfoController(server_connections.active_connection(), this.transferInfoEvents);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onInitialize() {
|
||||||
|
const path = this.defaultChannelId ? "/" + channelPathPrefix + this.defaultChannelId + "/" : "/";
|
||||||
|
this.remoteBrowseEvents.fire("action_navigate_to", { path: path });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onDestroy() {
|
||||||
|
this.remoteBrowseEvents.fire("notify_modal_closed");
|
||||||
|
this.transferInfoEvents.fire("notify_modal_closed");
|
||||||
|
}
|
||||||
|
|
||||||
|
title(): string {
|
||||||
|
return "File Browser";
|
||||||
|
}
|
||||||
|
|
||||||
|
renderBody() {
|
||||||
|
const path = this.defaultChannelId ? "/" + channelPathPrefix + this.defaultChannelId + "/" : "/";
|
||||||
|
return <div className={cssStyle.container} style={{width: "600px"}}>
|
||||||
|
<NavigationBar events={this.remoteBrowseEvents} currentPath={path} />
|
||||||
|
<FileBrowser events={this.remoteBrowseEvents} currentPath={path} />
|
||||||
|
<TransferInfo events={this.transferInfoEvents} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function spawnFileTransferModal(channel: number) {
|
||||||
|
const modal = spawnReactModal(FileTransferModal, channel);
|
||||||
|
modal.show();
|
||||||
|
}
|
|
@ -0,0 +1,780 @@
|
||||||
|
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||||
|
import {Registry} from "tc-shared/events";
|
||||||
|
import {FileType} from "tc-shared/file/FileManager";
|
||||||
|
import {CommandResult, ErrorCode, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||||
|
import PermissionType from "tc-shared/permission/PermissionType";
|
||||||
|
import * as log from "tc-shared/log";
|
||||||
|
import {LogCategory} from "tc-shared/log";
|
||||||
|
import {Entry, MenuEntry, MenuEntryType, spawn_context_menu} from "tc-shared/ui/elements/ContextMenu";
|
||||||
|
import * as ppt from "tc-backend/ppt";
|
||||||
|
import {SpecialKey} from "tc-shared/PPTListener";
|
||||||
|
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
|
||||||
|
import {tra, traj} from "tc-shared/i18n/localize";
|
||||||
|
import {FileTransfer, FileTransferState, FileUploadTransfer, TransferProvider} from "tc-shared/file/Transfer";
|
||||||
|
import {createErrorModal} from "tc-shared/ui/elements/Modal";
|
||||||
|
import {
|
||||||
|
avatarsPathPrefix,
|
||||||
|
channelPathPrefix,
|
||||||
|
FileBrowserEvents,
|
||||||
|
iconPathPrefix, ListedFileInfo, PathInfo
|
||||||
|
} from "tc-shared/ui/modal/transfer/ModalFileTransfer";
|
||||||
|
|
||||||
|
function parsePath(path: string, connection: ConnectionHandler) : PathInfo {
|
||||||
|
if(path === "/" || !path) {
|
||||||
|
return {
|
||||||
|
channel: undefined,
|
||||||
|
channelId: 0,
|
||||||
|
path: "/",
|
||||||
|
type: "root"
|
||||||
|
};
|
||||||
|
} else if(path.startsWith("/" + channelPathPrefix)) {
|
||||||
|
const pathParts = path.split("/");
|
||||||
|
|
||||||
|
const channelId = parseInt(pathParts[1].substr(channelPathPrefix.length));
|
||||||
|
if(isNaN(channelId)) {
|
||||||
|
throw tr("Invalid channel id (ID is NaN)");
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = connection.channelTree.findChannel(channelId);
|
||||||
|
if(!channel) {
|
||||||
|
throw tr("Channel not visible anymore");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "channel",
|
||||||
|
path: "/" + pathParts.slice(2).join("/"),
|
||||||
|
channelId: channelId,
|
||||||
|
channel: channel
|
||||||
|
};
|
||||||
|
} else if(path == "/" + iconPathPrefix + "/") {
|
||||||
|
return {
|
||||||
|
type: "icon",
|
||||||
|
path: "/icons/",
|
||||||
|
channelId: 0,
|
||||||
|
channel: undefined
|
||||||
|
};
|
||||||
|
} else if(path == "/" + avatarsPathPrefix + "/") {
|
||||||
|
return {
|
||||||
|
type: "avatar",
|
||||||
|
path: "/",
|
||||||
|
channelId: 0,
|
||||||
|
channel: undefined
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw tr("Unknown path");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initializeRemoteFileBrowserController(connection: ConnectionHandler, events: Registry<FileBrowserEvents>) {
|
||||||
|
events.on("action_navigate_to", event => {
|
||||||
|
try {
|
||||||
|
const info = parsePath(event.path, connection);
|
||||||
|
|
||||||
|
events.fire_async("action_navigate_to_result", {
|
||||||
|
path: event.path || "/",
|
||||||
|
status: "success",
|
||||||
|
pathInfo: info
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
events.fire_async("action_navigate_to_result", {
|
||||||
|
path: event.path,
|
||||||
|
status: "error",
|
||||||
|
error: error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
events.on("query_files", event => {
|
||||||
|
let path: PathInfo;
|
||||||
|
try {
|
||||||
|
path = parsePath(event.path, connection);
|
||||||
|
} catch (error) {
|
||||||
|
events.fire_async("query_files_result", {
|
||||||
|
path: event.path,
|
||||||
|
status: "error",
|
||||||
|
error: error
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let request: Promise<ListedFileInfo[]>;
|
||||||
|
if(path.type === "root") {
|
||||||
|
request = (async () => {
|
||||||
|
const result: ListedFileInfo[] = [];
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
type: FileType.DIRECTORY,
|
||||||
|
name: iconPathPrefix,
|
||||||
|
size: 0,
|
||||||
|
datetime: 0,
|
||||||
|
mode: "normal",
|
||||||
|
virtual: true,
|
||||||
|
path: "/"
|
||||||
|
});
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
type: FileType.DIRECTORY,
|
||||||
|
name: avatarsPathPrefix,
|
||||||
|
size: 0,
|
||||||
|
datetime: 0,
|
||||||
|
mode: "normal",
|
||||||
|
virtual: true,
|
||||||
|
path: "/"
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestArray = connection.channelTree.channels.map(e => {
|
||||||
|
return {
|
||||||
|
request: {
|
||||||
|
path: "/",
|
||||||
|
channelId: e.channelId
|
||||||
|
},
|
||||||
|
name: channelPathPrefix + e.getChannelId(),
|
||||||
|
channel: e
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const channelInfos = await connection.fileManager.requestFileInfo(requestArray.map(e => e.request));
|
||||||
|
for(let index = 0; index < requestArray.length; index++) {
|
||||||
|
const response = channelInfos[index];
|
||||||
|
|
||||||
|
if(response instanceof CommandResult) {
|
||||||
|
/* some kind of error occured (maybe password set, or non existing) */
|
||||||
|
result.push({
|
||||||
|
type: FileType.DIRECTORY,
|
||||||
|
name: requestArray[index].name,
|
||||||
|
size: 0,
|
||||||
|
datetime: 0,
|
||||||
|
mode: requestArray[index].channel.properties.channel_flag_password ? "password" : "empty",
|
||||||
|
virtual: true,
|
||||||
|
path: "/"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
result.push({
|
||||||
|
type: FileType.DIRECTORY,
|
||||||
|
name: requestArray[index].name,
|
||||||
|
size: 0,
|
||||||
|
datetime: 0,
|
||||||
|
mode: response.empty ? "empty" : "normal",
|
||||||
|
virtual: true,
|
||||||
|
path: "/"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
})();
|
||||||
|
} else if(path.type === "channel") {
|
||||||
|
request = (async () => {
|
||||||
|
const hash = path.channel.properties.channel_flag_password ? await path.channel.requestChannelPassword(PermissionType.B_FT_IGNORE_PASSWORD) : undefined;
|
||||||
|
return connection.fileManager.requestFileList(path.path, path.channelId, hash?.hash).then(result => result.map(e => {
|
||||||
|
const transfer = connection.fileManager.findTransfer(path.channelId, path.path, e.name);
|
||||||
|
return {
|
||||||
|
datetime: e.datetime,
|
||||||
|
name: e.name,
|
||||||
|
size: e.size,
|
||||||
|
type: e.type,
|
||||||
|
path: event.path,
|
||||||
|
mode: e.empty ? "empty" : "normal",
|
||||||
|
virtual: false,
|
||||||
|
transfer: !transfer ? undefined : {
|
||||||
|
id: transfer.clientTransferId,
|
||||||
|
percent: transfer.isRunning() && transfer.lastProgressInfo() ? transfer.lastProgressInfo().file_current_offset / transfer.lastProgressInfo().file_total_size : 0,
|
||||||
|
status: transfer.isPending() ? "pending" : transfer.isRunning() ? "transferring" : "finished"
|
||||||
|
}
|
||||||
|
} as ListedFileInfo;
|
||||||
|
})).catch(async error => {
|
||||||
|
/* patch for the case that the channel directory hasn't been created yet */
|
||||||
|
if(error instanceof CommandResult) {
|
||||||
|
if(error.id === ErrorCode.FILE_NOT_FOUND && path.path === "/") {
|
||||||
|
return [];
|
||||||
|
} else if(error.id === 781) { //Invalid password
|
||||||
|
path.channel.invalidateCachedPassword();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
} else if(path.type === "icon" || path.type === "avatar") {
|
||||||
|
request = connection.fileManager.requestFileList(path.path, 0).then(result => result.map(e => {
|
||||||
|
return {
|
||||||
|
datetime: e.datetime,
|
||||||
|
name: e.name,
|
||||||
|
size: e.size,
|
||||||
|
type: e.type,
|
||||||
|
mode: e.empty ? "empty" : "normal",
|
||||||
|
path: event.path
|
||||||
|
} as ListedFileInfo;
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
events.fire_async("query_files_result", {
|
||||||
|
path: event.path,
|
||||||
|
status: "error",
|
||||||
|
error: tr("Unknown parsed path type")
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
request.then(files => {
|
||||||
|
events.fire_async("query_files_result", {
|
||||||
|
path: event.path,
|
||||||
|
status: "success",
|
||||||
|
files: files.map(e => { e.datetime *= 1000; return e; })
|
||||||
|
});
|
||||||
|
}).catch(error => {
|
||||||
|
let message;
|
||||||
|
if(error instanceof CommandResult) {
|
||||||
|
if(error.id === ErrorID.PERMISSION_ERROR) {
|
||||||
|
const permission = connection.permissions.resolveInfo(error.json["failed_permid"] as number);
|
||||||
|
events.fire_async("query_files_result", {
|
||||||
|
path: event.path,
|
||||||
|
status: "no-permissions",
|
||||||
|
error: permission ? permission.name : "unknown"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else if(error.id === 781) { //Invalid password
|
||||||
|
events.fire_async("query_files_result", {
|
||||||
|
path: event.path,
|
||||||
|
status: "invalid-password"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
message = error.message + (error.extra_message ? " (" + error.extra_message + ")" : "");
|
||||||
|
} else if(typeof error === "string") {
|
||||||
|
message = error;
|
||||||
|
} else {
|
||||||
|
log.error(LogCategory.FILE_TRANSFER, tr("Failed to query channel directory files: %o"), error);
|
||||||
|
message = tr("lookup the console");
|
||||||
|
}
|
||||||
|
|
||||||
|
events.fire_async("query_files_result", {
|
||||||
|
path: event.path,
|
||||||
|
status: "error",
|
||||||
|
error: message
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
events.on("action_rename_file", event => {
|
||||||
|
if(event.newPath === event.oldPath && event.newName === event.oldName) {
|
||||||
|
events.fire_async("action_rename_file_result", {
|
||||||
|
oldPath: event.oldPath,
|
||||||
|
oldName: event.oldName,
|
||||||
|
|
||||||
|
newPath: event.newPath,
|
||||||
|
newName: event.newName,
|
||||||
|
|
||||||
|
status: "no-changes"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sourcePath: PathInfo, targetPath: PathInfo;
|
||||||
|
try {
|
||||||
|
sourcePath = parsePath(event.oldPath, connection);
|
||||||
|
if(sourcePath.type !== "channel")
|
||||||
|
throw tr("Icon/avatars could not be renamed");
|
||||||
|
} catch (error) {
|
||||||
|
events.fire_async("action_rename_file_result", {
|
||||||
|
oldPath: event.oldPath,
|
||||||
|
oldName: event.oldName,
|
||||||
|
status: "error",
|
||||||
|
error: tr("Invalid source path") + " (" + error + ")"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
targetPath = parsePath(event.newPath, connection);
|
||||||
|
if(sourcePath.type !== "channel")
|
||||||
|
throw tr("Target path isn't a channel");
|
||||||
|
} catch (error) {
|
||||||
|
events.fire_async("action_rename_file_result", {
|
||||||
|
oldPath: event.oldPath,
|
||||||
|
oldName: event.oldName,
|
||||||
|
status: "error",
|
||||||
|
error: tr("Invalid target path") + " (" + error + ")"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const sourcePassword = sourcePath.channel.properties.channel_flag_password ? await sourcePath.channel.requestChannelPassword(PermissionType.B_FT_IGNORE_PASSWORD) : undefined;
|
||||||
|
const targetPassword = targetPath.channel.properties.channel_flag_password ? await targetPath.channel.requestChannelPassword(PermissionType.B_FT_IGNORE_PASSWORD) : undefined;
|
||||||
|
return await connection.serverConnection.send_command("ftrenamefile", {
|
||||||
|
cid: sourcePath.channelId,
|
||||||
|
cpw: sourcePassword,
|
||||||
|
tcid: targetPath.channelId,
|
||||||
|
tcpw: targetPassword,
|
||||||
|
oldname: sourcePath.path + event.oldName,
|
||||||
|
newname: targetPath.path + event.newName
|
||||||
|
})
|
||||||
|
})().then(result => {
|
||||||
|
if(result.id !== 0)
|
||||||
|
throw result;
|
||||||
|
|
||||||
|
events.fire("action_rename_file_result", {
|
||||||
|
oldPath: event.oldPath,
|
||||||
|
oldName: event.oldName,
|
||||||
|
status: "success",
|
||||||
|
|
||||||
|
newName: event.newName,
|
||||||
|
newPath: event.newPath
|
||||||
|
});
|
||||||
|
}).catch(error => {
|
||||||
|
let message;
|
||||||
|
if(error instanceof CommandResult) {
|
||||||
|
if(error.id === ErrorID.PERMISSION_ERROR) {
|
||||||
|
const permission = connection.permissions.resolveInfo(error.json["failed_permid"] as number);
|
||||||
|
events.fire_async("action_rename_file_result", {
|
||||||
|
oldPath: event.oldPath,
|
||||||
|
oldName: event.oldName,
|
||||||
|
status: "error",
|
||||||
|
error: tr("Failed on permission ") + (permission ? permission.name : "unknown")
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else if(error.id === 781) { //Invalid password
|
||||||
|
events.fire_async("action_rename_file_result", {
|
||||||
|
oldPath: event.oldPath,
|
||||||
|
oldName: event.oldName,
|
||||||
|
status: "error",
|
||||||
|
error: tr("Invalid channel password")
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
message = error.message + (error.extra_message ? " (" + error.extra_message + ")" : "");
|
||||||
|
} else if(typeof error === "string") {
|
||||||
|
message = error;
|
||||||
|
} else {
|
||||||
|
log.error(LogCategory.FILE_TRANSFER, tr("Failed to rename/move files: %o"), error);
|
||||||
|
message = tr("lookup the console");
|
||||||
|
}
|
||||||
|
events.fire_async("action_rename_file_result", {
|
||||||
|
oldPath: event.oldPath,
|
||||||
|
oldName: event.oldName,
|
||||||
|
status: "error",
|
||||||
|
error: message
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* currently selected files */
|
||||||
|
let currentPath = "/";
|
||||||
|
let currentPathInfo: PathInfo;
|
||||||
|
let selection: { name: string, type: FileType }[] = [];
|
||||||
|
events.on("action_navigate_to_result", result => {
|
||||||
|
if(result.status !== "success")
|
||||||
|
return;
|
||||||
|
|
||||||
|
currentPathInfo = result.pathInfo;
|
||||||
|
currentPath = result.path;
|
||||||
|
selection = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
events.on("action_rename_file_result", result => {
|
||||||
|
if(result.status !== "success")
|
||||||
|
return;
|
||||||
|
if(result.oldPath !== currentPath)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const index = selection.map(e => e.name).findIndex(e => e === result.oldName);
|
||||||
|
if(index !== -1)
|
||||||
|
selection[index].name = result.newName;
|
||||||
|
});
|
||||||
|
|
||||||
|
events.on("action_select_files", event => {
|
||||||
|
if(event.mode === "exclusive") {
|
||||||
|
selection = event.files.slice(0);
|
||||||
|
} else if(event.mode === "toggle") {
|
||||||
|
event.files.forEach(e => {
|
||||||
|
const index = selection.map(e => e.name).findIndex(b => b === e.name);
|
||||||
|
if(index === -1)
|
||||||
|
selection.push(e);
|
||||||
|
else
|
||||||
|
selection.splice(index);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* the selection handler */
|
||||||
|
events.on("action_selection_context_menu", event => {
|
||||||
|
const entries = [] as MenuEntry[];
|
||||||
|
|
||||||
|
if(currentPathInfo.type === "root") {
|
||||||
|
entries.push({
|
||||||
|
type: MenuEntryType.ENTRY,
|
||||||
|
name: tr("Refresh file list"),
|
||||||
|
icon_class: "client-file_refresh"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const forceDelete = ppt.key_pressed(SpecialKey.SHIFT);
|
||||||
|
if(selection.length === 0) {
|
||||||
|
entries.push({
|
||||||
|
type: MenuEntryType.ENTRY,
|
||||||
|
name: tr("Upload"),
|
||||||
|
icon_class: "client-upload",
|
||||||
|
callback: () => events.fire("action_start_upload", { mode: "browse", path: currentPath })
|
||||||
|
});
|
||||||
|
} else if(selection.length === 1) {
|
||||||
|
const file = selection[0];
|
||||||
|
if(file.type === FileType.FILE) {
|
||||||
|
entries.push({
|
||||||
|
type: MenuEntryType.ENTRY,
|
||||||
|
name: tr("Download"),
|
||||||
|
icon_class: "client-download",
|
||||||
|
callback: () => events.fire("action_start_download", { files: [{ name: file.name, path: currentPath }] })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(currentPathInfo.type === "channel") {
|
||||||
|
entries.push({
|
||||||
|
type: MenuEntryType.ENTRY,
|
||||||
|
name: tr("Rename"),
|
||||||
|
icon_class: "client-change_nickname",
|
||||||
|
callback: () => events.fire("action_start_rename", { name: file.name, path: currentPath })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
entries.push({
|
||||||
|
type: MenuEntryType.ENTRY,
|
||||||
|
name: forceDelete ? tr("Force delete file") : tr("Delete file"),
|
||||||
|
icon_class: "client-delete",
|
||||||
|
callback: () => events.fire("action_delete_file", { mode: forceDelete ? "force" : "ask", files: "selection" })
|
||||||
|
});
|
||||||
|
entries.push(Entry.HR());
|
||||||
|
} else if(selection.length > 1) {
|
||||||
|
if(selection.findIndex(e => e.type === FileType.DIRECTORY) === -1) {
|
||||||
|
entries.push({
|
||||||
|
type: MenuEntryType.ENTRY,
|
||||||
|
name: tr("Download"),
|
||||||
|
icon_class: "client-download",
|
||||||
|
callback: () => events.fire("action_start_download", { files: selection.map(file => { return { name: file.name, path: currentPath }}) })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
entries.push({
|
||||||
|
type: MenuEntryType.ENTRY,
|
||||||
|
name: forceDelete ? tr("Force delete files") : tr("Delete files"),
|
||||||
|
icon_class: "client-delete",
|
||||||
|
callback: () => events.fire("action_delete_file", { mode: forceDelete ? "force" : "ask", files: "selection" })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
entries.push({
|
||||||
|
type: MenuEntryType.ENTRY,
|
||||||
|
name: tr("Refresh file list"),
|
||||||
|
icon_class: "client-file_refresh",
|
||||||
|
callback: () => events.fire("action_navigate_to", { path: currentPath })
|
||||||
|
});
|
||||||
|
entries.push(Entry.HR());
|
||||||
|
entries.push({
|
||||||
|
type: MenuEntryType.ENTRY,
|
||||||
|
name: tr("Create folder"),
|
||||||
|
icon_class: "client-add_folder",
|
||||||
|
callback: () => events.fire("action_start_create_directory", { defaultName: tr("New folder") })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
spawn_context_menu(event.pageX, event.pageY, ...entries);
|
||||||
|
});
|
||||||
|
|
||||||
|
events.on("action_delete_file", event => {
|
||||||
|
const files = event.files === "selection" ? selection.map(e => { return { path: currentPath, name: e.name }}) : event.files;
|
||||||
|
|
||||||
|
if(event.mode === "ask") {
|
||||||
|
spawnYesNo(tr("Are you sure?"), tra("Do you really want to delete {0} {1}?", files.length, files.length === 1 ? tr("files") : tr("files")), result => {
|
||||||
|
if(result)
|
||||||
|
events.fire("action_delete_file", {
|
||||||
|
files: files,
|
||||||
|
mode: "force"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileInfos = files.map(e => { return { info: parsePath(e.path, connection), path: e.path, name: e.name }});
|
||||||
|
|
||||||
|
connection.serverConnection.send_command("ftdeletefile", fileInfos.map(e => { return {
|
||||||
|
path: e.info.path,
|
||||||
|
cid: e.info.channelId,
|
||||||
|
cpw: e.info.channel?.cached_password(),
|
||||||
|
name: e.name
|
||||||
|
}})).then(async result => {
|
||||||
|
throw result;
|
||||||
|
}).catch(result => {
|
||||||
|
let message;
|
||||||
|
if(result instanceof CommandResult) {
|
||||||
|
if(result.bulks.length !== fileInfos.length) {
|
||||||
|
events.fire_async("action_delete_file_result", {
|
||||||
|
results: fileInfos.map((e) => {
|
||||||
|
return {
|
||||||
|
error: result.bulks.length === 1 ? (result.message + (result.extra_message ? " (" + result.extra_message + ")" : "")) : tr("Response contained invalid bulk length"),
|
||||||
|
path: e.path,
|
||||||
|
name: e.name,
|
||||||
|
status: "error"
|
||||||
|
};
|
||||||
|
})
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let results = [];
|
||||||
|
result.getBulks().forEach((e, index) => {
|
||||||
|
if(e.id === ErrorID.PERMISSION_ERROR) {
|
||||||
|
const permission = connection.permissions.resolveInfo(e.json["failed_permid"] as number);
|
||||||
|
results.push({
|
||||||
|
path: fileInfos[index].path,
|
||||||
|
name: fileInfos[index].name,
|
||||||
|
status: "error",
|
||||||
|
error: tr("Failed on permission ") + (permission ? permission.name : "unknown")
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else if(e.id === 781) { //Invalid password
|
||||||
|
results.push({
|
||||||
|
path: fileInfos[index].path,
|
||||||
|
name: fileInfos[index].name,
|
||||||
|
status: "error",
|
||||||
|
error: tr("Invalid channel password")
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else if(e.id !== 0) {
|
||||||
|
results.push({
|
||||||
|
path: fileInfos[index].path,
|
||||||
|
name: fileInfos[index].name,
|
||||||
|
status: "error",
|
||||||
|
error: e.message + (e.extra_message ? " (" + e.extra_message + ")" : "")
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
path: fileInfos[index].path,
|
||||||
|
name: fileInfos[index].name,
|
||||||
|
status: "success"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
events.fire_async("action_delete_file_result", {
|
||||||
|
results: results
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else if(typeof result === "string") {
|
||||||
|
message = result;
|
||||||
|
} else {
|
||||||
|
log.error(LogCategory.FILE_TRANSFER, tr("Failed to create directory: %o"), result);
|
||||||
|
message = tr("lookup the console");
|
||||||
|
}
|
||||||
|
|
||||||
|
events.fire_async("action_delete_file_result", {
|
||||||
|
results: files.map((e) => {
|
||||||
|
return {
|
||||||
|
error: message,
|
||||||
|
path: e.path,
|
||||||
|
name: e.name,
|
||||||
|
status: "error"
|
||||||
|
};
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
events.fire_async("action_delete_file_result", {
|
||||||
|
results: files.map((e) => {
|
||||||
|
return {
|
||||||
|
error: tr("Failed to parse path for one or more entries ") + " (" + error + ")",
|
||||||
|
path: e.path,
|
||||||
|
name: e.name,
|
||||||
|
status: "error"
|
||||||
|
};
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
events.on("action_create_directory", event => {
|
||||||
|
let path: PathInfo;
|
||||||
|
try {
|
||||||
|
path = parsePath(event.path, connection);
|
||||||
|
if(path.type !== "channel")
|
||||||
|
throw tr("Directories could only created for channels");
|
||||||
|
} catch (error) {
|
||||||
|
events.fire_async("action_create_directory_result", {
|
||||||
|
name: event.name,
|
||||||
|
path: event.path,
|
||||||
|
status: "error",
|
||||||
|
error: tr("Invalid path") + " (" + error + ")"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//ftcreatedir cid=4 cpw dirname=\/TestDir return_code=1:17
|
||||||
|
connection.serverConnection.send_command("ftcreatedir", {
|
||||||
|
cid: path.channelId,
|
||||||
|
cpw: path.channel.cached_password(),
|
||||||
|
dirname: path.path + event.name
|
||||||
|
}).then(() => {
|
||||||
|
events.fire("action_create_directory_result", { path: event.path, name: event.name, status: "success" });
|
||||||
|
}).catch(error => {
|
||||||
|
let message;
|
||||||
|
if(error instanceof CommandResult) {
|
||||||
|
if(error.id === ErrorID.PERMISSION_ERROR) {
|
||||||
|
const permission = connection.permissions.resolveInfo(error.json["failed_permid"] as number);
|
||||||
|
events.fire_async("action_create_directory_result", {
|
||||||
|
name: event.name,
|
||||||
|
path: event.path,
|
||||||
|
status: "error",
|
||||||
|
error: tr("Failed on permission ") + (permission ? permission.name : "unknown")
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else if(error.id === 781) { //Invalid password
|
||||||
|
events.fire_async("action_create_directory_result", {
|
||||||
|
name: event.name,
|
||||||
|
path: event.path,
|
||||||
|
status: "error",
|
||||||
|
error: tr("Invalid channel password")
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
message = error.message + (error.extra_message ? " (" + error.extra_message + ")" : "");
|
||||||
|
} else if(typeof error === "string") {
|
||||||
|
message = error;
|
||||||
|
} else {
|
||||||
|
log.error(LogCategory.FILE_TRANSFER, tr("Failed to create directory: %o"), error);
|
||||||
|
message = tr("lookup the console");
|
||||||
|
}
|
||||||
|
events.fire_async("action_create_directory_result", {
|
||||||
|
name: event.name,
|
||||||
|
path: event.path,
|
||||||
|
status: "error",
|
||||||
|
error: message
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
events.on("action_start_download", event => {
|
||||||
|
event.files.forEach(file => {
|
||||||
|
try {
|
||||||
|
const fileName = file.name;
|
||||||
|
const info = parsePath(file.path, connection);
|
||||||
|
const transfer = connection.fileManager.initializeFileDownload({
|
||||||
|
channel: info.channelId,
|
||||||
|
path: info.type === "channel" ? info.path : "",
|
||||||
|
name: info.type === "channel" ? file.name : "/" + file.name,
|
||||||
|
channelPassword: info.channel?.cached_password(),
|
||||||
|
targetSupplier: async () => TransferProvider.provider().createDownloadTarget()
|
||||||
|
});
|
||||||
|
transfer.awaitFinished().then(() => {
|
||||||
|
if(transfer.transferState() === FileTransferState.ERRORED) {
|
||||||
|
createErrorModal(tr("Failed to download file"), traj("Failed to download {0}:{:br:}{1}", fileName, transfer.currentErrorMessage())).open();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
log.error(LogCategory.FILE_TRANSFER, tr("Failed to parse path for file download: %s"), error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
events.on("action_start_upload", event => {
|
||||||
|
if(event.mode === "browse") {
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "file";
|
||||||
|
input.multiple = true;
|
||||||
|
|
||||||
|
document.body.appendChild(input);
|
||||||
|
input.onchange = () => {
|
||||||
|
if((input.files?.length | 0) === 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
events.fire("action_start_upload", { mode: "files", path: event.path, files: [...input.files] });
|
||||||
|
};
|
||||||
|
input.onblur = () => input.remove();
|
||||||
|
setTimeout(() => {
|
||||||
|
input.focus({ preventScroll: true });
|
||||||
|
input.click();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else if(event.mode === "files") {
|
||||||
|
const pathInfo = parsePath(event.path, connection);
|
||||||
|
if(pathInfo.type !== "channel") {
|
||||||
|
createErrorModal(tr("Failed to upload file(s)"), tra("Failed to upload files:{:br:}File uplaod is only supported in channel directories")).open();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for(const file of event.files) {
|
||||||
|
const fileName = file.name;
|
||||||
|
const transfer = connection.fileManager.initializeFileUpload({
|
||||||
|
channel: pathInfo.channelId,
|
||||||
|
channelPassword: pathInfo.channel?.cached_password(),
|
||||||
|
name: file.name,
|
||||||
|
path: pathInfo.path,
|
||||||
|
source: async () => TransferProvider.provider().createBrowserFileSource(file)
|
||||||
|
});
|
||||||
|
transfer.awaitFinished().then(() => {
|
||||||
|
if(transfer.transferState() === FileTransferState.ERRORED) {
|
||||||
|
createErrorModal(tr("Failed to upload file"), tra("Failed to upload {0}:{:br:}{1}", fileName, transfer.currentErrorMessage())).open();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* transfer status listener */
|
||||||
|
{
|
||||||
|
const listenToTransfer = (transfer: FileTransfer) => {
|
||||||
|
/* We've currently only support for channel files */
|
||||||
|
if(transfer.properties.channel_id === 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const progressListener = event => events.fire("notify_transfer_progress", {
|
||||||
|
id: transfer.clientTransferId,
|
||||||
|
progress: event.progress.file_current_offset / event.progress.file_total_size,
|
||||||
|
status: "transferring",
|
||||||
|
fileSize: event.progress.file_current_offset
|
||||||
|
});
|
||||||
|
|
||||||
|
transfer.events.on("notify_progress", progressListener);
|
||||||
|
transfer.events.on("notify_state_updated", () => {
|
||||||
|
switch (transfer.transferState()) {
|
||||||
|
case FileTransferState.INITIALIZING:
|
||||||
|
case FileTransferState.PENDING:
|
||||||
|
case FileTransferState.CONNECTING:
|
||||||
|
events.fire("notify_transfer_status", {id: transfer.clientTransferId, status: "pending"});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FileTransferState.RUNNING:
|
||||||
|
events.fire("notify_transfer_status", { id: transfer.clientTransferId, status: "transferring", fileSize: transfer.transferProperties().fileSize });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FileTransferState.FINISHED:
|
||||||
|
case FileTransferState.CANCELED:
|
||||||
|
events.fire("notify_transfer_status", { id: transfer.clientTransferId, status: "finished" });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FileTransferState.ERRORED:
|
||||||
|
events.fire("notify_transfer_status", { id: transfer.clientTransferId, status: "errored" });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(transfer.isFinished()) {
|
||||||
|
unregisterEvents();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
events.fire("notify_transfer_start", {
|
||||||
|
id: transfer.clientTransferId,
|
||||||
|
name: transfer.properties.name,
|
||||||
|
path: "/" + channelPathPrefix + transfer.properties.channel_id + transfer.properties.path,
|
||||||
|
mode: transfer instanceof FileUploadTransfer ? "upload" : "download"
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeListener = () => unregisterEvents();
|
||||||
|
events.on("notify_modal_closed", closeListener);
|
||||||
|
|
||||||
|
const unregisterEvents = () => {
|
||||||
|
events.off("notify_modal_closed", closeListener);
|
||||||
|
transfer.events.off("notify_progress", progressListener);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
connection.fileManager.registeredTransfers().forEach(transfer => listenToTransfer(transfer));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,300 @@
|
||||||
|
@import "../../../../css/static/mixin";
|
||||||
|
@import "../../../../css/static/properties";
|
||||||
|
|
||||||
|
.container {
|
||||||
|
z-index: 1;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
bottom: .75em;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
@include user-select(none);
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
position: absolute;
|
||||||
|
background-color: #19191b;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
opacity: 1;
|
||||||
|
@include transition($button_hover_animation_time ease-in-out);
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.noTransfers, &.querying, &.error {
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #595959;
|
||||||
|
align-self: center;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.extended {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.expendedContainer {
|
||||||
|
height: 100%;
|
||||||
|
margin-top: auto;
|
||||||
|
background-color: #19191b;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
padding: 1em;
|
||||||
|
pointer-events: all;
|
||||||
|
|
||||||
|
@include transition($button_hover_animation_time ease-in-out);
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
padding: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
a {
|
||||||
|
font-size: 1.4em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
padding-bottom: .5em;
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #cccccc;
|
||||||
|
font-size: 1.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
align-self: center;
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: .7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
border-bottom: 1px solid #393939;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
min-height: 2em;
|
||||||
|
|
||||||
|
/* for the scroll bar */
|
||||||
|
padding-right: .5em;
|
||||||
|
margin-right: -.5em;
|
||||||
|
|
||||||
|
@include chat-scrollbar-vertical();
|
||||||
|
|
||||||
|
.noTransfers, .queryError, .querying {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
font-size: 1.2em;
|
||||||
|
padding: 1em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
a {
|
||||||
|
align-self: center;
|
||||||
|
color: #595959;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottomContainer {
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
|
||||||
|
height: 2em;
|
||||||
|
padding-left: 1em;
|
||||||
|
padding-right: 1em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
pointer-events: all;
|
||||||
|
|
||||||
|
.info {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
height: 1.5em;
|
||||||
|
min-width: 1.5em;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.runningTransfers {
|
||||||
|
padding-right: 1em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.expansionContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
height: 1.5em;
|
||||||
|
width: 1.5em;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
|
height: 1.4em;
|
||||||
|
fill: hsla(0, 0%, 21%, 1);
|
||||||
|
|
||||||
|
@include transform(rotate(-180deg));
|
||||||
|
@include transition($button_hover_animation_time ease-in-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.expended {
|
||||||
|
svg {
|
||||||
|
@include transform(rotate(-90deg));
|
||||||
|
@include transition($button_hover_animation_time ease-in-out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
svg {
|
||||||
|
fill: hsla(0, 0%, 25%, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.transferEntryContainer {
|
||||||
|
margin-top: .5em;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
|
||||||
|
height: 3.5em;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
@include transition($button_hover_animation_time ease-in-out);
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
margin-top: 0;
|
||||||
|
height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.transferEntry {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
background-color: #19191b;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 3.5em;
|
||||||
|
|
||||||
|
.image {
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
width: 3em;
|
||||||
|
height: 3em;
|
||||||
|
|
||||||
|
margin-left: .5em;
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-grow: 1;
|
||||||
|
min-width: 2em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
.name {
|
||||||
|
margin-top: .2em;
|
||||||
|
line-height: 1em;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.path {
|
||||||
|
margin-top: .1em;
|
||||||
|
line-height: 1em;
|
||||||
|
font-size: .75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
margin-top: .3em;
|
||||||
|
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
font-size: .7em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,478 @@
|
||||||
|
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";
|
||||||
|
import {
|
||||||
|
TransferProgress,
|
||||||
|
} from "tc-shared/file/Transfer";
|
||||||
|
import {tra} from "tc-shared/i18n/localize";
|
||||||
|
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";
|
||||||
|
|
||||||
|
const cssStyle = require("./TransferInfo.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);
|
||||||
|
|
||||||
|
props.events.reactUse("action_toggle_expansion", event => setExpended(event.visible));
|
||||||
|
return <div className={cssStyle.expansionContainer + (expended ? " " + cssStyle.expended : "")} onClick={() => props.events.fire("action_toggle_expansion", { visible: !expended })}>
|
||||||
|
<HTMLRenderer purify={false}>{iconArrow}</HTMLRenderer>
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ToggleFinishedTransfersCheckbox = (props: { events: Registry<TransferInfoEvents> }) => {
|
||||||
|
const ref = useRef<Checkbox>(null);
|
||||||
|
const [state, setState] = useState({ disabled: true, checked: false });
|
||||||
|
props.events.reactUse("action_toggle_finished_transfers", event => {
|
||||||
|
setState({
|
||||||
|
checked: event.visible,
|
||||||
|
disabled: false
|
||||||
|
});
|
||||||
|
ref.current?.setState({
|
||||||
|
checked: event.visible,
|
||||||
|
disabled: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
props.events.reactUse("query_transfer_result", event => {
|
||||||
|
if(event.status !== "success")
|
||||||
|
return;
|
||||||
|
|
||||||
|
setState({
|
||||||
|
checked: event.showFinished,
|
||||||
|
disabled: false
|
||||||
|
});
|
||||||
|
ref.current?.setState({
|
||||||
|
checked: event.showFinished,
|
||||||
|
disabled: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
ref={ref}
|
||||||
|
initialValue={state.checked}
|
||||||
|
disabled={state.disabled}
|
||||||
|
onChange={state => props.events.fire("action_toggle_finished_transfers", { visible: state })}
|
||||||
|
label={<Translatable>Show finished transfers</Translatable>} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
@ReactEventHandler<RunningTransfersInfo>(e => e.props.events)
|
||||||
|
class RunningTransfersInfo extends React.Component<{ events: Registry<TransferInfoEvents> }, { state: "error" | "querying" | "normal" }> {
|
||||||
|
private runningTransfers: { transfer: TransferInfoData, progress: TransferProgress | undefined }[] = [];
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
state: "querying"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private currentStatistic() {
|
||||||
|
const progress = this.runningTransfers.map(e => e.progress).filter(e => !!e);
|
||||||
|
return {
|
||||||
|
totalBytes: progress.map(e => e.file_total_size).reduce((a, b) => a + b, 0),
|
||||||
|
currentOffset: progress.map(e => e.file_current_offset).reduce((a, b) => a + b, 0),
|
||||||
|
speed: progress.map(e => e.network_current_speed).reduce((a, b) => a + b, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if(this.state.state === "querying") {
|
||||||
|
return (
|
||||||
|
<div key={"querying"} className={cssStyle.overlay + " " + cssStyle.querying}>
|
||||||
|
<a><Translatable>loading</Translatable> <LoadingDots maxDots={3} /></a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if(this.state.state === "error") {
|
||||||
|
return (
|
||||||
|
<div key={"query-error"} className={cssStyle.overlay + " " + cssStyle.error}>
|
||||||
|
<a><Translatable>query error</Translatable></a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if(this.runningTransfers.length === 0) {
|
||||||
|
return (
|
||||||
|
<div key={"no-transfers"} className={cssStyle.overlay + " " + cssStyle.noTransfers}>
|
||||||
|
<a><Translatable>No running transfers</Translatable></a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = this.currentStatistic();
|
||||||
|
const totalBytes = network.format_bytes(stats.totalBytes, { unit: "B", time: "", exact: false });
|
||||||
|
const currentOffset = network.format_bytes(stats.currentOffset, { unit: "B", time: "", exact: false });
|
||||||
|
const speed = network.format_bytes(stats.speed, { unit: "B", time: "second", exact: false });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={"running-transfers"} className={cssStyle.overlay + " " + cssStyle.runningTransfers}>
|
||||||
|
<ProgressBar value={stats.currentOffset * 100 / stats.totalBytes} type={"normal"} text={tra("Transferred {0} out of {1} total bytes ({2})", currentOffset, totalBytes, speed)} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler<TransferInfoEvents>("query_transfers")
|
||||||
|
private handleQueryTransfers() {
|
||||||
|
this.setState({ state: "querying" });
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler<TransferInfoEvents>("query_transfer_result")
|
||||||
|
private handleQueryTransferResult(event: TransferInfoEvents["query_transfer_result"]) {
|
||||||
|
this.setState({
|
||||||
|
state: event.status !== "success" ? "error" : "normal"
|
||||||
|
});
|
||||||
|
|
||||||
|
this.runningTransfers = (event.transfers || []).filter(e => e.status !== "finished" && e.status !== "errored").map(e => {
|
||||||
|
return {
|
||||||
|
progress: undefined,
|
||||||
|
transfer: e
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler<TransferInfoEvents>("notify_transfer_registered")
|
||||||
|
private handleTransferRegistered(event: TransferInfoEvents["notify_transfer_registered"]) {
|
||||||
|
this.runningTransfers.push({ transfer: event.transfer, progress: undefined });
|
||||||
|
this.forceUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler<TransferInfoEvents>("notify_transfer_status")
|
||||||
|
private handleTransferStatus(event: TransferInfoEvents["notify_transfer_status"]) {
|
||||||
|
const index = this.runningTransfers.findIndex(e => e.transfer.id === event.id);
|
||||||
|
if(index === -1) return;
|
||||||
|
|
||||||
|
if(event.status === "finished" || event.status === "errored")
|
||||||
|
this.runningTransfers.splice(index, 1);
|
||||||
|
this.forceUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler<TransferInfoEvents>("notify_transfer_progress")
|
||||||
|
private handleTransferProgress(event: TransferInfoEvents["notify_transfer_progress"]) {
|
||||||
|
const index = this.runningTransfers.findIndex(e => e.transfer.id === event.id);
|
||||||
|
if(index === -1) return;
|
||||||
|
|
||||||
|
this.runningTransfers[index].progress = event.progress;
|
||||||
|
this.forceUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const BottomTransferInfo = (props: { events: Registry<TransferInfoEvents> }) => {
|
||||||
|
const [extendedInfo, setExtendedInfo] = useState(false);
|
||||||
|
|
||||||
|
props.events.reactUse("action_toggle_expansion", event => setExtendedInfo(event.visible));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.info}>
|
||||||
|
<RunningTransfersInfo events={props.events} />
|
||||||
|
<div className={cssStyle.overlay + (extendedInfo ? "" : " " + cssStyle.hidden) + " " + cssStyle.extended} >
|
||||||
|
<ToggleFinishedTransfersCheckbox events={props.events} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
const BottomBar = (props: { events: Registry<TransferInfoEvents> }) => (
|
||||||
|
<div className={cssStyle.bottomContainer}>
|
||||||
|
<BottomTransferInfo events={props.events} />
|
||||||
|
<ExpendState extended={false} events={props.events} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const TransferEntry = (props: { transfer: TransferInfoData, events: Registry<TransferInfoEvents>, finishedShown: boolean }) => {
|
||||||
|
const [finishedShown, setFinishedShown] = useState(props.finishedShown);
|
||||||
|
const [transferState, setTransferState] = useState<TransferStatus>(props.transfer.status);
|
||||||
|
const [finishAnimationFinished, setFinishAnimationFinished] = useState(props.transfer.status === "finished" || props.transfer.status === "errored");
|
||||||
|
|
||||||
|
const progressBar = useRef<ProgressBar>(null);
|
||||||
|
|
||||||
|
const progressBarText = (status: TransferStatus, info?: TransferProgress) => {
|
||||||
|
switch (status) {
|
||||||
|
case "errored":
|
||||||
|
return props.transfer.error ? tr("file transfer failed: ") + props.transfer.error : tr("file transfer failed");
|
||||||
|
|
||||||
|
case "finished":
|
||||||
|
const neededTime = format_time(props.transfer.timestampEnd - props.transfer.timestampBegin, tr("less than a second"));
|
||||||
|
const totalBytes = network.format_bytes(props.transfer.transferredBytes, { unit: "B", time: "", exact: false });
|
||||||
|
const speed = network.format_bytes(props.transfer.transferredBytes * 1000 / Math.max(props.transfer.timestampEnd - props.transfer.timestampBegin, 1000), { unit: "B", time: "second", exact: false });
|
||||||
|
return tra("transferred {0} in {1} ({2})", totalBytes, format_time(props.transfer.timestampEnd - props.transfer.timestampBegin, neededTime), speed);
|
||||||
|
|
||||||
|
case "pending":
|
||||||
|
return tr("pending");
|
||||||
|
|
||||||
|
case "none":
|
||||||
|
return tr("invalid state!");
|
||||||
|
|
||||||
|
case "transferring": {
|
||||||
|
if(!info) {
|
||||||
|
return tr("awaiting info");
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentBytes = network.format_bytes(info.file_current_offset, { unit: "B", time: "", exact: false });
|
||||||
|
const totalBytes = network.format_bytes(info.file_total_size, { unit: "B", time: "", exact: false });
|
||||||
|
const speed = network.format_bytes(info.network_current_speed, { unit: "B", time: "second", exact: false });
|
||||||
|
|
||||||
|
return tra("transferred {0} out of {1} ({2})", currentBytes, totalBytes, speed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const progressBarMode = (status: TransferStatus) => {
|
||||||
|
switch (status) {
|
||||||
|
case "errored":
|
||||||
|
return "error";
|
||||||
|
case "finished":
|
||||||
|
return "success";
|
||||||
|
default:
|
||||||
|
return "normal";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
props.events.reactUse("notify_transfer_status", event => {
|
||||||
|
if(event.id !== props.transfer.id)
|
||||||
|
return;
|
||||||
|
|
||||||
|
setTransferState(event.status);
|
||||||
|
if(!progressBar.current)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const pbState = {
|
||||||
|
text: progressBarText(event.status),
|
||||||
|
type: progressBarMode(event.status)
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
if(event.status === "errored" || event.status === "finished") {
|
||||||
|
pbState.value = 100;
|
||||||
|
|
||||||
|
} else if(event.status === "none" || event.status === "pending")
|
||||||
|
pbState.value = 0;
|
||||||
|
|
||||||
|
progressBar.current.setState(pbState);
|
||||||
|
});
|
||||||
|
|
||||||
|
props.events.reactUse("notify_transfer_progress", event => {
|
||||||
|
if(event.id !== props.transfer.id || !progressBar.current)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const pb = progressBar.current;
|
||||||
|
pb.setState({
|
||||||
|
text: progressBarText(event.status, event.progress),
|
||||||
|
type: progressBarMode(event.status),
|
||||||
|
value: event.status === "errored" || event.status === "finished" ? 100 : event.status === "pending" || event.status === "none" ? 0 : (event.progress.file_current_offset / event.progress.file_total_size) * 100
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
props.events.reactUse("action_toggle_finished_transfers", event => setFinishedShown(event.visible));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(finishAnimationFinished)
|
||||||
|
return;
|
||||||
|
if(transferState !== "finished" && transferState !== "errored")
|
||||||
|
return;
|
||||||
|
|
||||||
|
const id = setTimeout(() => setFinishAnimationFinished(true), 1500);
|
||||||
|
return () => clearTimeout(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
let hidden = transferState === "finished" || transferState === "errored" ? !finishedShown && finishAnimationFinished : false;
|
||||||
|
return <div className={cssStyle.transferEntryContainer + (hidden ? " " + cssStyle.hidden : "")}>
|
||||||
|
<div className={cssStyle.transferEntry}>
|
||||||
|
<div className={cssStyle.image}>
|
||||||
|
<HTMLRenderer purify={false}>{props.transfer.direction === "upload" ? iconTransferUpload : iconTransferDownload}</HTMLRenderer>
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.info}>
|
||||||
|
<a className={cssStyle.name}>{props.transfer.name}</a>
|
||||||
|
<a className={cssStyle.path}>{props.transfer.path}</a>
|
||||||
|
<div className={cssStyle.status}>
|
||||||
|
<ProgressBar ref={progressBar} value={props.transfer.progress * 100} type={progressBarMode(transferState)} text={progressBarText(transferState)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
};
|
||||||
|
|
||||||
|
@ReactEventHandler<TransferList>(e => e.props.events)
|
||||||
|
class TransferList extends React.PureComponent<{ events: Registry<TransferInfoEvents> }, { state: "loading" | "error" | "normal", error?: string }> {
|
||||||
|
private transfers: TransferInfoData[] = [];
|
||||||
|
private showFinishedTransfers: boolean = true;
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
state: "loading"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const entries = [];
|
||||||
|
|
||||||
|
if(this.state.state === "error") {
|
||||||
|
entries.push(<div key={"query-error"} className={cssStyle.queryError}><a><Translatable>Failed to query the file transfers:</Translatable><br/>{this.state.error}</a></div>);
|
||||||
|
} else if(this.state.state === "loading") {
|
||||||
|
entries.push(<div key={"loading"} className={cssStyle.querying}><a><Translatable>loading</Translatable> <LoadingDots maxDots={3}/></a></div>);
|
||||||
|
} else {
|
||||||
|
this.transfers.forEach(e => {
|
||||||
|
entries.push(<TransferEntry finishedShown={this.showFinishedTransfers} key={"transfer-" + e.id} transfer={e} events={this.props.events} />);
|
||||||
|
});
|
||||||
|
entries.push(<div key={"no-transfers"} className={cssStyle.noTransfers}><a><Translatable>No transfers</Translatable></a></div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.list}>{entries}</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount(): void {
|
||||||
|
this.props.events.fire("query_transfers");
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler<TransferInfoEvents>("action_toggle_finished_transfers")
|
||||||
|
private handleToggleFinishedTransfers(event: TransferInfoEvents["action_toggle_finished_transfers"]) {
|
||||||
|
this.showFinishedTransfers = event.visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@EventHandler<TransferInfoEvents>("action_remove_finished")
|
||||||
|
private handleRemoveFinishedTransfers() {
|
||||||
|
this.transfers = this.transfers.filter(e => e.status !== "finished" && e.status !== "errored");
|
||||||
|
this.forceUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler<TransferInfoEvents>("query_transfers")
|
||||||
|
private handleQueryTransfers() {
|
||||||
|
this.setState({ state: "loading" });
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler<TransferInfoEvents>("query_transfer_result")
|
||||||
|
private handleQueryTransferResult(event: TransferInfoEvents["query_transfer_result"]) {
|
||||||
|
this.setState({
|
||||||
|
state: event.status === "success" ? "normal" : "error",
|
||||||
|
error: event.status === "timeout" ? tr("Request timed out") : event.error || tr("unknown error")
|
||||||
|
});
|
||||||
|
if(event.status === "success")
|
||||||
|
this.showFinishedTransfers = event.showFinished;
|
||||||
|
|
||||||
|
this.transfers = event.transfers || [];
|
||||||
|
this.transfers.sort((a, b) => b.timestampRegistered - a.timestampRegistered);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler<TransferInfoEvents>("notify_transfer_registered")
|
||||||
|
private handleTransferRegistered(event: TransferInfoEvents["notify_transfer_registered"]) {
|
||||||
|
this.transfers.splice(0, 0, event.transfer);
|
||||||
|
this.forceUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler<TransferInfoEvents>("notify_transfer_status")
|
||||||
|
private handleTransferStatus(event: TransferInfoEvents["notify_transfer_status"]) {
|
||||||
|
const transfer = this.transfers.find(e => e.id === event.id);
|
||||||
|
if(!transfer) return;
|
||||||
|
|
||||||
|
switch (event.status) {
|
||||||
|
case "finished":
|
||||||
|
case "errored":
|
||||||
|
case "none":
|
||||||
|
transfer.timestampEnd = Date.now();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "transferring":
|
||||||
|
if(transfer.timestampBegin === 0)
|
||||||
|
transfer.timestampBegin = Date.now();
|
||||||
|
}
|
||||||
|
transfer.status = event.status;
|
||||||
|
transfer.error = event.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler<TransferInfoEvents>("notify_transfer_progress")
|
||||||
|
private handleTransferProgress(event: TransferInfoEvents["notify_transfer_progress"]) {
|
||||||
|
const transfer = this.transfers.find(e => e.id === event.id);
|
||||||
|
if(!transfer) return;
|
||||||
|
|
||||||
|
transfer.progress = event.progress.file_current_offset / event.progress.file_total_size;
|
||||||
|
transfer.status = event.status;
|
||||||
|
transfer.transferredBytes = event.progress.file_bytes_transferred;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExtendedInfo = (props: { events: Registry<TransferInfoEvents> }) => {
|
||||||
|
const [expended, setExpended] = useState(false);
|
||||||
|
const [finishedShown, setFinishedShown] = useState(true);
|
||||||
|
|
||||||
|
props.events.reactUse("action_toggle_expansion", event => setExpended(event.visible));
|
||||||
|
props.events.reactUse("action_toggle_finished_transfers", event => setFinishedShown(event.visible));
|
||||||
|
props.events.reactUse("query_transfer_result", event => event.status === "success" && setFinishedShown(event.showFinished));
|
||||||
|
|
||||||
|
return <div className={cssStyle.expendedContainer + (expended ? "" : " " + cssStyle.hidden)} >
|
||||||
|
<div className={cssStyle.header}>
|
||||||
|
<a>{finishedShown ? <Translatable key={"file-transfers"}>File transfers</Translatable> : <Translatable key={"running-file-transfers"}>Running file transfers</Translatable>}</a>
|
||||||
|
<Button disabled={!finishedShown} color={"blue"} onClick={() => props.events.fire("action_remove_finished")}><Translatable>Remove finished</Translatable></Button>
|
||||||
|
</div>
|
||||||
|
<TransferList events={props.events} />
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TransferInfo = (props: { events: Registry<TransferInfoEvents> }) => (
|
||||||
|
<div className={cssStyle.container} >
|
||||||
|
<ExtendedInfo events={props.events} />
|
||||||
|
<BottomBar events={props.events} />
|
||||||
|
</div>
|
||||||
|
);
|
|
@ -0,0 +1,142 @@
|
||||||
|
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||||
|
import {Registry} from "tc-shared/events";
|
||||||
|
import {
|
||||||
|
FileTransfer,
|
||||||
|
FileTransferDirection,
|
||||||
|
FileTransferState,
|
||||||
|
TransferProgress,
|
||||||
|
TransferProperties
|
||||||
|
} from "tc-shared/file/Transfer";
|
||||||
|
import {
|
||||||
|
avatarsPathPrefix,
|
||||||
|
channelPathPrefix,
|
||||||
|
iconPathPrefix,
|
||||||
|
TransferStatus
|
||||||
|
} from "tc-shared/ui/modal/transfer/ModalFileTransfer";
|
||||||
|
import {Settings, settings} from "tc-shared/settings";
|
||||||
|
import {TransferInfoData, TransferInfoEvents} from "tc-shared/ui/modal/transfer/TransferInfo";
|
||||||
|
|
||||||
|
export const initializeTransferInfoController = (connection: ConnectionHandler, events: Registry<TransferInfoEvents>) => {
|
||||||
|
const generateTransferPath = (properties: TransferProperties) => {
|
||||||
|
let path;
|
||||||
|
if(properties.channel_id !== 0) {
|
||||||
|
path = "/" + channelPathPrefix + properties.channel_id + properties.path;
|
||||||
|
} else if(properties.name.startsWith("/avatar_")) {
|
||||||
|
path = "/" + avatarsPathPrefix + "/";
|
||||||
|
} else {
|
||||||
|
path = "/" + iconPathPrefix + "/";
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTransferStatus = (transfer: FileTransfer) : TransferStatus => {
|
||||||
|
switch (transfer.transferState()) {
|
||||||
|
case FileTransferState.INITIALIZING:
|
||||||
|
case FileTransferState.PENDING:
|
||||||
|
case FileTransferState.CONNECTING:
|
||||||
|
return "pending";
|
||||||
|
case FileTransferState.RUNNING:
|
||||||
|
return "transferring";
|
||||||
|
case FileTransferState.FINISHED:
|
||||||
|
case FileTransferState.CANCELED:
|
||||||
|
return "finished";
|
||||||
|
case FileTransferState.ERRORED:
|
||||||
|
return "errored";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateTransferInfo = (transfer: FileTransfer): TransferInfoData => {
|
||||||
|
return {
|
||||||
|
id: transfer.clientTransferId,
|
||||||
|
direction: transfer.direction === FileTransferDirection.UPLOAD ? "upload" : "download",
|
||||||
|
progress: 0,
|
||||||
|
name: transfer.properties.name,
|
||||||
|
path: generateTransferPath(transfer.properties),
|
||||||
|
status: getTransferStatus(transfer),
|
||||||
|
error: transfer.currentErrorMessage(),
|
||||||
|
timestampRegistered: transfer.timings.timestampScheduled,
|
||||||
|
timestampBegin: transfer.timings.timestampTransferBegin,
|
||||||
|
timestampEnd: transfer.timings.timestampEnd,
|
||||||
|
transferredBytes: transfer.lastProgressInfo() ? transfer.lastProgressInfo().file_current_offset - transfer.lastProgressInfo().file_start_offset : 0
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
events.on("action_toggle_finished_transfers", event => {
|
||||||
|
settings.changeGlobal(Settings.KEY_TRANSFERS_SHOW_FINISHED, event.visible);
|
||||||
|
});
|
||||||
|
|
||||||
|
events.on("action_remove_finished", () => {
|
||||||
|
connection.fileManager.finishedTransfers.splice(0, connection.fileManager.finishedTransfers.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
events.on("query_transfers", () => {
|
||||||
|
const transfers: TransferInfoData[] = connection.fileManager.registeredTransfers().map(generateTransferInfo);
|
||||||
|
transfers.push(...connection.fileManager.finishedTransfers.map(e => {
|
||||||
|
return {
|
||||||
|
id: e.clientTransferId,
|
||||||
|
direction: e.direction === FileTransferDirection.UPLOAD ? "upload" : "download",
|
||||||
|
progress: 100,
|
||||||
|
name: e.properties.name,
|
||||||
|
path: generateTransferPath(e.properties),
|
||||||
|
|
||||||
|
status: e.state === FileTransferState.FINISHED ? "finished" : "errored",
|
||||||
|
error: e.transferErrorMessage,
|
||||||
|
|
||||||
|
timestampRegistered: e.timings.timestampScheduled,
|
||||||
|
timestampBegin: e.timings.timestampTransferBegin,
|
||||||
|
timestampEnd: e.timings.timestampEnd,
|
||||||
|
|
||||||
|
transferredBytes: e.bytesTransferred
|
||||||
|
} as TransferInfoData;
|
||||||
|
}));
|
||||||
|
|
||||||
|
events.fire_async("query_transfer_result", {
|
||||||
|
status: "success",
|
||||||
|
transfers: transfers,
|
||||||
|
showFinished: settings.global(Settings.KEY_TRANSFERS_SHOW_FINISHED)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* the active transfer listener */
|
||||||
|
{
|
||||||
|
const listenToTransfer = (transfer: FileTransfer) => {
|
||||||
|
const fireProgress = (progress: TransferProgress) => events.fire("notify_transfer_progress", {
|
||||||
|
id: transfer.clientTransferId,
|
||||||
|
progress: progress,
|
||||||
|
status: "transferring",
|
||||||
|
});
|
||||||
|
|
||||||
|
const progressListener = (event: {progress: TransferProgress}) => fireProgress(event.progress);
|
||||||
|
|
||||||
|
transfer.events.on("notify_progress", progressListener);
|
||||||
|
|
||||||
|
transfer.events.on("notify_state_updated", () => {
|
||||||
|
const status = getTransferStatus(transfer);
|
||||||
|
if(transfer.lastProgressInfo()) fireProgress(transfer.lastProgressInfo()); /* fire the progress info at least once */
|
||||||
|
events.fire("notify_transfer_status", { id: transfer.clientTransferId, status: status, error: transfer.currentErrorMessage() });
|
||||||
|
|
||||||
|
if(transfer.isFinished()) {
|
||||||
|
unregisterEvents();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
events.fire("notify_transfer_registered", { transfer: generateTransferInfo(transfer) });
|
||||||
|
|
||||||
|
const closeListener = () => unregisterEvents();
|
||||||
|
events.on("notify_modal_closed", closeListener);
|
||||||
|
|
||||||
|
const unregisterEvents = () => {
|
||||||
|
events.off("notify_modal_closed", closeListener);
|
||||||
|
transfer.events.off("notify_progress", progressListener);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
connection.fileManager.registeredTransfers().forEach(transfer => listenToTransfer(transfer));
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
width="898.3px" height="898.3px" viewBox="0 0 898.3 898.3" style="enable-background:new 0 0 898.3 898.3;" xml:space="preserve"
|
||||||
|
>
|
||||||
|
<g>
|
||||||
|
<polygon points="120.2,882.5 553.6,449.2 120.2,15.8 0,136 313.2,449.2 0,762.3"/>
|
||||||
|
<polygon points="344.7,762.3 464.9,882.5 898.3,449.2 464.9,15.8 344.7,136 657.9,449.2"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 584 B |
|
@ -0,0 +1,10 @@
|
||||||
|
<svg width="512" height="512" enable-background="new 0 0 24 24" version="1.1" viewBox="0 0 24 24" fill="#7289da" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g transform="rotate(180 12 4.313)">
|
||||||
|
<path d="m14.25 3.75c-.192 0-.384-.073-.53-.22l-1.72-1.719-1.72 1.72c-.293.293-.768.293-1.061 0s-.293-.768 0-1.061l2.25-2.25c.293-.293.768-.293 1.061 0l2.25 2.25c.293.293.293.768 0 1.061-.146.146-.338.219-.53.219z"/>
|
||||||
|
</g>
|
||||||
|
<path d="m15.25 11h-6.5c-.965 0-1.75-.785-1.75-1.75v-1.5c0-.414.336-.75.75-.75s.75.336.75.75v1.5c0 .138.112.25.25.25h6.5c.138 0 .25-.112.25-.25v-1.5c0-.414.336-.75.75-.75s.75.336.75.75v1.5c0 .965-.785 1.75-1.75 1.75z"/>
|
||||||
|
<path d="m12 8c-.414 0-.75-.336-.75-.75v-6.25c0-.414.336-.75.75-.75s.75.336.75.75v6.25c0 .414-.336.75-.75.75z"/>
|
||||||
|
<path d="m22.25 21h-20.5c-.965 0-1.75-.785-1.75-1.75v-13.5c0-.965.785-1.75 1.75-1.75h3.5c.415 0 .75.336.75.75s-.335.75-.75.75h-3.5c-.138 0-.25.112-.25.25v13.5c0 .138.112.25.25.25h20.5c.138 0 .25-.112.25-.25v-13.5c0-.138-.112-.25-.25-.25h-3.5c-.414 0-.75-.336-.75-.75s.336-.75.75-.75h3.5c.965 0 1.75.785 1.75 1.75v13.5c0 .965-.785 1.75-1.75 1.75z"/>
|
||||||
|
<path d="m23.25 17.5h-22.5c-.414 0-.75-.336-.75-.75s.336-.75.75-.75h22.5c.414 0 .75.336.75.75s-.336.75-.75.75z"/>
|
||||||
|
<path d="m15.25 24h-6.5c-.303 0-.577-.183-.693-.463s-.052-.602.163-.817c1.041-1.041 1.265-2.553 1.267-2.567.054-.411.432-.704.841-.646.411.054.7.431.646.841-.008.059-.147 1.066-.744 2.152h3.523c-.591-1.08-.738-2.088-.746-2.147-.057-.41.229-.788.64-.846.414-.058.788.229.846.639.007.045.232 1.469 1.184 2.487.195.136.323.361.323.617 0 .414-.336.75-.75.75z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,21 @@
|
||||||
|
<svg enable-background="new 0 0 24 24" height="512" viewBox="0 0 24 24" width="512" fill="#7289da"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g>
|
||||||
|
<path d="m15.25 11h-6.5c-.965 0-1.75-.785-1.75-1.75v-1.5c0-.414.336-.75.75-.75s.75.336.75.75v1.5c0 .138.112.25.25.25h6.5c.138 0 .25-.112.25-.25v-1.5c0-.414.336-.75.75-.75s.75.336.75.75v1.5c0 .965-.785 1.75-1.75 1.75z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path d="m12 8c-.414 0-.75-.336-.75-.75v-6.25c0-.414.336-.75.75-.75s.75.336.75.75v6.25c0 .414-.336.75-.75.75z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path d="m14.25 3.75c-.192 0-.384-.073-.53-.22l-1.72-1.719-1.72 1.72c-.293.293-.768.293-1.061 0s-.293-.768 0-1.061l2.25-2.25c.293-.293.768-.293 1.061 0l2.25 2.25c.293.293.293.768 0 1.061-.146.146-.338.219-.53.219z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path d="m22.25 21h-20.5c-.965 0-1.75-.785-1.75-1.75v-13.5c0-.965.785-1.75 1.75-1.75h3.5c.415 0 .75.336.75.75s-.335.75-.75.75h-3.5c-.138 0-.25.112-.25.25v13.5c0 .138.112.25.25.25h20.5c.138 0 .25-.112.25-.25v-13.5c0-.138-.112-.25-.25-.25h-3.5c-.414 0-.75-.336-.75-.75s.336-.75.75-.75h3.5c.965 0 1.75.785 1.75 1.75v13.5c0 .965-.785 1.75-1.75 1.75z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path d="m23.25 17.5h-22.5c-.414 0-.75-.336-.75-.75s.336-.75.75-.75h22.5c.414 0 .75.336.75.75s-.336.75-.75.75z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path d="m15.25 24h-6.5c-.303 0-.577-.183-.693-.463s-.052-.602.163-.817c1.041-1.041 1.265-2.553 1.267-2.567.054-.411.432-.704.841-.646.411.054.7.431.646.841-.008.059-.147 1.066-.744 2.152h3.523c-.591-1.08-.738-2.088-.746-2.147-.057-.41.229-.788.64-.846.414-.058.788.229.846.639.007.045.232 1.469 1.184 2.487.195.136.323.361.323.617 0 .414-.336.75-.75.75z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -1,8 +1,9 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import {ReactElement} from "react";
|
||||||
const cssStyle = require("./Checkbox.scss");
|
const cssStyle = require("./Checkbox.scss");
|
||||||
|
|
||||||
export interface CheckboxProperties {
|
export interface CheckboxProperties {
|
||||||
label?: string;
|
label?: ReactElement | string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onChange?: (value: boolean) => void;
|
onChange?: (value: boolean) => void;
|
||||||
initialValue?: boolean;
|
initialValue?: boolean;
|
||||||
|
@ -11,7 +12,8 @@ export interface CheckboxProperties {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CheckboxState {
|
export interface CheckboxState {
|
||||||
checked: boolean;
|
checked?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Checkbox extends React.Component<CheckboxProperties, CheckboxState> {
|
export class Checkbox extends React.Component<CheckboxProperties, CheckboxState> {
|
||||||
|
@ -19,17 +21,20 @@ export class Checkbox extends React.Component<CheckboxProperties, CheckboxState>
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
checked: this.props.initialValue
|
checked: this.props.initialValue,
|
||||||
|
disabled: this.props.disabled
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const disabledClass = this.props.disabled ? cssStyle.disabled : "";
|
const disabled = typeof this.state.disabled === "boolean" ? this.state.disabled : this.props.disabled;
|
||||||
|
const checked = typeof this.state.checked === "boolean" ? this.state.checked : this.props.initialValue;
|
||||||
|
const disabledClass = disabled ? cssStyle.disabled : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label className={cssStyle.labelCheckbox + " " + disabledClass}>
|
<label className={cssStyle.labelCheckbox + " " + disabledClass}>
|
||||||
<div className={cssStyle.checkbox + " " + disabledClass}>
|
<div className={cssStyle.checkbox + " " + disabledClass}>
|
||||||
<input type={"checkbox"} checked={this.state.checked} disabled={this.props.disabled} onChange={() => this.onStateChange()} />
|
<input type={"checkbox"} checked={checked} disabled={disabled} onChange={() => this.onStateChange()} />
|
||||||
<div className={cssStyle.mark} />
|
<div className={cssStyle.mark} />
|
||||||
</div>
|
</div>
|
||||||
{this.props.label ? <a>{this.props.label}</a> : undefined}
|
{this.props.label ? <a>{this.props.label}</a> : undefined}
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as purify from "dompurify";
|
||||||
|
|
||||||
|
/*
|
||||||
|
export const HTMLRenderer = (props: { purify: boolean, children: string }) => {
|
||||||
|
const html = props.purify ? purify.sanitize(props.children) : props.children;
|
||||||
|
|
||||||
|
return <span dangerouslySetInnerHTML={{ __html: html }} />
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class HTMLRenderer extends React.PureComponent<{ purify: boolean, children: string }, {}> {
|
||||||
|
private readonly reference = React.createRef<HTMLSpanElement>();
|
||||||
|
private readonly newNodes: Element[];
|
||||||
|
private originalNode: HTMLSpanElement;
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
const html = this.props.purify ? purify.sanitize(this.props.children) : this.props.children;
|
||||||
|
const node = document.createElement("div");
|
||||||
|
node.innerHTML = html;
|
||||||
|
|
||||||
|
this.newNodes = [...node.children];
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if(this.newNodes.length === 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return <span ref={this.reference} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount(): void {
|
||||||
|
if(this.newNodes.length === 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.originalNode = this.reference.current;
|
||||||
|
|
||||||
|
this.originalNode.replaceWith(this.newNodes[0]);
|
||||||
|
this.newNodes.forEach((node, index, array) => {
|
||||||
|
if(index === 0) return;
|
||||||
|
|
||||||
|
node.after(array[index - 1]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount(): void {
|
||||||
|
if(this.newNodes.length === 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.newNodes.forEach((node, index) => {
|
||||||
|
if(index === 0) return;
|
||||||
|
|
||||||
|
node.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.newNodes[0].replaceWith(this.originalNode);
|
||||||
|
}
|
||||||
|
}
|
|
@ -46,7 +46,7 @@ export class LocalIconRenderer extends React.Component<LoadedIconRenderer, {}> {
|
||||||
return <div className={"icon-container icon-empty"} title={this.props.title} />;
|
return <div className={"icon-container icon-empty"} title={this.props.title} />;
|
||||||
return <div className={"icon_em client-group_" + icon.icon_id} />;
|
return <div className={"icon_em client-group_" + icon.icon_id} />;
|
||||||
}
|
}
|
||||||
return <div key={"icon"} className={"icon-container"}><img src={icon.loaded_url} alt={this.props.title || ("icon " + icon.icon_id)} /></div>;
|
return <div key={"icon"} className={"icon-container"}><img style={{ maxWidth: "100%", maxHeight: "100%" }} src={icon.loaded_url} alt={this.props.title || ("icon " + icon.icon_id)} /></div>;
|
||||||
} else if(icon.status === "loading")
|
} else if(icon.status === "loading")
|
||||||
return <div key={"loading"} className={"icon-container"} title={this.props.title}><div className={"icon_loading"} /></div>;
|
return <div key={"loading"} className={"icon-container"} title={this.props.title}><div className={"icon_loading"} /></div>;
|
||||||
else if(icon.status === "error")
|
else if(icon.status === "error")
|
||||||
|
|
|
@ -0,0 +1,125 @@
|
||||||
|
@import "../../../css/static/mixin";
|
||||||
|
@import "../../../css/static/properties";
|
||||||
|
|
||||||
|
.container {
|
||||||
|
border-radius: .2em;
|
||||||
|
border: 1px solid #111112;
|
||||||
|
|
||||||
|
background-color: #121213;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
color: #b3b3b3;
|
||||||
|
|
||||||
|
&.size-normal {
|
||||||
|
height: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.size-large {
|
||||||
|
height: 2.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.size-small {
|
||||||
|
height: 1.7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include placeholder(&) {
|
||||||
|
color: #606060;
|
||||||
|
};
|
||||||
|
|
||||||
|
.prefix {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
line-height: initial;
|
||||||
|
align-self: center;
|
||||||
|
padding: 0 .5em;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
@include transition($button_hover_animation_time ease-in-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-invalid {
|
||||||
|
background-color: #180d0d;
|
||||||
|
border-color: #721c1c;
|
||||||
|
|
||||||
|
background-image: unset!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus, &:focus-within {
|
||||||
|
background-color: #131b22;
|
||||||
|
border-color: #284262;
|
||||||
|
|
||||||
|
color: #e1e2e3;
|
||||||
|
|
||||||
|
.prefix {
|
||||||
|
width: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input, select, .inputBox {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
color: #b3b3b3;
|
||||||
|
|
||||||
|
min-width: 2em;
|
||||||
|
|
||||||
|
&.editable {
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputBox {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prefix + input {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
&:focus, &:focus-within {
|
||||||
|
.prefix + input {
|
||||||
|
padding-left: .5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled, &:disabled {
|
||||||
|
background-color: #1a1819;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.noRightIcon {
|
||||||
|
input, select {
|
||||||
|
padding-right: .5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.noLeftIcon {
|
||||||
|
input, select {
|
||||||
|
padding-left: .5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include transition($button_hover_animation_time ease-in-out);
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import {ReactElement} from "react";
|
||||||
|
|
||||||
|
const cssStyle = require("./InputField.scss");
|
||||||
|
|
||||||
|
export interface BoxedInputFieldProperties {
|
||||||
|
prefix?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
|
||||||
|
disabled?: boolean;
|
||||||
|
editable?: boolean;
|
||||||
|
|
||||||
|
defaultValue?: string;
|
||||||
|
|
||||||
|
rightIcon?: () => ReactElement;
|
||||||
|
leftIcon?: () => ReactElement;
|
||||||
|
inputBox?: () => ReactElement; /* if set the onChange and onInput will not work anymore! */
|
||||||
|
|
||||||
|
isInvalid?: boolean;
|
||||||
|
|
||||||
|
className?: string;
|
||||||
|
|
||||||
|
size?: "normal" | "large" | "small";
|
||||||
|
|
||||||
|
onFocus?: () => void;
|
||||||
|
onBlur?: () => void;
|
||||||
|
|
||||||
|
onChange?: (newValue: string) => void;
|
||||||
|
onInput?: (newValue: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BoxedInputFieldState {
|
||||||
|
disabled?: boolean;
|
||||||
|
defaultValue?: string;
|
||||||
|
isInvalid?: boolean;
|
||||||
|
value?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BoxedInputField extends React.Component<BoxedInputFieldProperties, BoxedInputFieldState> {
|
||||||
|
private refInput = React.createRef<HTMLInputElement>();
|
||||||
|
private inputEdited = false;
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
draggable={false}
|
||||||
|
className={
|
||||||
|
cssStyle.container + " " +
|
||||||
|
cssStyle["size-" + (this.props.size || "normal")] +
|
||||||
|
(this.state.disabled || this.props.disabled ? cssStyle.disabled : "") + " " +
|
||||||
|
(this.state.isInvalid || this.props.isInvalid ? cssStyle.isInvalid : "") + " " +
|
||||||
|
(this.props.leftIcon ? "" : cssStyle.noLeftIcon) + " " +
|
||||||
|
(this.props.rightIcon ? "" : cssStyle.noRightIcon) + " " +
|
||||||
|
this.props.className
|
||||||
|
}
|
||||||
|
|
||||||
|
onFocus={this.props.onFocus}
|
||||||
|
onBlur={() => this.onInputBlur()}
|
||||||
|
>
|
||||||
|
{this.props.leftIcon ? this.props.leftIcon() : ""}
|
||||||
|
{this.props.prefix ? <a key={"prefix"} className={cssStyle.prefix}>{this.props.prefix}</a> : undefined}
|
||||||
|
{this.props.inputBox ?
|
||||||
|
<span key={"custom-input"} className={cssStyle.inputBox + " " + (this.props.editable ? cssStyle.editable : "")} onClick={this.props.onFocus}>{this.props.inputBox()}</span> :
|
||||||
|
<input key={"input"}
|
||||||
|
ref={this.refInput}
|
||||||
|
value={this.state.value}
|
||||||
|
defaultValue={this.state.defaultValue || this.props.defaultValue}
|
||||||
|
placeholder={this.props.placeholder}
|
||||||
|
readOnly={typeof this.props.editable === "boolean" ? this.props.editable : false}
|
||||||
|
disabled={this.state.disabled || this.props.disabled}
|
||||||
|
onInput={this.props.onInput && (event => this.props.onInput(event.currentTarget.value))}
|
||||||
|
onKeyDown={e => this.onKeyDown(e)}
|
||||||
|
/>}
|
||||||
|
{this.props.rightIcon ? this.props.rightIcon() : ""}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
focusInput() {
|
||||||
|
this.refInput.current?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private onKeyDown(event: React.KeyboardEvent) {
|
||||||
|
this.inputEdited = true;
|
||||||
|
|
||||||
|
if(event.key === "Enter")
|
||||||
|
this.refInput.current?.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
private onInputBlur() {
|
||||||
|
if(this.props.onChange && this.inputEdited) {
|
||||||
|
this.inputEdited = false;
|
||||||
|
this.props.onChange(this.refInput.current.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.props.onBlur)
|
||||||
|
this.props.onBlur();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export const LoadingDots = (props: { maxDots?: number, speed?: number }) => {
|
||||||
|
if(!props.maxDots || props.maxDots < 1)
|
||||||
|
props.maxDots = 3;
|
||||||
|
|
||||||
|
const [dots, setDots] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timeout = setTimeout(() => setDots(dots + 1), props.speed || 500);
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = ".";
|
||||||
|
for(let index = 0; index < dots % props.maxDots; index++)
|
||||||
|
result += ".";
|
||||||
|
return <div style={{ width: (props.maxDots / 3) + "em", display: "inline-block", textAlign: "left" }}>{result}</div>;
|
||||||
|
};
|
|
@ -21,9 +21,9 @@ export enum ModalState {
|
||||||
DESTROYED
|
DESTROYED
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ModalController {
|
export class ModalController<InstanceType extends Modal = Modal> {
|
||||||
readonly events: Registry<ModalEvents>;
|
readonly events: Registry<ModalEvents>;
|
||||||
readonly modalInstance: Modal;
|
readonly modalInstance: InstanceType;
|
||||||
|
|
||||||
private initializedPromise: Promise<void>;
|
private initializedPromise: Promise<void>;
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ export class ModalController {
|
||||||
private refModal: React.RefObject<ModalImpl>;
|
private refModal: React.RefObject<ModalImpl>;
|
||||||
private modalState_: ModalState = ModalState.HIDDEN;
|
private modalState_: ModalState = ModalState.HIDDEN;
|
||||||
|
|
||||||
constructor(instance: Modal) {
|
constructor(instance: InstanceType) {
|
||||||
this.modalInstance = instance;
|
this.modalInstance = instance;
|
||||||
instance["__modal_controller"] = this;
|
instance["__modal_controller"] = this;
|
||||||
|
|
||||||
|
@ -170,6 +170,6 @@ class ModalImpl extends React.PureComponent<{ controller: ModalController }, {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function spawnReactModal<ModalClass extends Modal>(modalClass: new () => ModalClass) : ModalController {
|
export function spawnReactModal<ModalClass extends Modal, T>(modalClass: new (T) => ModalClass, properties?: T) : ModalController<ModalClass> {
|
||||||
return new ModalController(new modalClass());
|
return new ModalController(new modalClass(properties));
|
||||||
}
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
@import "../../../css/static/mixin";
|
||||||
|
@import "../../../css/static/properties";
|
||||||
|
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
height: 1.4em;
|
||||||
|
border-radius: 0.2em;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
background-color: #242527;
|
||||||
|
-webkit-box-shadow: inset 0 0 2px 0 rgba(0, 0, 0, 0.75);
|
||||||
|
-moz-box-shadow: inset 0 0 2px 0 rgba(0, 0, 0, 0.75);
|
||||||
|
box-shadow: inset 0 0 2px 0 rgba(0, 0, 0, 0.75);
|
||||||
|
|
||||||
|
.filler {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
@include transition($button_hover_animation_time ease-in-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
align-self: center;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.type-normal {
|
||||||
|
.filler {
|
||||||
|
background-color: #4370a299;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.type-error {
|
||||||
|
.filler {
|
||||||
|
background-color: #a1000099;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.type-success {
|
||||||
|
.filler {
|
||||||
|
background-color: #2b854199;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import {ReactElement} from "react";
|
||||||
|
const cssStyle = require("./ProgressBar.scss");
|
||||||
|
|
||||||
|
export interface ProgressBarState {
|
||||||
|
value?: number; /* [0;100] */
|
||||||
|
text?: ReactElement | string;
|
||||||
|
type?: "normal" | "error" | "success";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProgressBarProperties {
|
||||||
|
value: number; /* [0;100] */
|
||||||
|
text?: ReactElement | string;
|
||||||
|
type: "normal" | "error" | "success";
|
||||||
|
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ProgressBar extends React.Component<ProgressBarProperties, ProgressBarState> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.container + " " + cssStyle["type-" + (typeof this.state.type === "undefined" ? this.props.type : this.state.type)] + " " + (this.props.className || "")}>
|
||||||
|
<div className={cssStyle.filler} style={{width: (typeof this.state.value === "number" ? this.state.value : this.props.value) + "%"}} />
|
||||||
|
<div className={cssStyle.text}>
|
||||||
|
{typeof this.state.text !== "undefined" ? this.state.text : this.props.text}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
min-height: 5em;
|
||||||
|
|
||||||
|
.dynamicColumn {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
min-width: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fixedColumn {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
min-height: 3em;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
|
||||||
|
.headerSpacer {
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,196 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import {ReactElement} from "react";
|
||||||
|
|
||||||
|
const cssStyle = require("./Table.scss");
|
||||||
|
|
||||||
|
export interface TableColumn {
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
header: () => ReactElement | ReactElement[];
|
||||||
|
width?: number;
|
||||||
|
fixedWidth?: string;
|
||||||
|
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TableRow<T = any> {
|
||||||
|
columns: {[key: string]: () => ReactElement | ReactElement[]};
|
||||||
|
|
||||||
|
className?: string;
|
||||||
|
userData?: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TableProperties {
|
||||||
|
columns: TableColumn[];
|
||||||
|
rows: TableRow[];
|
||||||
|
|
||||||
|
className?: string;
|
||||||
|
headerClassName?: string;
|
||||||
|
bodyClassName?: string;
|
||||||
|
|
||||||
|
bodyOverlayOnly?: boolean;
|
||||||
|
bodyOverlay?: () => ReactElement;
|
||||||
|
|
||||||
|
hiddenColumns?: string[];
|
||||||
|
|
||||||
|
onHeaderContextMenu?: (event: React.MouseEvent) => void;
|
||||||
|
onBodyContextMenu?: (event: React.MouseEvent) => void;
|
||||||
|
|
||||||
|
renderRow?: (row: TableRow, columns: TableColumn[], uniqueId: string) => React.ReactElement<TableRowElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TableState {
|
||||||
|
hiddenColumns: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TableRowProperties {
|
||||||
|
columns: TableColumn[];
|
||||||
|
rowData: TableRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TableRowState {
|
||||||
|
hidden?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TableRowElement extends React.Component<TableRowProperties & React.HTMLProps<HTMLDivElement>, TableRowState> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if(this.state.hidden)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
let totalWidth = this.props.columns.map(e => e.width | 0).reduce((a, b) => a + b, 0);
|
||||||
|
if(totalWidth === 0)
|
||||||
|
totalWidth = 1;
|
||||||
|
|
||||||
|
const properties = Object.assign({}, this.props) as any;
|
||||||
|
delete properties.rowData;
|
||||||
|
delete properties.columns;
|
||||||
|
properties.className = (properties.className || "") + " " + cssStyle.row;
|
||||||
|
|
||||||
|
const children = Array.isArray(this.props.children) ? this.props.children : typeof this.props.children !== "undefined" ? [this.props.children] : [];
|
||||||
|
return React.createElement("div", properties, ...this.props.columns.map(column => {
|
||||||
|
const supplier = this.props.rowData.columns[column.name];
|
||||||
|
if(column.width) {
|
||||||
|
return (
|
||||||
|
<div key={"tr-" + column.name}
|
||||||
|
className={cssStyle.dynamicColumn + " " + (column.className || "")}
|
||||||
|
style={{width: (column.width * 100 / totalWidth) + "%"}}>
|
||||||
|
{supplier ? supplier() : undefined}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if(column.fixedWidth) {
|
||||||
|
return (
|
||||||
|
<div key={"th-" + column.name}
|
||||||
|
className={cssStyle.fixedColumn + " " + (column.className || "")}
|
||||||
|
style={{width: column.fixedWidth}}>
|
||||||
|
{supplier ? supplier() : undefined}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}), ...children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Table extends React.Component<TableProperties, TableState> {
|
||||||
|
private rowIndex = 0;
|
||||||
|
|
||||||
|
private refHeader = React.createRef<HTMLDivElement>();
|
||||||
|
private refHiddenHeader = React.createRef<HTMLDivElement>();
|
||||||
|
private refBody = React.createRef<HTMLDivElement>();
|
||||||
|
|
||||||
|
private lastHeaderHeight = 20;
|
||||||
|
private lastScrollbarWidth = 20;
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
hiddenColumns: this.props.hiddenColumns || []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const columns = this.props.columns.filter(e => this.state.hiddenColumns.findIndex(b => e.name === b) === -1);
|
||||||
|
let totalWidth = columns.map(e => e.width | 0).reduce((a, b) => a + b, 0);
|
||||||
|
if(totalWidth === 0)
|
||||||
|
totalWidth = 1;
|
||||||
|
|
||||||
|
const rowRenderer = this.props.renderRow || ((row, columns, uniqueId) => {
|
||||||
|
return <TableRowElement key={uniqueId} rowData={row} columns={columns} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
let body;
|
||||||
|
if(this.props.bodyOverlayOnly) {
|
||||||
|
body = this.props.bodyOverlay ? this.props.bodyOverlay() : undefined;
|
||||||
|
} else {
|
||||||
|
body = this.props.rows.map((row: TableRow & { __rowIndex: number }) => {
|
||||||
|
if(typeof row.__rowIndex !== "number")
|
||||||
|
row.__rowIndex = ++this.rowIndex;
|
||||||
|
|
||||||
|
return rowRenderer(row, columns, "tr-" + row.__rowIndex);
|
||||||
|
});
|
||||||
|
|
||||||
|
if(this.props.bodyOverlay)
|
||||||
|
body.push(this.props.bodyOverlay());
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.container + " " + (this.props.className || " ")}>
|
||||||
|
<div
|
||||||
|
ref={this.refHeader}
|
||||||
|
className={cssStyle.header + " " + (this.props.headerClassName || " ")}
|
||||||
|
style={{right: this.lastScrollbarWidth}}
|
||||||
|
onContextMenu={event => this.props.onHeaderContextMenu && this.props.onHeaderContextMenu(event)}
|
||||||
|
>
|
||||||
|
{columns.map(column => {
|
||||||
|
if(column.width) {
|
||||||
|
return (
|
||||||
|
<div key={"th-" + column.name}
|
||||||
|
className={cssStyle.dynamicColumn + " " + (column.className || "")}
|
||||||
|
style={{width: (column.width * 100 / totalWidth) + "%"}}
|
||||||
|
>
|
||||||
|
{column.header()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if(column.fixedWidth) {
|
||||||
|
return (
|
||||||
|
<div key={"th-" + column.name}
|
||||||
|
className={cssStyle.fixedColumn + " " + (column.className || "")}
|
||||||
|
style={{width: column.fixedWidth}}
|
||||||
|
>
|
||||||
|
{column.header()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={cssStyle.body + " " + (this.props.bodyClassName || " ")}
|
||||||
|
ref={this.refBody}
|
||||||
|
onContextMenu={e => this.props.onBodyContextMenu && this.props.onBodyContextMenu(e)}
|
||||||
|
>
|
||||||
|
<div ref={this.refHiddenHeader} style={{height: this.lastHeaderHeight}} className={cssStyle.row} />
|
||||||
|
{body}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: Readonly<TableProperties>, prevState: Readonly<TableState>, snapshot?: any): void {
|
||||||
|
if(!this.refHiddenHeader.current || !this.refHeader.current || !this.refBody.current)
|
||||||
|
return;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.lastHeaderHeight = this.refHeader.current.clientHeight;
|
||||||
|
this.lastScrollbarWidth = this.refBody.current.parentElement.clientWidth - this.refBody.current.clientWidth;
|
||||||
|
|
||||||
|
this.refHiddenHeader.current.style.height = this.lastHeaderHeight + "px";
|
||||||
|
this.refHeader.current.style.right = this.lastScrollbarWidth + "px";
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import {LocalIconRenderer} from "tc-shared/ui/react-elements/Icon";
|
||||||
import {EventHandler, ReactEventHandler} from "tc-shared/events";
|
import {EventHandler, ReactEventHandler} from "tc-shared/events";
|
||||||
import {Settings, settings} from "tc-shared/settings";
|
import {Settings, settings} from "tc-shared/settings";
|
||||||
import {TreeEntry, UnreadMarker} from "tc-shared/ui/tree/TreeEntry";
|
import {TreeEntry, UnreadMarker} from "tc-shared/ui/tree/TreeEntry";
|
||||||
|
import {spawnFileTransferModal} from "tc-shared/ui/modal/transfer/ModalFileTransfer";
|
||||||
|
|
||||||
const channelStyle = require("./Channel.scss");
|
const channelStyle = require("./Channel.scss");
|
||||||
const viewStyle = require("./View.scss");
|
const viewStyle = require("./View.scss");
|
||||||
|
@ -247,6 +248,7 @@ export class ChannelEntryView extends TreeEntry<ChannelEntryViewProperties, {}>
|
||||||
onMouseUp={e => this.onMouseUp(e)}
|
onMouseUp={e => this.onMouseUp(e)}
|
||||||
onDoubleClick={() => this.onDoubleClick()}
|
onDoubleClick={() => this.onDoubleClick()}
|
||||||
onContextMenu={e => this.onContextMenu(e)}
|
onContextMenu={e => this.onContextMenu(e)}
|
||||||
|
onMouseDown={e => this.onMouseDown(e)}
|
||||||
>
|
>
|
||||||
<UnreadMarker entry={this.props.channel} />
|
<UnreadMarker entry={this.props.channel} />
|
||||||
{collapsed_indicator && <ChannelCollapsedIndicator key={"collapsed-indicator"} onToggle={() => this.onCollapsedToggle()} collapsed={this.props.channel.collapsed} />}
|
{collapsed_indicator && <ChannelCollapsedIndicator key={"collapsed-indicator"} onToggle={() => this.onCollapsedToggle()} collapsed={this.props.channel.collapsed} />}
|
||||||
|
@ -279,6 +281,13 @@ export class ChannelEntryView extends TreeEntry<ChannelEntryViewProperties, {}>
|
||||||
channel.joinChannel();
|
channel.joinChannel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onMouseDown(event: React.MouseEvent) {
|
||||||
|
if(event.buttons !== 4)
|
||||||
|
return;
|
||||||
|
|
||||||
|
spawnFileTransferModal(this.props.channel.getChannelId());
|
||||||
|
}
|
||||||
|
|
||||||
private onContextMenu(event: React.MouseEvent) {
|
private onContextMenu(event: React.MouseEvent) {
|
||||||
if(settings.static(Settings.KEY_DISABLE_CONTEXT_MENU))
|
if(settings.static(Settings.KEY_DISABLE_CONTEXT_MENU))
|
||||||
return;
|
return;
|
||||||
|
|
5413
shared/test.json
|
@ -0,0 +1,335 @@
|
||||||
|
import {
|
||||||
|
BrowserFileTransferSource,
|
||||||
|
BufferTransferSource,
|
||||||
|
DownloadTransferTarget,
|
||||||
|
FileDownloadTransfer,
|
||||||
|
FileTransfer,
|
||||||
|
FileTransferState,
|
||||||
|
FileUploadTransfer,
|
||||||
|
ResponseTransferTarget,
|
||||||
|
TextTransferSource,
|
||||||
|
TransferProvider,
|
||||||
|
TransferSourceType,
|
||||||
|
TransferTargetType
|
||||||
|
} from "tc-shared/file/Transfer";
|
||||||
|
import * as log from "tc-shared/log";
|
||||||
|
import {LogCategory} from "tc-shared/log";
|
||||||
|
|
||||||
|
TransferProvider.setProvider(new class extends TransferProvider {
|
||||||
|
executeFileUpload(transfer: FileUploadTransfer) {
|
||||||
|
try {
|
||||||
|
if(!transfer.source) throw tr("transfer source is undefined");
|
||||||
|
|
||||||
|
let response: Promise<void>;
|
||||||
|
transfer.setTransferState(FileTransferState.CONNECTING);
|
||||||
|
if(transfer.source instanceof BrowserFileTransferSourceImpl) {
|
||||||
|
response = formDataUpload(transfer, transfer.source.getFile());
|
||||||
|
} else if(transfer.source instanceof BufferTransferSourceImpl) {
|
||||||
|
response = formDataUpload(transfer, transfer.source.getBuffer());
|
||||||
|
} else if(transfer.source instanceof TextTransferSourceImpl) {
|
||||||
|
response = formDataUpload(transfer, transfer.source.getArrayBuffer());
|
||||||
|
} else {
|
||||||
|
transfer.setFailed({
|
||||||
|
error: "io",
|
||||||
|
reason: "unsupported-target"
|
||||||
|
}, tr("invalid source type"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* let the server notify us when the transfer has been finished */
|
||||||
|
response.catch(error => {
|
||||||
|
if(typeof error !== "string")
|
||||||
|
log.error(LogCategory.FILE_TRANSFER, tr("Failed to upload object via HTTPS connection: %o"), error);
|
||||||
|
|
||||||
|
transfer.setFailed({
|
||||||
|
error: "connection",
|
||||||
|
reason: "network-error",
|
||||||
|
extraMessage: typeof error === "string" ? error : tr("Lookup the console")
|
||||||
|
}, typeof error === "string" ? error : tr("Lookup the console"));
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if(typeof error !== "string")
|
||||||
|
log.error(LogCategory.FILE_TRANSFER, tr("Failed to initialize transfer source: %o"), error);
|
||||||
|
|
||||||
|
transfer.setFailed({
|
||||||
|
error: "io",
|
||||||
|
reason: "failed-to-initialize-target",
|
||||||
|
extraMessage: typeof error === "string" ? error : tr("Lookup the console")
|
||||||
|
}, typeof error === "string" ? error : tr("Lookup the console"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
executeFileDownload(transfer: FileDownloadTransfer) {
|
||||||
|
transfer.targetSupplier(transfer).then(target => {
|
||||||
|
if(!target) throw tr("transfer target is undefined");
|
||||||
|
transfer.target = target;
|
||||||
|
|
||||||
|
let response: Promise<void>;
|
||||||
|
transfer.setTransferState(FileTransferState.CONNECTING);
|
||||||
|
if(target instanceof ResponseTransferTargetImpl) {
|
||||||
|
response = responseFileDownload(transfer, target);
|
||||||
|
} else if(target instanceof DownloadTransferTargetImpl) {
|
||||||
|
response = downloadFileDownload(transfer, target);
|
||||||
|
} else {
|
||||||
|
transfer.setFailed({
|
||||||
|
error: "io",
|
||||||
|
reason: "unsupported-target"
|
||||||
|
}, tr("invalid transfer target type"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.then(() => {
|
||||||
|
if(!transfer.isFinished()) {
|
||||||
|
/* we still need to stream the body */
|
||||||
|
transfer.setTransferState(FileTransferState.RUNNING);
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
if(typeof error !== "string")
|
||||||
|
log.error(LogCategory.FILE_TRANSFER, tr("Failed to download file to response object: %o"), error);
|
||||||
|
|
||||||
|
transfer.setFailed({
|
||||||
|
error: "connection",
|
||||||
|
reason: "network-error",
|
||||||
|
extraMessage: typeof error === "string" ? error : tr("Lookup the console")
|
||||||
|
}, typeof error === "string" ? error : tr("Lookup the console"));
|
||||||
|
});
|
||||||
|
}).catch(error => {
|
||||||
|
if(typeof error !== "string")
|
||||||
|
log.error(LogCategory.FILE_TRANSFER, tr("Failed to initialize transfer target: %o"), error);
|
||||||
|
|
||||||
|
transfer.setFailed({
|
||||||
|
error: "io",
|
||||||
|
reason: "failed-to-initialize-target",
|
||||||
|
extraMessage: typeof error === "string" ? error : tr("Lookup the console")
|
||||||
|
}, typeof error === "string" ? error : tr("Lookup the console"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
targetSupported(type: TransferTargetType) {
|
||||||
|
switch (type) {
|
||||||
|
case TransferTargetType.DOWNLOAD:
|
||||||
|
case TransferTargetType.RESPONSE:
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createDownloadTarget(filename: string) {
|
||||||
|
return new DownloadTransferTargetImpl(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createResponseTarget() {
|
||||||
|
return new ResponseTransferTargetImpl();
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSupported(type: TransferSourceType) {
|
||||||
|
switch (type) {
|
||||||
|
case TransferSourceType.BROWSER_FILE:
|
||||||
|
case TransferSourceType.BUFFER:
|
||||||
|
case TransferSourceType.TEXT:
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createBufferSource(buffer: ArrayBuffer): Promise<BufferTransferSource> {
|
||||||
|
return new BufferTransferSourceImpl(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createBrowserFileSource(file: File): Promise<BrowserFileTransferSource> {
|
||||||
|
return new BrowserFileTransferSourceImpl(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createTextSource(text: string): Promise<TextTransferSource> {
|
||||||
|
return new TextTransferSourceImpl(text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function generateTransferURL(transfer: FileTransfer, fileName?: string) {
|
||||||
|
const properties = transfer.transferProperties();
|
||||||
|
const url = "https://" + properties.addresses[0].serverAddress + ":" + properties.addresses[0].serverPort + "/";
|
||||||
|
const parameters = {
|
||||||
|
"transfer-key": properties.transferKey
|
||||||
|
};
|
||||||
|
if(typeof fileName !== "undefined")
|
||||||
|
parameters["file-name"] = fileName;
|
||||||
|
const query = "?" + Object.keys(parameters).map(e => e + "=" + encodeURIComponent(parameters[e])).join("&");
|
||||||
|
return url + query;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function performHTTPSTransfer(transfer: FileTransfer, body: FormData | undefined) : Promise<Response> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(generateTransferURL(transfer), {
|
||||||
|
method: typeof body === "number" ? "GET" : "POST",
|
||||||
|
cache: "no-cache",
|
||||||
|
mode: "cors",
|
||||||
|
body: body,
|
||||||
|
headers: {
|
||||||
|
/* for legacy TeaSpeak servers (prior to 1.4.15) */
|
||||||
|
'transfer-key': transfer.transferProperties().transferKey,
|
||||||
|
'download-name': transfer.properties.name,
|
||||||
|
/* end legacy */
|
||||||
|
|
||||||
|
"Access-Control-Allow-Headers": "*",
|
||||||
|
"Access-Control-Expose-Headers": "*"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!response.ok) {
|
||||||
|
throw (response.type == 'opaque' || response.type == 'opaqueredirect' ? "invalid cross origin flag! May target isn't a TeaSpeak server?" : response.statusText || "response is not ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* the transfer may not running anymore, because of a finished signal from the server (especially on file upload!) */
|
||||||
|
if(transfer.isRunning()) {
|
||||||
|
response.clone().blob().then(() => {
|
||||||
|
if(transfer.isRunning())
|
||||||
|
transfer.setTransferState(FileTransferState.FINISHED);
|
||||||
|
}).catch(error => {
|
||||||
|
if(typeof error !== "string")
|
||||||
|
log.error(LogCategory.FILE_TRANSFER, tr("Failed to transfer data throw a HTTPS request: %o"), error);
|
||||||
|
transfer.setFailed({
|
||||||
|
error: "io",
|
||||||
|
reason: "buffer-transfer-failed",
|
||||||
|
extraMessage: typeof error === "string" ? error : tr("lookup the console")
|
||||||
|
}, typeof error === "string" ? error : tr("lookup the console"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
if(error instanceof Error && error.message === "Failed to fetch")
|
||||||
|
throw "HTTPS download failed";
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function responseFileDownload(transfer: FileDownloadTransfer, target: ResponseTransferTargetImpl) {
|
||||||
|
target.setResponse(await performHTTPSTransfer(transfer, undefined));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadFileDownload(transfer: FileDownloadTransfer, target: DownloadTransferTargetImpl) {
|
||||||
|
const url = generateTransferURL(transfer);
|
||||||
|
target.startDownloadURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ResponseTransferTargetImpl extends ResponseTransferTarget {
|
||||||
|
private response: Response;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
hasResponse() {
|
||||||
|
return typeof this.response !== "undefined";
|
||||||
|
}
|
||||||
|
|
||||||
|
getResponse() {
|
||||||
|
return this.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
setResponse(response: Response) {
|
||||||
|
this.response = response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DownloadTransferTargetImpl extends DownloadTransferTarget {
|
||||||
|
readonly fileName: string | undefined;
|
||||||
|
|
||||||
|
constructor(fileName: string | undefined) {
|
||||||
|
super();
|
||||||
|
this.fileName = fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
startDownloadURL(url: string) {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.style.display = 'none';
|
||||||
|
a.href = url;
|
||||||
|
a.target = "_blank";
|
||||||
|
if(this.fileName)
|
||||||
|
a.download = this.fileName;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BrowserFileTransferSourceImpl extends BrowserFileTransferSource {
|
||||||
|
private readonly file: File;
|
||||||
|
|
||||||
|
constructor(file: File) {
|
||||||
|
super();
|
||||||
|
this.file = file;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFile(): File {
|
||||||
|
return this.file;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fileSize(): Promise<number> {
|
||||||
|
return this.file.size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BufferTransferSourceImpl extends BufferTransferSource {
|
||||||
|
private readonly buffer: ArrayBuffer;
|
||||||
|
|
||||||
|
constructor(buffer: ArrayBuffer) {
|
||||||
|
super();
|
||||||
|
this.buffer = buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
getBuffer(): ArrayBuffer {
|
||||||
|
return this.buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fileSize(): Promise<number> {
|
||||||
|
return this.buffer.byteLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TextTransferSourceImpl extends TextTransferSource {
|
||||||
|
private readonly text: string;
|
||||||
|
private buffer: ArrayBuffer;
|
||||||
|
|
||||||
|
constructor(text: string) {
|
||||||
|
super();
|
||||||
|
this.text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
getText(): string {
|
||||||
|
return this.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async fileSize(): Promise<number> {
|
||||||
|
return this.getArrayBuffer().byteLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
getArrayBuffer() : ArrayBuffer {
|
||||||
|
if(this.buffer) return this.buffer;
|
||||||
|
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
this.buffer = encoder.encode(this.text);
|
||||||
|
return this.buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function formDataUpload(transfer: FileUploadTransfer, data: File | ArrayBuffer | string) {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
if(data instanceof File) {
|
||||||
|
formData.append("file", data);
|
||||||
|
} else if(typeof(data) === "string") {
|
||||||
|
formData.append("file", new Blob([data], { type: "application/octet-stream" }));
|
||||||
|
} else {
|
||||||
|
const buffer = data as BufferSource;
|
||||||
|
formData.append("file", new Blob([buffer], { type: "application/octet-stream" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
await performHTTPSTransfer(transfer, formData);
|
||||||
|
}
|
|
@ -384,7 +384,7 @@ export class ServerConnection extends AbstractServerConnection {
|
||||||
this._ping.last_response = 'now' in performance ? performance.now() : Date.now();
|
this._ping.last_response = 'now' in performance ? performance.now() : Date.now();
|
||||||
this._ping.value = this._ping.last_response - this._ping.last_request;
|
this._ping.value = this._ping.last_response - this._ping.last_request;
|
||||||
this._ping.value_native = parseInt(json["ping_native"]) / 1000; /* we're getting it in microseconds and not milliseconds */
|
this._ping.value_native = parseInt(json["ping_native"]) / 1000; /* we're getting it in microseconds and not milliseconds */
|
||||||
log.debug(LogCategory.NETWORKING, tr("Received new pong. Updating ping to: JS: %o Native: %o"), this._ping.value.toFixed(3), this._ping.value_native.toFixed(3));
|
//log.debug(LogCategory.NETWORKING, tr("Received new pong. Updating ping to: JS: %o Native: %o"), this._ping.value.toFixed(3), this._ping.value_native.toFixed(3));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.warn(LogCategory.NETWORKING, tr("Unknown command type %o"), json["type"]);
|
log.warn(LogCategory.NETWORKING, tr("Unknown command type %o"), json["type"]);
|
||||||
|
|
|
@ -2,3 +2,5 @@ const webrtc_adapter = require("webrtc-adapter");
|
||||||
/* typescript keep alive */ let _x = (webrtc_adapter || "").toString();
|
/* typescript keep alive */ let _x = (webrtc_adapter || "").toString();
|
||||||
const tc = require("tc-shared/main");
|
const tc = require("tc-shared/main");
|
||||||
export = tc;
|
export = tc;
|
||||||
|
|
||||||
|
require("./FileTransfer");
|
|
@ -1 +1 @@
|
||||||
Subproject commit adcb7bc21d0afa79c1975030b29dfeef76651839
|
Subproject commit 5c94ec3205c30171ffd01056f5b4622b7c0ab54c
|
|
@ -97,7 +97,7 @@ export const config = async (target: "web" | "client") => { return {
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.s[ac]ss$/,
|
test: /\.(s[ac]|c)ss$/,
|
||||||
loader: [
|
loader: [
|
||||||
'style-loader',
|
'style-loader',
|
||||||
/*
|
/*
|
||||||
|
@ -138,7 +138,8 @@ export const config = async (target: "web" | "client") => { return {
|
||||||
getCustomTransformers: (prog: ts.Program) => {
|
getCustomTransformers: (prog: ts.Program) => {
|
||||||
return {
|
return {
|
||||||
before: [trtransformer(prog, {
|
before: [trtransformer(prog, {
|
||||||
optimized: true,
|
optimized: false,
|
||||||
|
verbose: true,
|
||||||
target_file: path.join(__dirname, "dist", "translations.json")
|
target_file: path.join(__dirname, "dist", "translations.json")
|
||||||
})]
|
})]
|
||||||
};
|
};
|
||||||
|
@ -158,6 +159,10 @@ export const config = async (target: "web" | "client") => { return {
|
||||||
loader: [
|
loader: [
|
||||||
"./webpack/WatLoader.js"
|
"./webpack/WatLoader.js"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.svg$/,
|
||||||
|
loader: 'svg-inline-loader'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|