Added the new bookmark system and updated the bookmark UI
This commit is contained in:
parent
439ba5488e
commit
6ff210be07
63 changed files with 3078 additions and 1538 deletions
18
ChangeLog.md
18
ChangeLog.md
|
@ -1,4 +1,12 @@
|
|||
# Changelog:
|
||||
* **14.03.21**
|
||||
- Enchanted the bookmark system
|
||||
- Added support for auto connect on startup
|
||||
- Cleaned and simplified up the bookmark UI
|
||||
- Added support for importing/exporting bookmarks
|
||||
- Added support for duplicating bookmarks
|
||||
- Adding support for default channels and passwords
|
||||
|
||||
* **12.03.21**
|
||||
- Added a new video spotlight mode which allows showing multiple videos at the same time as well as
|
||||
dragging and resizing them
|
||||
|
@ -150,7 +158,7 @@
|
|||
- Fixed invalid channel tree unique id assignment for the initial server entry ([#F2986](https://forum.teaspeak.de/index.php?threads/2986))
|
||||
|
||||
* **27.09.20**
|
||||
- Middle clicking on bookmarks now directly connects in a new tab
|
||||
- Middle clicking on bookmarksOld now directly connects in a new tab
|
||||
|
||||
* **26.09.20**
|
||||
- Updating group prefix/suffixes when the group naming mode changes
|
||||
|
@ -320,7 +328,7 @@
|
|||
- Fixed channel tree deletions
|
||||
- Removed layout recalculate bottleneck on connection handler switching
|
||||
- Fixed empty channel tree on tab change, if the tree has some scroll offset
|
||||
- Added the ability to duplicate bookmarks
|
||||
- Added the ability to duplicate bookmarksOld
|
||||
- Fixed issue [#106](https://github.com/TeaSpeak/TeaWeb/issues/106)
|
||||
- Fixed issue [#90](https://github.com/TeaSpeak/TeaWeb/issues/90)
|
||||
|
||||
|
@ -350,7 +358,7 @@
|
|||
* **21.04.20**
|
||||
- Clicking on the music bot does not longer results in the insufficient permission sound when the client has no permissions
|
||||
- Fixed permission editor overflow
|
||||
- Fixed the bookmark edit window (bookmarks have failed to save)
|
||||
- Fixed the bookmark edit window (bookmarksOld have failed to save)
|
||||
|
||||
* **18.04.20**
|
||||
- Recoded the channel tree using React
|
||||
|
@ -447,7 +455,7 @@
|
|||
- Improved the server info modal experience (Correctly showing no permissions)
|
||||
- Improved "About" modal overflow behaviour
|
||||
- Allow the client to use the scroll bar without closing the modal within modals
|
||||
- Improved bookmarks modal for smaller devices
|
||||
- Improved bookmarksOld modal for smaller devices
|
||||
- Fixed invalid white space representation
|
||||
|
||||
* **10.12.19**
|
||||
|
@ -637,7 +645,7 @@
|
|||
- Added query account management (since server 1.2.32b)
|
||||
|
||||
* **18.12.18**
|
||||
- Added bookmarks and bookmarks management
|
||||
- Added bookmarksOld and bookmarksOld management
|
||||
- Added query user visibility button and creation (Query management will follow soon)
|
||||
- Fixed overflow within the group assignment dialog
|
||||
|
||||
|
|
2
file.ts
2
file.ts
|
@ -40,7 +40,7 @@ const APP_FILE_LIST_SHARED_SOURCE: ProjectResource[] = [
|
|||
},
|
||||
{ /* javascript files as manifest.json */
|
||||
"type": "js",
|
||||
"search-pattern": /.*\.(js|json|svg)$/,
|
||||
"search-pattern": /.*\.(js|json|svg|png)$/,
|
||||
"build-target": "dev|rel",
|
||||
|
||||
"path": "js/",
|
||||
|
|
88
package-lock.json
generated
88
package-lock.json
generated
|
@ -8912,6 +8912,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"jsonschema": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.4.0.tgz",
|
||||
"integrity": "sha512-/YgW6pRMr6M7C+4o8kS+B/2myEpHCrxO4PEWnqJNBFMjn7EWXqlQ4tGwL6xTHeRplwuZmcAncdvfOad1nT2yMw=="
|
||||
},
|
||||
"jsprim": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
|
||||
|
@ -14431,6 +14436,89 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"url-loader": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz",
|
||||
"integrity": "sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"loader-utils": "^2.0.0",
|
||||
"mime-types": "^2.1.27",
|
||||
"schema-utils": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/json-schema": {
|
||||
"version": "7.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz",
|
||||
"integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==",
|
||||
"dev": true
|
||||
},
|
||||
"ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
}
|
||||
},
|
||||
"ajv-keywords": {
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
|
||||
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
|
||||
"dev": true
|
||||
},
|
||||
"json5": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
|
||||
"integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"minimist": "^1.2.5"
|
||||
}
|
||||
},
|
||||
"loader-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
"json5": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.46.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.46.0.tgz",
|
||||
"integrity": "sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ==",
|
||||
"dev": true
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.29",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.29.tgz",
|
||||
"integrity": "sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"mime-db": "1.46.0"
|
||||
}
|
||||
},
|
||||
"schema-utils": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz",
|
||||
"integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/json-schema": "^7.0.6",
|
||||
"ajv": "^6.12.5",
|
||||
"ajv-keywords": "^3.5.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"url-parse-lax": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
|
||||
|
|
|
@ -74,6 +74,7 @@
|
|||
"ts-loader": "^6.2.2",
|
||||
"tsd": "^0.13.1",
|
||||
"typescript": "^3.7.0",
|
||||
"url-loader": "^4.1.1",
|
||||
"wabt": "^1.0.13",
|
||||
"webpack": "^4.42.1",
|
||||
"webpack-bundle-analyzer": "^3.6.1",
|
||||
|
@ -101,6 +102,7 @@
|
|||
"highlight.js": "^10.1.1",
|
||||
"ip-regex": "^4.2.0",
|
||||
"jquery": "^3.5.1",
|
||||
"jsonschema": "^1.4.0",
|
||||
"jsrender": "^1.0.7",
|
||||
"moment": "^2.24.0",
|
||||
"react": "^16.13.1",
|
||||
|
|
|
@ -14,7 +14,6 @@ import "!style-loader!css-loader?url=false!sass-loader?sourceMap=true!./static/m
|
|||
import "!style-loader!css-loader?url=false!sass-loader?sourceMap=true!./static/modal-avatar.scss"
|
||||
import "!style-loader!css-loader?url=false!sass-loader?sourceMap=true!./static/modal-banclient.scss"
|
||||
import "!style-loader!css-loader?url=false!sass-loader?sourceMap=true!./static/modal-banlist.scss"
|
||||
import "!style-loader!css-loader?url=false!sass-loader?sourceMap=true!./static/modal-bookmarks.scss"
|
||||
import "!style-loader!css-loader?url=false!sass-loader?sourceMap=true!./static/modal-channelinfo.scss"
|
||||
import "!style-loader!css-loader?url=false!sass-loader?sourceMap=true!./static/modal-clientinfo.scss"
|
||||
import "!style-loader!css-loader?url=false!sass-loader?sourceMap=true!./static/modal-connect.scss"
|
||||
|
|
|
@ -66,29 +66,29 @@
|
|||
user-select: $mode;
|
||||
}
|
||||
|
||||
@mixin chat-scrollbar() {
|
||||
@mixin chat-scrollbar($width: .5em) {
|
||||
& {
|
||||
/* for moz */
|
||||
scrollbar-color: #353535 #555;
|
||||
scrollbarWidth: .5em;
|
||||
scrollbarWidth: $width;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
border-radius: .25em;
|
||||
border-radius: $width / 2;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: .5em;
|
||||
height: .5em;
|
||||
width: $width;
|
||||
height: $width;
|
||||
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: .25em;
|
||||
border-radius: $width / 2;
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,498 +0,0 @@
|
|||
@import "properties";
|
||||
@import "mixin";
|
||||
|
||||
.modal .modal-bookmark-create {
|
||||
.property {
|
||||
margin-top: 5px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
.key {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
select, input {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
text-align: right;
|
||||
|
||||
button {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body.modal-bookmarks {
|
||||
padding: 0!important;
|
||||
|
||||
display: flex!important;
|
||||
flex-direction: row!important;
|
||||
justify-content: stretch!important;
|
||||
|
||||
min-width: 30em!important;
|
||||
height: 45em;
|
||||
width: 80em;
|
||||
|
||||
@include user-select(none);
|
||||
|
||||
.container-tooltip {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
position: relative;
|
||||
width: 1.6em;
|
||||
margin-left: .5em;
|
||||
font-size: .9em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
|
||||
align-self: center;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.input-boxed {
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
.left {
|
||||
min-width: 12em;
|
||||
width: 30%;
|
||||
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
padding: .5em;
|
||||
background-color: #212125;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
.title {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
text-align: center;
|
||||
|
||||
font-size: 1.5em;
|
||||
color: #557edc;
|
||||
text-transform: uppercase;
|
||||
|
||||
@include text-dotdotdot();
|
||||
}
|
||||
|
||||
.container-bookmarks {
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
|
||||
min-height: 6em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
overflow: auto;
|
||||
@include chat-scrollbar-vertical();
|
||||
@include chat-scrollbar-horizontal();
|
||||
|
||||
.bookmark, .directory {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
border-radius: $border_radius_middle;
|
||||
padding: .25em .5em;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
.icon-container {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
align-self: center;
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
.name {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
min-width: 5em;
|
||||
align-self: center;
|
||||
|
||||
@include text-dotdotdot();
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #2c2d2f;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: #1a1a1b;
|
||||
}
|
||||
|
||||
.link {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
position: relative;
|
||||
width: 1.5em;
|
||||
|
||||
$line_width: 2px;
|
||||
$color: hsla(0, 0%, 35%, 1);
|
||||
&:not(.hidden) {
|
||||
&:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
|
||||
height: 2.25em; /* connect with the previous one */
|
||||
width: .75em;
|
||||
|
||||
left: .5em; /* icons have a width of 1em */
|
||||
bottom: calc(.75em - #{$line_width / 2});
|
||||
|
||||
border-left: $line_width solid $color;
|
||||
}
|
||||
|
||||
&.connected {
|
||||
&:before {
|
||||
border-bottom: $line_width solid $color;
|
||||
|
||||
border-bottom-left-radius: .3em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-start {
|
||||
.link.connected {
|
||||
&:before {
|
||||
height: 1.25em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.directory {
|
||||
.name {
|
||||
//color: #557edc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
padding-top: .5em;
|
||||
|
||||
button {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-left: .5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
min-width: 25em;
|
||||
width: 30%;
|
||||
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
background-color: #2f2f35;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
.header {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
height: 10em;
|
||||
|
||||
background: url('../../../img/bookmark_background.png'), url('../../img/bookmark_background.png'), url('img/bookmark_background.png') no-repeat;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
|
||||
padding: .5em;
|
||||
|
||||
.container-name {
|
||||
font-size: 2em;
|
||||
color: #fcfcfc;
|
||||
|
||||
@include text-dotdotdot();
|
||||
}
|
||||
|
||||
.container-address {
|
||||
font-size: 1.5em;
|
||||
color: #fcfcfc;
|
||||
|
||||
@include text-dotdotdot();
|
||||
}
|
||||
}
|
||||
|
||||
.container-settings {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-height: 10em;
|
||||
|
||||
padding: .5em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
@include chat-scrollbar-vertical();
|
||||
|
||||
.group {
|
||||
padding: .5em;
|
||||
|
||||
border-radius: .2em;
|
||||
border: 1px solid #1f2122;
|
||||
|
||||
background-color: #28292b;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
> .row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
.key {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
|
||||
width: 15em;
|
||||
min-width: 2em;
|
||||
|
||||
align-self: center;
|
||||
|
||||
color: #557edc;
|
||||
|
||||
text-transform: uppercase;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.value {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
min-width: 2em;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
&.info {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.container-image {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
max-width: 15em;
|
||||
max-height: 9em; /* minus one padding */
|
||||
width: 15em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
object-fit: contain;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@include transition(.25s ease-in-out);
|
||||
}
|
||||
|
||||
.container-properties {
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
|
||||
min-width: 23em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
height: inherit;
|
||||
|
||||
.row {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
height: 1.8em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
|
||||
.key {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
color: #557edc;
|
||||
text-transform: uppercase;
|
||||
align-self: center;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
width: 15em;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #d6d6d7;
|
||||
align-self: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&.server-region {
|
||||
> div {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.country {
|
||||
margin-right: .25em;
|
||||
}
|
||||
}
|
||||
|
||||
.connect-count, .connect-never {
|
||||
display: inline-block;
|
||||
|
||||
color: #7a3131;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container-network {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
|
||||
.container-button {
|
||||
margin-right: 1em;
|
||||
|
||||
flex-shrink: 1;
|
||||
min-width: 5em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
|
||||
button {
|
||||
height: 2.5em;
|
||||
width: 12em;
|
||||
|
||||
max-width: 100%;
|
||||
|
||||
@include text-dotdotdot();
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
padding: .5em;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
|
||||
.button-duplicate {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
button {
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
|
||||
min-width: 2em;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-left: .5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media all and (max-width: 50em) {
|
||||
.modal-body.modal-bookmarks {
|
||||
.container-image {
|
||||
margin: 0!important;
|
||||
max-width: 0!important;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2035,173 +2035,6 @@
|
|||
</div>
|
||||
</script>
|
||||
|
||||
<script class="jsrender-template" id="tmpl_manage_bookmarks" type="text/html">
|
||||
<div>
|
||||
<div class="left">
|
||||
<div class="title" title="{{tr 'Your bookmarks' /}}">{{tr "Your bookmarks" /}}</div>
|
||||
<div class="container-bookmarks">
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<button class="btn btn-danger button-delete" title="{{tr 'Delete' /}}">{{tr "Delete" /}}</button>
|
||||
<button class="btn btn-purple button-add-folder" title="{{tr 'Add Folder' /}}">{{tr "Add Folder" /}}</button>
|
||||
<button class="btn btn-success button-add-bookmark" title="{{tr 'Add Bookmark' /}}">{{tr "Add Bookmark" /}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-seperator vertical" seperator-id="seperator-bookmarks"></div>
|
||||
<div class="right">
|
||||
<div class="header">
|
||||
<div class="container-name">{{tr "Your bookmarks" /}}</div>
|
||||
<div class="container-address"></div>
|
||||
</div>
|
||||
<div class="container-settings" data-simplebar data-simplebar-auto-hide="false">
|
||||
<div class="group">
|
||||
<div class="row">
|
||||
<div class="key">{{tr "Bookmark Name" /}}</div>
|
||||
<div class="value">
|
||||
<div class="input-boxed">
|
||||
<input class="input-bookmark-name">
|
||||
<!--
|
||||
<div class="container-tooltip">
|
||||
<img src="img/icon_tooltip.svg"/>
|
||||
<div class="tooltip">
|
||||
<a>{{tr "The displayed name of this bookmark." /}}</a>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="key">{{tr "Connect Profile" /}}</div>
|
||||
<div class="value">
|
||||
<div class="input-boxed">
|
||||
<select class="input-connect-profile">
|
||||
</select>
|
||||
<!--
|
||||
<div class="container-tooltip">
|
||||
<img src="img/icon_tooltip.svg"/>
|
||||
<div class="tooltip">
|
||||
<a>{{tr "The profile which you're connection with when you're using the bookmark." /}}</a>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="group">
|
||||
<div class="row">
|
||||
<div class="key">{{tr "Server Address" /}}</div>
|
||||
<div class="value">
|
||||
<div class="input-boxed">
|
||||
<input class="input-server-address">
|
||||
<!--
|
||||
<div class="container-tooltip">
|
||||
<img src="img/icon_tooltip.svg"/>
|
||||
<div class="tooltip">
|
||||
<a>{{tr "The server address of the bookmark. The port is separated via a colon" /}}</a>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="key">{{tr "Server Password" /}}</div>
|
||||
<div class="value">
|
||||
<div class="input-boxed">
|
||||
<input type="password" class="input-server-password">
|
||||
<!--
|
||||
<div class="container-tooltip">
|
||||
<img src="img/icon_tooltip.svg"/>
|
||||
<div class="tooltip">
|
||||
<a>{{tr "The server port of the bookmark" /}}</a>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="key">{{tr "Default Channel" /}}</div>
|
||||
<div class="value">
|
||||
<div class="input-boxed">
|
||||
<input class="input-default-channel" placeholder="{{tr 'Not yet implemented' /}}" disabled>
|
||||
<!--
|
||||
<div class="container-tooltip">
|
||||
<img src="img/icon_tooltip.svg"/>
|
||||
<div class="tooltip">
|
||||
<a>{{tr "The server port of the bookmark" /}}</a>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="key">{{tr "Channel Password" /}}</div>
|
||||
<div class="value">
|
||||
<div class="input-boxed">
|
||||
<input type="password" class="input-default-channel-password" placeholder="{{tr 'Not yet implemented' /}}" disabled>
|
||||
<!--
|
||||
<div class="container-tooltip">
|
||||
<img src="img/icon_tooltip.svg"/>
|
||||
<div class="tooltip">
|
||||
<a>{{tr "The server port of the bookmark" /}}</a>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="group info">
|
||||
<div class="container-image">
|
||||
<img src="img/serveredit_1.png">
|
||||
</div>
|
||||
<div class="container-properties">
|
||||
<div class="row">
|
||||
<a class="key">{{tr "Server name" /}}</a>
|
||||
<div class="value server-name">
|
||||
error: name
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<a class="key">{{tr "Server region" /}}</a>
|
||||
<div class="value server-region">
|
||||
error: region
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<a class="key">{{tr "Last ping" /}}</a>
|
||||
<div class="value server-ping">
|
||||
error: ping
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<a class="key">{{tr "Last client count" /}}</a>
|
||||
<div class="value server-client-count">
|
||||
error: last client
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<a class="key">{{tr "Your connection" /}}</a>
|
||||
<div class="value server-connection-count">
|
||||
error: connection count
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<button class="btn btn-blue button-duplicate">{{tr "Duplicate" /}}</button>
|
||||
<button class="btn btn-success button-connect-tab">{{tr "Connect in a new tab" /}}</button>
|
||||
<button class="btn btn-success button-connect">{{tr "Connect" /}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script class="jsrender-template" id="tmpl_icon_select" type="text/html">
|
||||
<div class="container-icons">
|
||||
<div class="left">
|
||||
|
|
4
shared/img/client-icons/bookmark_edit_name.svg
Normal file
4
shared/img/client-icons/bookmark_edit_name.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg id="client-change_nickname" x="384" y="32" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m3.57 9.56-2.445 5.315 5.316-2.445c-.358-.566-.781-1.103-1.274-1.596s-1.03-.916-1.596-1.274z" fill="#7289da" style="fill:#fffffe"/>
|
||||
<path d="m14.875 3.687c0-.297-.104-.547-.313-.749l-1.498-1.501c-.203-.207-.452-.311-.751-.311s-.552.101-.759.302l-7.175 7.174c.447.293.877.624 1.284.992.129.117.256.237.381.362s.245.252.363.382c.369.407.699.836.992 1.284l7.175-7.175c.2-.207.301-.461.301-.76z" fill="#7289da" style="fill:#fffffe"/>
|
||||
</svg>
|
After Width: | Height: | Size: 566 B |
|
@ -1,261 +1,524 @@
|
|||
import {LogCategory, logError} from "./log";
|
||||
import {guid} from "./crypto/uid";
|
||||
import {createErrorModal, createInfoModal, createInputModal} from "./ui/elements/Modal";
|
||||
import {defaultConnectProfile, findConnectProfile} from "./profiles/ConnectionProfile";
|
||||
import {ConnectionHandler} from "./ConnectionHandler";
|
||||
import * as loader from "tc-loader";
|
||||
import {Stage} from "tc-loader";
|
||||
import {WritableKeys} from "tc-shared/proto";
|
||||
import {LogCategory, logDebug, logError, logInfo, logTrace, logWarn} from "tc-shared/log";
|
||||
import {guid} from "tc-shared/crypto/uid";
|
||||
import {Registry} from "tc-events";
|
||||
import {server_connections} from "tc-shared/ConnectionManager";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {spawnConnectModalNew} from "tc-shared/ui/modal/connect/Controller";
|
||||
import {defaultConnectProfile, findConnectProfile} from "tc-shared/profiles/ConnectionProfile";
|
||||
import {ConnectionState} from "tc-shared/ConnectionHandler";
|
||||
import * as _ from "lodash";
|
||||
|
||||
/* TODO: much better events? */
|
||||
export interface BookmarkEvents {
|
||||
notify_bookmarks_updated: {}
|
||||
}
|
||||
type BookmarkBase = {
|
||||
readonly uniqueId: string,
|
||||
displayName: string,
|
||||
|
||||
export const bookmarkEvents = new Registry<BookmarkEvents>();
|
||||
|
||||
export const boorkmak_connect = (mark: Bookmark, new_tab?: boolean) => {
|
||||
const profile = findConnectProfile(mark.connect_profile) || defaultConnectProfile();
|
||||
if(profile.valid()) {
|
||||
const connection = (typeof(new_tab) !== "boolean" || !new_tab) ? server_connections.getActiveConnectionHandler() : server_connections.spawnConnectionHandler();
|
||||
server_connections.setActiveConnectionHandler(connection);
|
||||
connection.startConnection(
|
||||
mark.server_properties.server_address + ":" + mark.server_properties.server_port,
|
||||
profile,
|
||||
true,
|
||||
{
|
||||
nickname: mark.nickname === "Another TeaSpeak user" || !mark.nickname ? profile.connectUsername() : mark.nickname,
|
||||
password: mark.server_properties.server_password_hash ? {
|
||||
password: mark.server_properties.server_password_hash,
|
||||
hashed: true
|
||||
} : mark.server_properties.server_password ? {
|
||||
hashed: false,
|
||||
password: mark.server_properties.server_password
|
||||
} : undefined
|
||||
}
|
||||
);
|
||||
} else {
|
||||
spawnConnectModalNew({
|
||||
selectedAddress: mark.server_properties.server_address + ":" + mark.server_properties.server_port,
|
||||
selectedProfile: profile
|
||||
});
|
||||
}
|
||||
previousEntry: string | undefined,
|
||||
parentEntry: string | undefined,
|
||||
};
|
||||
|
||||
export interface ServerProperties {
|
||||
server_address: string;
|
||||
server_port: number;
|
||||
server_password_hash?: string;
|
||||
server_password?: string;
|
||||
export type BookmarkInfo = BookmarkBase & {
|
||||
readonly type: "entry",
|
||||
|
||||
connectOnStartup: boolean,
|
||||
connectProfile: string,
|
||||
|
||||
serverAddress: string,
|
||||
serverPasswordHash: string | undefined,
|
||||
|
||||
defaultChannel: string | undefined,
|
||||
defaultChannelPasswordHash: string | undefined,
|
||||
}
|
||||
|
||||
export enum BookmarkType {
|
||||
ENTRY,
|
||||
DIRECTORY
|
||||
export type BookmarkDirectory = BookmarkBase & {
|
||||
readonly type: "directory",
|
||||
}
|
||||
|
||||
export interface Bookmark {
|
||||
type: BookmarkType.ENTRY;
|
||||
/* readonly */ parent: DirectoryBookmark;
|
||||
export type BookmarkEntry = BookmarkInfo | BookmarkDirectory;
|
||||
|
||||
server_properties: ServerProperties;
|
||||
display_name: string;
|
||||
unique_id: string;
|
||||
|
||||
nickname: string;
|
||||
default_channel?: number | string;
|
||||
default_channel_password_hash?: string;
|
||||
default_channel_password?: string;
|
||||
|
||||
connect_profile: string;
|
||||
|
||||
last_icon_id?: number;
|
||||
last_icon_server_id?: string;
|
||||
export interface BookmarkEvents {
|
||||
notify_bookmark_created: { bookmark: BookmarkEntry },
|
||||
notify_bookmark_edited: { bookmark: BookmarkEntry, keys: (keyof BookmarkInfo | keyof BookmarkDirectory)[] },
|
||||
notify_bookmark_deleted: { bookmark: BookmarkEntry, children: BookmarkEntry[] },
|
||||
notify_bookmarks_imported: { bookmarks: BookmarkEntry[] },
|
||||
}
|
||||
|
||||
export interface DirectoryBookmark {
|
||||
type: BookmarkType.DIRECTORY;
|
||||
/* readonly */ parent: DirectoryBookmark;
|
||||
export type OrderedBookmarkEntry = {
|
||||
entry: BookmarkEntry,
|
||||
depth: number,
|
||||
childCount: number,
|
||||
};
|
||||
|
||||
readonly content: (Bookmark | DirectoryBookmark)[];
|
||||
unique_id: string;
|
||||
display_name: string;
|
||||
}
|
||||
const kLocalStorageKey = "bookmarks_v2";
|
||||
export class BookmarkManager {
|
||||
readonly events: Registry<BookmarkEvents>;
|
||||
private readonly registeredBookmarks: BookmarkEntry[];
|
||||
private defaultBookmarkCreated: boolean;
|
||||
|
||||
interface BookmarkConfig {
|
||||
root_bookmark?: DirectoryBookmark;
|
||||
default_added?: boolean;
|
||||
}
|
||||
|
||||
let _bookmark_config: BookmarkConfig;
|
||||
|
||||
function bookmark_config() : BookmarkConfig {
|
||||
if(_bookmark_config)
|
||||
return _bookmark_config;
|
||||
|
||||
let bookmark_json = localStorage.getItem("bookmarks");
|
||||
let bookmarks;
|
||||
try {
|
||||
bookmarks = JSON.parse(bookmark_json) || {} as BookmarkConfig;
|
||||
} catch(error) {
|
||||
logError(LogCategory.BOOKMARKS, tr("Failed to load bookmarks: %o"), error);
|
||||
bookmarks = {} as any;
|
||||
constructor() {
|
||||
this.events = new Registry<BookmarkEvents>();
|
||||
this.registeredBookmarks = [];
|
||||
this.defaultBookmarkCreated = false;
|
||||
this.loadBookmarks();
|
||||
}
|
||||
|
||||
_bookmark_config = bookmarks;
|
||||
_bookmark_config.root_bookmark = _bookmark_config.root_bookmark || { content: [], display_name: "root", type: BookmarkType.DIRECTORY} as DirectoryBookmark;
|
||||
|
||||
if(!_bookmark_config.default_added) {
|
||||
_bookmark_config.default_added = true;
|
||||
create_bookmark("TeaSpeak official Test-Server", _bookmark_config.root_bookmark, {
|
||||
server_address: "ts.teaspeak.de",
|
||||
server_port: 9987
|
||||
}, undefined);
|
||||
|
||||
save_config();
|
||||
}
|
||||
|
||||
const fix_parent = (parent: DirectoryBookmark, entry: Bookmark | DirectoryBookmark) => {
|
||||
entry.parent = parent;
|
||||
if(entry.type === BookmarkType.DIRECTORY)
|
||||
for(const child of (entry as DirectoryBookmark).content)
|
||||
fix_parent(entry as DirectoryBookmark, child);
|
||||
};
|
||||
for(const entry of _bookmark_config.root_bookmark.content)
|
||||
fix_parent(_bookmark_config.root_bookmark, entry);
|
||||
|
||||
return _bookmark_config;
|
||||
}
|
||||
|
||||
function save_config() {
|
||||
localStorage.setItem("bookmarks", JSON.stringify(bookmark_config(), (key, value) => {
|
||||
if(key === "parent")
|
||||
return undefined;
|
||||
return value;
|
||||
}));
|
||||
}
|
||||
|
||||
export function bookmarks() : DirectoryBookmark {
|
||||
return bookmark_config().root_bookmark;
|
||||
}
|
||||
|
||||
export function bookmarks_flat() : Bookmark[] {
|
||||
const result: Bookmark[] = [];
|
||||
const _flat = (bookmark: Bookmark | DirectoryBookmark) => {
|
||||
if(bookmark.type == BookmarkType.DIRECTORY)
|
||||
for(const book of (bookmark as DirectoryBookmark).content)
|
||||
_flat(book);
|
||||
else
|
||||
result.push(bookmark as Bookmark);
|
||||
};
|
||||
_flat(bookmark_config().root_bookmark);
|
||||
return result;
|
||||
}
|
||||
|
||||
function find_bookmark_recursive(parent: DirectoryBookmark, uuid: string) : Bookmark | DirectoryBookmark {
|
||||
for(const entry of parent.content) {
|
||||
if(entry.unique_id == uuid)
|
||||
return entry;
|
||||
if(entry.type == BookmarkType.DIRECTORY) {
|
||||
const result = find_bookmark_recursive(entry as DirectoryBookmark, uuid);
|
||||
if(result) return result;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function find_bookmark(uuid: string) : Bookmark | DirectoryBookmark | undefined {
|
||||
return find_bookmark_recursive(bookmarks(), uuid);
|
||||
}
|
||||
|
||||
export function parent_bookmark(bookmark: Bookmark) : DirectoryBookmark {
|
||||
const books: (DirectoryBookmark | Bookmark)[] = [bookmarks()];
|
||||
while(!books.length) {
|
||||
const directory = books.pop_front();
|
||||
if(directory.type == BookmarkType.DIRECTORY) {
|
||||
const cast = <DirectoryBookmark>directory;
|
||||
|
||||
if(cast.content.indexOf(bookmark) != -1)
|
||||
return cast;
|
||||
books.push(...cast.content);
|
||||
}
|
||||
}
|
||||
return bookmarks();
|
||||
}
|
||||
|
||||
export function create_bookmark(display_name: string, directory: DirectoryBookmark, server_properties: ServerProperties, nickname: string) : Bookmark {
|
||||
const bookmark = {
|
||||
display_name: display_name,
|
||||
server_properties: server_properties,
|
||||
nickname: nickname,
|
||||
type: BookmarkType.ENTRY,
|
||||
connect_profile: "default",
|
||||
unique_id: guid(),
|
||||
parent: directory
|
||||
} as Bookmark;
|
||||
|
||||
directory.content.push(bookmark);
|
||||
return bookmark;
|
||||
}
|
||||
|
||||
export function create_bookmark_directory(parent: DirectoryBookmark, name: string) : DirectoryBookmark {
|
||||
const bookmark = {
|
||||
type: BookmarkType.DIRECTORY,
|
||||
|
||||
display_name: name,
|
||||
content: [],
|
||||
unique_id: guid(),
|
||||
parent: parent
|
||||
} as DirectoryBookmark;
|
||||
|
||||
parent.content.push(bookmark);
|
||||
return bookmark;
|
||||
}
|
||||
|
||||
//TODO test if the new parent is within the old bookmark
|
||||
export function change_directory(parent: DirectoryBookmark, bookmark: Bookmark | DirectoryBookmark) {
|
||||
delete_bookmark(bookmark);
|
||||
parent.content.push(bookmark)
|
||||
}
|
||||
|
||||
export function save_bookmark(bookmark?: Bookmark | DirectoryBookmark) {
|
||||
bookmarkEvents.fire("notify_bookmarks_updated");
|
||||
save_config(); /* nvm we dont give a fuck... saving everything */
|
||||
}
|
||||
|
||||
function delete_bookmark_recursive(parent: DirectoryBookmark, bookmark: Bookmark | DirectoryBookmark) {
|
||||
const index = parent.content.indexOf(bookmark);
|
||||
if(index != -1)
|
||||
parent.content.remove(bookmark);
|
||||
else
|
||||
for(const entry of parent.content)
|
||||
if(entry.type == BookmarkType.DIRECTORY)
|
||||
delete_bookmark_recursive(entry as DirectoryBookmark, bookmark)
|
||||
}
|
||||
|
||||
export function delete_bookmark(bookmark: Bookmark | DirectoryBookmark) {
|
||||
delete_bookmark_recursive(bookmarks(), bookmark)
|
||||
}
|
||||
|
||||
export function add_server_to_bookmarks(server: ConnectionHandler) {
|
||||
if(server && server.connected) {
|
||||
const ce = server.getClient();
|
||||
const name = ce ? ce.clientNickName() : undefined;
|
||||
createInputModal(tr("Enter bookmarks name"), tr("Please enter the bookmarks name:<br>"), text => text.length > 0, result => {
|
||||
if(result) {
|
||||
const bookmark = create_bookmark(result as string, bookmarks(), {
|
||||
server_port: server.serverConnection.remote_address().port,
|
||||
server_address: server.serverConnection.remote_address().host,
|
||||
|
||||
server_password: "",
|
||||
server_password_hash: ""
|
||||
}, name);
|
||||
save_bookmark(bookmark);
|
||||
|
||||
createInfoModal(tr("Server added"), tr("Server has been successfully added to your bookmarks.")).open();
|
||||
private loadBookmarks() {
|
||||
const bookmarksJson = localStorage.getItem(kLocalStorageKey);
|
||||
if(typeof bookmarksJson !== "string") {
|
||||
const oldBookmarksJson = localStorage.getItem("bookmarks");
|
||||
if(typeof oldBookmarksJson === "string") {
|
||||
logDebug(LogCategory.BOOKMARKS, tr("Found no new bookmarks but found old bookmarks. Trying to import."));
|
||||
try {
|
||||
this.importOldBookmarks(oldBookmarksJson);
|
||||
logInfo(LogCategory.BOOKMARKS, tr("Successfully imported %d old bookmarks."), this.registeredBookmarks.length);
|
||||
this.saveBookmarks();
|
||||
} catch (error) {
|
||||
const saveKey = "bookmarks_v1_save_" + Date.now();
|
||||
logError(LogCategory.BOOKMARKS, tr("Failed to import old bookmark data. Saving it as %s"), saveKey);
|
||||
localStorage.setItem(saveKey, oldBookmarksJson);
|
||||
} finally {
|
||||
localStorage.removeItem("bookmarks");
|
||||
}
|
||||
}
|
||||
}).open();
|
||||
} else {
|
||||
createErrorModal(tr("You have to be connected"), tr("You have to be connected!")).open();
|
||||
} else {
|
||||
try {
|
||||
const storageData = JSON.parse(bookmarksJson);
|
||||
if(storageData.version !== 2) {
|
||||
throw tr("bookmark storage has an invalid version");
|
||||
}
|
||||
|
||||
this.defaultBookmarkCreated = storageData.defaultBookmarkCreated;
|
||||
this.registeredBookmarks.slice(0, this.registeredBookmarks.length);
|
||||
this.registeredBookmarks.push(...storageData.bookmarks);
|
||||
logTrace(LogCategory.BOOKMARKS, tr("Loaded %d bookmarks."), this.registeredBookmarks.length);
|
||||
} catch (error) {
|
||||
const saveKey = "bookmarks_v2_save_" + Date.now();
|
||||
logError(LogCategory.BOOKMARKS, tr("Failed to parse bookmarks. Saving them at %s and using a clean setup."), saveKey)
|
||||
localStorage.setItem(saveKey, bookmarksJson);
|
||||
localStorage.removeItem("bookmarks_v2");
|
||||
}
|
||||
}
|
||||
|
||||
if(!this.defaultBookmarkCreated && this.registeredBookmarks.length === 0) {
|
||||
this.defaultBookmarkCreated = true;
|
||||
|
||||
logDebug(LogCategory.BOOKMARKS, tr("No bookmarks found. Registering default bookmark."));
|
||||
this.createBookmark({
|
||||
connectOnStartup: false,
|
||||
connectProfile: "default",
|
||||
|
||||
displayName: "Official TeaSpeak - Test server",
|
||||
|
||||
parentEntry: undefined,
|
||||
previousEntry: undefined,
|
||||
|
||||
serverAddress: "ts.teaspeak.de",
|
||||
serverPasswordHash: undefined,
|
||||
|
||||
defaultChannel: undefined,
|
||||
defaultChannelPasswordHash: undefined,
|
||||
});
|
||||
|
||||
this.saveBookmarks();
|
||||
}
|
||||
}
|
||||
|
||||
private importOldBookmarks(jsonData: string) {
|
||||
const data = JSON.parse(jsonData);
|
||||
if(typeof data?.root_bookmark !== "object") {
|
||||
throw tr("missing root bookmark");
|
||||
}
|
||||
|
||||
if(!Array.isArray(data?.root_bookmark?.content)) {
|
||||
throw tr("Missing root bookmarks content");
|
||||
}
|
||||
|
||||
const registerBookmarks = (parentEntry: string, previousEntry: string, entry: any) : string | undefined => {
|
||||
if(typeof entry.display_name !== "string") {
|
||||
logWarn(LogCategory.BOOKMARKS, tr("Missing display_name in old bookmark entry. Skipping entry."));
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if("content" in entry) {
|
||||
/* it was a directory */
|
||||
const directory = this.createDirectory({
|
||||
previousEntry,
|
||||
parentEntry,
|
||||
|
||||
displayName: entry.display_name,
|
||||
});
|
||||
|
||||
previousEntry = undefined;
|
||||
entry.content.forEach(entry => {
|
||||
previousEntry = registerBookmarks(directory.uniqueId, previousEntry, entry) || previousEntry;
|
||||
});
|
||||
} else {
|
||||
/* it was a normal entry */
|
||||
if(typeof entry.connect_profile !== "string") {
|
||||
logWarn(LogCategory.BOOKMARKS, tr("Missing connect_profile in old bookmark entry. Skipping entry."));
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if(typeof entry.server_properties?.server_address !== "string") {
|
||||
logWarn(LogCategory.BOOKMARKS, tr("Missing server_address in old bookmark entry. Skipping entry."));
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if(typeof entry.server_properties?.server_port !== "number") {
|
||||
logWarn(LogCategory.BOOKMARKS, tr("Missing server_port in old bookmark entry. Skipping entry."));
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let serverAddress;
|
||||
if(entry.server_properties.server_address.indexOf(":") !== -1) {
|
||||
serverAddress = `[${entry.server_properties.server_address}]`;
|
||||
} else {
|
||||
serverAddress = entry.server_properties.server_address;
|
||||
}
|
||||
serverAddress += ":" + entry.server_properties.server_port;
|
||||
|
||||
return this.createBookmark({
|
||||
previousEntry,
|
||||
parentEntry,
|
||||
|
||||
serverAddress,
|
||||
serverPasswordHash: entry.server_properties?.server_password_hash,
|
||||
|
||||
defaultChannel: undefined,
|
||||
defaultChannelPasswordHash: undefined,
|
||||
|
||||
displayName: entry.display_name,
|
||||
connectProfile: entry.connect_profile,
|
||||
|
||||
connectOnStartup: false
|
||||
}).uniqueId;
|
||||
}
|
||||
}
|
||||
|
||||
let previousEntry = undefined;
|
||||
data.root_bookmark.content.forEach(entry => {
|
||||
previousEntry = registerBookmarks(undefined, previousEntry, entry) || previousEntry;
|
||||
});
|
||||
|
||||
this.defaultBookmarkCreated = true;
|
||||
}
|
||||
|
||||
private saveBookmarks() {
|
||||
localStorage.setItem(kLocalStorageKey, JSON.stringify({
|
||||
version: 2,
|
||||
bookmarks: this.registeredBookmarks,
|
||||
defaultBookmarkCreated: this.defaultBookmarkCreated,
|
||||
}))
|
||||
}
|
||||
|
||||
getRegisteredBookmarks() : BookmarkEntry[] {
|
||||
return this.registeredBookmarks;
|
||||
}
|
||||
|
||||
getOrderedRegisteredBookmarks() : OrderedBookmarkEntry[] {
|
||||
const unorderedBookmarks = this.registeredBookmarks.slice(0);
|
||||
const orderedBookmarks: OrderedBookmarkEntry[] = [];
|
||||
|
||||
const orderTreeLayer = (entries: BookmarkEntry[]): BookmarkEntry[] => {
|
||||
if(entries.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result = [];
|
||||
while(entries.length > 0) {
|
||||
let head = entries.find(entry => !entry.previousEntry) || entries[0];
|
||||
while(head) {
|
||||
result.push(head);
|
||||
entries.remove(head);
|
||||
head = entries.find(entry => entry.previousEntry === head.uniqueId);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const traverseTree = (parentEntry: string | undefined, depth: number): number => {
|
||||
const children = unorderedBookmarks.filter(e => e.parentEntry === parentEntry);
|
||||
children.forEach(child => unorderedBookmarks.remove(child));
|
||||
|
||||
const childCount = children.length;
|
||||
for(const entry of orderTreeLayer(children)) {
|
||||
let orderedEntry: OrderedBookmarkEntry = {
|
||||
entry: entry,
|
||||
depth: depth,
|
||||
childCount: 0
|
||||
};
|
||||
|
||||
orderedBookmarks.push(orderedEntry);
|
||||
orderedEntry.childCount = traverseTree(entry.uniqueId, depth + 1);
|
||||
}
|
||||
|
||||
return childCount;
|
||||
};
|
||||
|
||||
traverseTree(undefined, 0);
|
||||
|
||||
/* Append all broken/unreachable elements */
|
||||
while (unorderedBookmarks.length > 0) {
|
||||
traverseTree(unorderedBookmarks[0].parentEntry, 0);
|
||||
}
|
||||
|
||||
return orderedBookmarks;
|
||||
}
|
||||
|
||||
findBookmark(uniqueId: string) : BookmarkEntry | undefined {
|
||||
return this.registeredBookmarks.find(entry => entry.uniqueId === uniqueId);
|
||||
}
|
||||
|
||||
createBookmark(properties: Pick<BookmarkInfo, WritableKeys<BookmarkInfo>>) : BookmarkInfo {
|
||||
this.validateHangInPoint(properties);
|
||||
const bookmark = Object.assign(properties, {
|
||||
uniqueId: guid(),
|
||||
type: "entry"
|
||||
} as BookmarkInfo);
|
||||
this.registeredBookmarks.push(bookmark);
|
||||
this.events.fire("notify_bookmark_created", { bookmark });
|
||||
this.saveBookmarks();
|
||||
return bookmark;
|
||||
}
|
||||
|
||||
editBookmark(uniqueId: string, newValues: Partial<Pick<BookmarkInfo, WritableKeys<BookmarkInfo>>>) {
|
||||
this.doEditBookmark(uniqueId, newValues);
|
||||
}
|
||||
|
||||
createDirectory(properties: Pick<BookmarkInfo, WritableKeys<BookmarkDirectory>>) : BookmarkDirectory {
|
||||
this.validateHangInPoint(properties);
|
||||
const bookmark = Object.assign(properties, {
|
||||
uniqueId: guid(),
|
||||
type: "directory"
|
||||
} as BookmarkDirectory);
|
||||
this.registeredBookmarks.push(bookmark);
|
||||
this.events.fire("notify_bookmark_created", { bookmark });
|
||||
this.saveBookmarks();
|
||||
return bookmark;
|
||||
}
|
||||
|
||||
editDirectory(uniqueId: string, newValues: Partial<Pick<BookmarkDirectory, WritableKeys<BookmarkDirectory>>>) {
|
||||
this.doEditBookmark(uniqueId, newValues);
|
||||
}
|
||||
|
||||
deleteEntry(uniqueId: string) {
|
||||
const index = this.registeredBookmarks.findIndex(entry => entry.uniqueId === uniqueId);
|
||||
if(index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [ entry ] = this.registeredBookmarks.splice(index, 1);
|
||||
const children = [], pendingChildren = [ entry ];
|
||||
|
||||
while(pendingChildren[0]) {
|
||||
const child = pendingChildren.pop_front();
|
||||
children.push(child);
|
||||
|
||||
const childChildren = this.registeredBookmarks.filter(entry => entry.parentEntry === child.uniqueId);
|
||||
pendingChildren.push(...childChildren);
|
||||
childChildren.forEach(entry => this.registeredBookmarks.remove(entry));
|
||||
}
|
||||
|
||||
children.pop_front();
|
||||
this.events.fire("notify_bookmark_deleted", { bookmark: entry, children });
|
||||
this.saveBookmarks();
|
||||
}
|
||||
|
||||
executeConnect(uniqueId: string, newTab: boolean) {
|
||||
const bookmark = this.findBookmark(uniqueId);
|
||||
if(!bookmark || bookmark.type !== "entry") {
|
||||
return;
|
||||
}
|
||||
|
||||
const connection = newTab ? server_connections.spawnConnectionHandler() : server_connections.getActiveConnectionHandler();
|
||||
if(!connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
let profile = findConnectProfile(bookmark.connectProfile) || defaultConnectProfile();
|
||||
connection.startConnectionNew({
|
||||
profile: profile,
|
||||
|
||||
targetAddress: bookmark.serverAddress,
|
||||
|
||||
serverPasswordHashed: true,
|
||||
serverPassword: bookmark.serverPasswordHash,
|
||||
|
||||
defaultChannel: bookmark.defaultChannel,
|
||||
defaultChannelPassword: bookmark.defaultChannelPasswordHash,
|
||||
defaultChannelPasswordHashed: true,
|
||||
|
||||
token: undefined,
|
||||
|
||||
nicknameSpecified: false,
|
||||
nickname: undefined
|
||||
}, false).then(undefined);
|
||||
}
|
||||
|
||||
executeAutoConnect() {
|
||||
let newTab = server_connections.getActiveConnectionHandler().connection_state !== ConnectionState.UNCONNECTED;
|
||||
for(const entry of this.getOrderedRegisteredBookmarks()) {
|
||||
if(entry.entry.type !== "entry") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!entry.entry.connectOnStartup) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.executeConnect(entry.entry.uniqueId, newTab);
|
||||
newTab = true;
|
||||
}
|
||||
}
|
||||
|
||||
exportBookmarks() : string {
|
||||
return JSON.stringify({
|
||||
version: 1,
|
||||
bookmarks: this.registeredBookmarks
|
||||
});
|
||||
}
|
||||
|
||||
importBookmarks(filePayload: string) : number {
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(filePayload)
|
||||
} catch (error) {
|
||||
throw tr("failed to parse bookmarks");
|
||||
}
|
||||
|
||||
if(data?.version !== 1) {
|
||||
throw tr("supplied data contains invalid version");
|
||||
}
|
||||
|
||||
const newBookmarks = data.bookmarks as BookmarkEntry[];
|
||||
if(!Array.isArray(newBookmarks)) {
|
||||
throw tr("missing bookmarks");
|
||||
}
|
||||
|
||||
/* TODO: Validate integrity? */
|
||||
for(const knownBookmark of this.registeredBookmarks) {
|
||||
const index = newBookmarks.findIndex(entry => entry.uniqueId === knownBookmark.uniqueId);
|
||||
if(index === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
newBookmarks.splice(index, 1);
|
||||
}
|
||||
|
||||
if(newBookmarks.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.registeredBookmarks.push(...newBookmarks);
|
||||
newBookmarks.forEach(entry => this.validateHangInPoint(entry));
|
||||
this.events.fire("notify_bookmarks_imported", { bookmarks: newBookmarks });
|
||||
return newBookmarks.length;
|
||||
}
|
||||
|
||||
private doEditBookmark(uniqueId: string, newValues: any) {
|
||||
const bookmarkInfo = this.findBookmark(uniqueId);
|
||||
if(!bookmarkInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const originalProperties = _.cloneDeep(bookmarkInfo);
|
||||
for(const key of Object.keys(newValues)) {
|
||||
bookmarkInfo[key] = newValues[key];
|
||||
}
|
||||
this.validateHangInPoint(bookmarkInfo);
|
||||
|
||||
const editedKeys = [];
|
||||
for(const key of Object.keys(newValues)) {
|
||||
if(_.isEqual(bookmarkInfo[key], originalProperties[key])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
editedKeys.push(key);
|
||||
}
|
||||
|
||||
if(editedKeys.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.saveBookmarks();
|
||||
this.events.fire("notify_bookmark_edited", { bookmark: bookmarkInfo, keys: editedKeys });
|
||||
}
|
||||
|
||||
private validateHangInPoint(entry: Partial<BookmarkBase>) {
|
||||
if(entry.previousEntry) {
|
||||
const previousEntry = this.findBookmark(entry.previousEntry);
|
||||
if(!previousEntry) {
|
||||
logError(LogCategory.BOOKMARKS, tr("New bookmark previous entry does not exists. Clearing it."));
|
||||
entry.previousEntry = undefined;
|
||||
} else if(previousEntry.parentEntry !== entry.parentEntry) {
|
||||
logWarn(LogCategory.BOOKMARKS, tr("Previous entries parent does not match our entries parent. Updating our parent from %s to %s"), entry.parentEntry, previousEntry.parentEntry);
|
||||
entry.parentEntry = previousEntry.parentEntry;
|
||||
}
|
||||
|
||||
|
||||
const openList = this.registeredBookmarks.filter(e1 => e1.parentEntry === entry.parentEntry);
|
||||
let currentEntry = entry;
|
||||
while(true) {
|
||||
if(!currentEntry.previousEntry) {
|
||||
break;
|
||||
}
|
||||
|
||||
const previousEntry = openList.find(entry => entry.uniqueId === currentEntry.previousEntry);
|
||||
if(!previousEntry) {
|
||||
logError(LogCategory.BOOKMARKS, tr("Found circular dependency within the previous entry or one of the previous entries does not exists. Clearing out previous entry."));
|
||||
entry.previousEntry = undefined;
|
||||
break;
|
||||
}
|
||||
|
||||
openList.remove(previousEntry);
|
||||
currentEntry = previousEntry;
|
||||
}
|
||||
}
|
||||
|
||||
if(entry.parentEntry) {
|
||||
const parentEntry = this.findBookmark(entry.parentEntry);
|
||||
if(!parentEntry) {
|
||||
logError(LogCategory.BOOKMARKS, tr("Missing parent entry %s. Clearing it."), entry.parentEntry);
|
||||
entry.parentEntry = undefined;
|
||||
}
|
||||
|
||||
const openList = this.registeredBookmarks.slice();
|
||||
let currentEntry = entry;
|
||||
while(true) {
|
||||
if(!currentEntry.parentEntry) {
|
||||
break;
|
||||
}
|
||||
|
||||
const parentEntry = openList.find(entry => entry.uniqueId === currentEntry.parentEntry);
|
||||
if(!parentEntry) {
|
||||
logError(LogCategory.BOOKMARKS, tr("Found circular dependency within a parent or one of the parents does not exists. Clearing out parent."));
|
||||
entry.parentEntry = undefined;
|
||||
break;
|
||||
}
|
||||
|
||||
openList.remove(parentEntry);
|
||||
currentEntry = parentEntry;
|
||||
}
|
||||
}
|
||||
|
||||
if(entry.previousEntry) {
|
||||
this.registeredBookmarks.forEach(bookmark => {
|
||||
if(bookmark.previousEntry === entry.previousEntry && bookmark !== entry) {
|
||||
bookmark.previousEntry = bookmark.uniqueId;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export let bookmarks: BookmarkManager;
|
||||
|
||||
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||
name: "initialize bookmarks",
|
||||
function: async () => {
|
||||
bookmarks = new BookmarkManager();
|
||||
(window as any).bookmarks = bookmarks;
|
||||
},
|
||||
priority: 20
|
||||
});
|
|
@ -2,9 +2,7 @@ import {CommandResult} from "../connection/ServerConnectionDeclaration";
|
|||
import {IdentitifyType} from "../profiles/Identity";
|
||||
import {AbstractServerConnection} from "../connection/ConnectionBase";
|
||||
import {DisconnectReason} from "../ConnectionHandler";
|
||||
import {tr} from "../i18n/localize";
|
||||
import {ConnectParameters} from "tc-shared/ui/modal/connect/Controller";
|
||||
import {LogCategory, logWarn} from "tc-shared/log";
|
||||
import {getBackend} from "tc-shared/backend";
|
||||
|
||||
export interface HandshakeIdentityHandler {
|
||||
|
|
|
@ -38,6 +38,9 @@ export type ConnectionHistoryServerInfo = {
|
|||
clientsOnline: number | -1,
|
||||
clientsMax: number | -1,
|
||||
|
||||
hostBannerUrl: string | undefined,
|
||||
hostBannerMode: number,
|
||||
|
||||
passwordProtected: boolean
|
||||
}
|
||||
|
||||
|
@ -310,6 +313,9 @@ export class ConnectionHistory {
|
|||
|
||||
databaseValue.clientsOnline = info.clientsOnline;
|
||||
databaseValue.clientsMax = info.clientsMax;
|
||||
|
||||
databaseValue.hostBannerUrl = info.hostBannerUrl
|
||||
databaseValue.hostBannerMode = info.hostBannerMode;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -378,6 +384,9 @@ export class ConnectionHistory {
|
|||
clientsOnline: value.clientsOnline,
|
||||
clientsMax: value.clientsMax,
|
||||
|
||||
hostBannerUrl: value.hostBannerUrl,
|
||||
hostBannerMode: typeof value.hostBannerMode === "number" ? value.hostBannerMode : 0,
|
||||
|
||||
passwordProtected: value.passwordProtected
|
||||
};
|
||||
}
|
||||
|
@ -500,7 +509,9 @@ const kConnectServerInfoUpdatePropertyKeys: (keyof ServerProperties)[] = [
|
|||
"virtualserver_maxclients",
|
||||
"virtualserver_clientsonline",
|
||||
"virtualserver_flag_password",
|
||||
"virtualserver_country_code"
|
||||
"virtualserver_country_code",
|
||||
"virtualserver_hostbanner_gfx_url",
|
||||
"virtualserver_hostbanner_mode"
|
||||
];
|
||||
|
||||
class ConnectionHistoryUpdateListener {
|
||||
|
@ -552,6 +563,9 @@ class ConnectionHistoryUpdateListener {
|
|||
clientsMax: event.server_properties.virtualserver_maxclients,
|
||||
clientsOnline: event.server_properties.virtualserver_clientsonline,
|
||||
|
||||
hostBannerUrl: event.server_properties.virtualserver_hostbanner_gfx_url,
|
||||
hostBannerMode: event.server_properties.virtualserver_hostbanner_mode,
|
||||
|
||||
passwordProtected: event.server_properties.virtualserver_flag_password
|
||||
}).catch(error => {
|
||||
logError(LogCategory.GENERAL, tr("Failed to update connect server info: %o"), error);
|
||||
|
|
|
@ -19,6 +19,7 @@ import {spawnVideoSourceSelectModal} from "tc-shared/ui/modal/video-source/Contr
|
|||
import {LogCategory, logError, logWarn} from "tc-shared/log";
|
||||
import {spawnEchoTestModal} from "tc-shared/ui/modal/echo-test/Controller";
|
||||
import {spawnConnectModalNew} from "tc-shared/ui/modal/connect/Controller";
|
||||
import {spawnBookmarkModal} from "tc-shared/ui/modal/bookmarks/Controller";
|
||||
|
||||
/*
|
||||
function initialize_sounds(event_registry: Registry<ClientGlobalControlEvents>) {
|
||||
|
@ -62,12 +63,7 @@ export function initialize(event_registry: Registry<ClientGlobalControlEvents>)
|
|||
const connection_handler = event.connection || current_connection_handler;
|
||||
switch (event.window) {
|
||||
case "bookmark-manage":
|
||||
import("../ui/modal/ModalBookmarks").catch(error => {
|
||||
handle_import_error(error);
|
||||
return undefined;
|
||||
}).then(window => {
|
||||
window?.spawnBookmarkModal();
|
||||
});
|
||||
spawnBookmarkModal();
|
||||
break;
|
||||
case "query-manage":
|
||||
if(!connection_handler || !connection_handler.connected) {
|
||||
|
|
|
@ -121,6 +121,10 @@ export abstract class AbstractIconManager {
|
|||
return "v2-" + serverUniqueId + "-" + iconId;
|
||||
}
|
||||
|
||||
resolveIconInfo(icon: RemoteIconInfo) : RemoteIcon {
|
||||
return this.resolveIcon(icon.iconId, icon.serverUniqueId, icon.handlerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param iconId The requested icon
|
||||
* @param serverUniqueId The server unique id for the icon
|
||||
|
|
37
shared/js/file/Utils.ts
Normal file
37
shared/js/file/Utils.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
export const downloadTextAsFile = (text: string, name: string) => {
|
||||
const payloadBlob = new Blob([ text ], { type: "text/plain" });
|
||||
const payloadUrl = URL.createObjectURL(payloadBlob);
|
||||
|
||||
const element = document.createElement("a");
|
||||
element.text = "download";
|
||||
element.setAttribute("href", payloadUrl);
|
||||
element.setAttribute("download", name);
|
||||
element.style.display = "none";
|
||||
document.body.appendChild(element);
|
||||
|
||||
element.click();
|
||||
|
||||
setTimeout(() => {
|
||||
element.remove();
|
||||
URL.revokeObjectURL(payloadUrl);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
export const requestFileAsText = async (): Promise<string> => {
|
||||
const element = document.createElement("input");
|
||||
element.style.display = "none";
|
||||
element.type = "file";
|
||||
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
await new Promise(resolve => {
|
||||
element.onchange = resolve;
|
||||
});
|
||||
|
||||
if (element.files.length !== 1)
|
||||
return undefined;
|
||||
const file = element.files[0];
|
||||
element.remove();
|
||||
|
||||
return await file.text();
|
||||
};
|
4
shared/js/global.d.ts
vendored
Normal file
4
shared/js/global.d.ts
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
declare module "*.png" {
|
||||
const value: any;
|
||||
export = value;
|
||||
}
|
|
@ -54,6 +54,9 @@ import {ActionResult} from "tc-services";
|
|||
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
import {ErrorCode} from "tc-shared/connection/ErrorCode";
|
||||
|
||||
import "./Bookmarks";
|
||||
import {bookmarks} from "tc-shared/Bookmarks";
|
||||
|
||||
assertMainApplication();
|
||||
|
||||
let preventWelcomeUI = false;
|
||||
|
@ -487,6 +490,8 @@ const task_connect_handler: loader.Task = {
|
|||
}
|
||||
|
||||
preventWelcomeUI = true;
|
||||
preventExecuteAutoConnect = true;
|
||||
|
||||
loader.register_task(loader.Stage.LOADED, {
|
||||
priority: 0,
|
||||
function: async () => {
|
||||
|
@ -494,6 +499,7 @@ const task_connect_handler: loader.Task = {
|
|||
},
|
||||
name: tr("default url connect")
|
||||
});
|
||||
|
||||
loader.register_task(loader.Stage.LOADED, task_teaweb_starter);
|
||||
},
|
||||
priority: 10
|
||||
|
@ -551,6 +557,19 @@ loader.register_task(loader.Stage.LOADED, {
|
|||
priority: 2000
|
||||
});
|
||||
|
||||
let preventExecuteAutoConnect = false;
|
||||
loader.register_task(loader.Stage.LOADED, {
|
||||
priority: 0,
|
||||
function: async () => {
|
||||
if(preventExecuteAutoConnect) {
|
||||
return;
|
||||
}
|
||||
|
||||
bookmarks.executeAutoConnect();
|
||||
},
|
||||
name: tr("bookmark auto connect")
|
||||
});
|
||||
|
||||
/* TODO: Remove this after the image preview has been rewritten into react */
|
||||
loader.register_task(Stage.JAVASCRIPT_INITIALIZING,{
|
||||
name: "app init",
|
||||
|
|
|
@ -85,6 +85,18 @@ declare global {
|
|||
}
|
||||
}
|
||||
|
||||
export type IfEquals<X, Y, A=X, B=never> =
|
||||
(<T>() => T extends X ? 1 : 2) extends
|
||||
(<T>() => T extends Y ? 1 : 2) ? A : B;
|
||||
|
||||
export type WritableKeys<T> = {
|
||||
[P in keyof T]-?: IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, P, never>
|
||||
}[keyof T];
|
||||
|
||||
export type ReadonlyKeys<T> = {
|
||||
[P in keyof T]: IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, never, P>
|
||||
}[keyof T];
|
||||
|
||||
if(!Object.isSimilar) {
|
||||
Object.isSimilar = function (a, b) {
|
||||
const aType = typeof a;
|
||||
|
@ -293,5 +305,3 @@ if(typeof ($) !== "undefined") {
|
|||
if(!Object.values) {
|
||||
Object.values = object => Object.keys(object).map(e => object[e]);
|
||||
}
|
||||
|
||||
export = {};
|
|
@ -6,10 +6,10 @@ import * as React from "react";
|
|||
import ReactRenderer from "vendor/xbbcode/renderer/react";
|
||||
import {Settings, settings} from "tc-shared/settings";
|
||||
|
||||
import * as emojiRegex from "emoji-regex";
|
||||
import emojiRegex from "emoji-regex";
|
||||
import {getTwenmojiHashFromNativeEmoji} from "tc-shared/text/bbcode/EmojiUtil";
|
||||
|
||||
const emojiRegexInstance = (emojiRegex as any)() as RegExp;
|
||||
const emojiRegexInstance = emojiRegex();
|
||||
|
||||
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||
name: "XBBCode emoji init",
|
||||
|
|
|
@ -15,7 +15,7 @@ import {Settings, settings} from "tc-shared/settings";
|
|||
import {LogCategory, logWarn} from "tc-shared/log";
|
||||
|
||||
const registerLanguage = (name, language: Promise<any>) => {
|
||||
language.then(lan => hljs.registerLanguage(name, lan)).catch(error => {
|
||||
language.then(lan => hljs.registerLanguage(name, lan.default)).catch(error => {
|
||||
logWarn(LogCategory.CHAT, tr("Failed to load language %s (%o)"), name, error);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -4,7 +4,6 @@ import * as contextmenu from "../ui/elements/ContextMenu";
|
|||
import * as log from "../log";
|
||||
import {LogCategory, logInfo, LogType} from "../log";
|
||||
import {Sound} from "../sound/Sounds";
|
||||
import * as bookmarks from "../bookmarks";
|
||||
import {openServerInfo} from "../ui/modal/ModalServerInfo";
|
||||
import {createServerModal} from "../ui/modal/ModalServerEdit";
|
||||
import {spawnIconSelect} from "../ui/modal/ModalIconSelect";
|
||||
|
@ -293,7 +292,6 @@ export class ServerEntry extends ChannelTreeEntry<ServerEvents> {
|
|||
}
|
||||
|
||||
let updatedProperties: Partial<ServerProperties> = {};
|
||||
let update_bookmarks = false;
|
||||
for(let variable of variables) {
|
||||
if(!JSON.map_field_to(this.properties, variable.value, variable.key)) {
|
||||
/* The value has not been updated */
|
||||
|
@ -303,7 +301,6 @@ export class ServerEntry extends ChannelTreeEntry<ServerEvents> {
|
|||
updatedProperties[variable.key] = variable.value;
|
||||
if(variable.key == "virtualserver_icon_id") {
|
||||
this.properties.virtualserver_icon_id = variable.value as any >>> 0;
|
||||
update_bookmarks = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -312,19 +309,6 @@ export class ServerEntry extends ChannelTreeEntry<ServerEvents> {
|
|||
server_properties: this.properties
|
||||
});
|
||||
|
||||
if(update_bookmarks) {
|
||||
const bmarks = bookmarks.bookmarks_flat()
|
||||
.filter(e => e.server_properties.server_address === this.remote_address.host && e.server_properties.server_port == this.remote_address.port)
|
||||
.filter(e => e.last_icon_id !== this.properties.virtualserver_icon_id || e.last_icon_server_id !== this.properties.virtualserver_unique_identifier);
|
||||
if(bmarks.length > 0) {
|
||||
bmarks.forEach(e => {
|
||||
e.last_icon_id = this.properties.virtualserver_icon_id;
|
||||
e.last_icon_server_id = this.properties.virtualserver_unique_identifier;
|
||||
});
|
||||
bookmarks.save_bookmark();
|
||||
}
|
||||
}
|
||||
|
||||
group.end();
|
||||
if(is_self_notify && this.info_request_promise_resolve) {
|
||||
this.info_request_promise_resolve();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as loader from "tc-loader";
|
||||
import {Stage} from "tc-loader";
|
||||
import {KeyCode} from "../../PPTListener";
|
||||
import * as $ from "jquery";
|
||||
import $ from "jquery";
|
||||
import {LogCategory, logError} from "tc-shared/log";
|
||||
|
||||
export enum ElementType {
|
||||
|
@ -304,6 +304,7 @@ export function createModal(data: ModalProperties | any) : Modal {
|
|||
|
||||
export class InputModalProperties extends ModalProperties {
|
||||
maxLength?: number;
|
||||
defaultValue?: string;
|
||||
|
||||
field_title?: string;
|
||||
field_label?: string;
|
||||
|
@ -367,6 +368,11 @@ export function createInputModal(headMessage: BodyCreator, question: BodyCreator
|
|||
modal.close();
|
||||
});
|
||||
|
||||
if(props.defaultValue) {
|
||||
input.val(props.defaultValue);
|
||||
setTimeout(() => input.trigger("change"), 0);
|
||||
}
|
||||
|
||||
modal.open_listener.push(() => input.focus());
|
||||
modal.close_listener.push(() => button_cancel.trigger('click'));
|
||||
return modal;
|
||||
|
|
|
@ -8,7 +8,7 @@ import {Settings} from "tc-shared/settings";
|
|||
|
||||
const cssStyle = require("./HostBannerRenderer.scss");
|
||||
|
||||
const HostBannerRenderer = React.memo((props: { banner: HostBannerInfoSet, }) => {
|
||||
export const HostBannerRenderer = React.memo((props: { banner: HostBannerInfoSet, className?: string }) => {
|
||||
const [ revision, setRevision ] = useState(Date.now());
|
||||
useEffect(() => {
|
||||
if(!props.banner.updateInterval) {
|
||||
|
@ -35,7 +35,7 @@ const HostBannerRenderer = React.memo((props: { banner: HostBannerInfoSet, }) =>
|
|||
<div
|
||||
className={
|
||||
cssStyle.containerImage + " " + cssStyle["mode-" + props.banner.mode] + " " + cssStyle["state-" + loadingState] + " " +
|
||||
(withBackground ? cssStyle.withBackground : "")
|
||||
(withBackground ? cssStyle.withBackground : "") + " " + props.className
|
||||
}
|
||||
onClick={() => {
|
||||
if(props.banner.linkUrl) {
|
||||
|
|
|
@ -155,10 +155,10 @@ html:root {
|
|||
border: .05em solid var(--menu-bar-dropdown-border);
|
||||
border-radius: 0 $border_radius_middle $border_radius_middle $border_radius_middle;
|
||||
|
||||
width: 15em; /* fallback */
|
||||
width: 20em; /* fallback */
|
||||
width: max-content;
|
||||
|
||||
max-width: 25em;
|
||||
max-width: 30em;
|
||||
|
||||
z-index: 1000;
|
||||
/*box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);*/
|
||||
|
@ -270,12 +270,6 @@ html:root {
|
|||
}
|
||||
}
|
||||
|
||||
.buttonBookmarks {
|
||||
.dropdown {
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.arrow {
|
||||
display: inline-block;
|
||||
border: solid black;
|
||||
|
|
|
@ -11,17 +11,6 @@ import {server_connections} from "tc-shared/ConnectionManager";
|
|||
import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler";
|
||||
import {Settings, settings} from "tc-shared/settings";
|
||||
import {global_client_actions} from "tc-shared/events/GlobalEvents";
|
||||
import {
|
||||
add_server_to_bookmarks,
|
||||
Bookmark as ServerBookmark,
|
||||
bookmarkEvents,
|
||||
bookmarks,
|
||||
bookmarks_flat,
|
||||
BookmarkType,
|
||||
boorkmak_connect,
|
||||
DirectoryBookmark
|
||||
} from "tc-shared/bookmarks";
|
||||
import {LogCategory, logWarn} from "tc-shared/log";
|
||||
import {createErrorModal, createInputModal} from "tc-shared/ui/elements/Modal";
|
||||
import {VideoBroadcastType, VideoConnectionStatus} from "tc-shared/connection/VideoConnection";
|
||||
import {tr} from "tc-shared/i18n/localize";
|
||||
|
@ -29,6 +18,10 @@ import {getVideoDriver} from "tc-shared/video/VideoSource";
|
|||
import {kLocalBroadcastChannels} from "tc-shared/ui/frames/video/Definitions";
|
||||
import {getRecorderBackend, IDevice} from "tc-shared/audio/recorder";
|
||||
import {defaultRecorder, defaultRecorderEvents} from "tc-shared/voice/RecorderProfile";
|
||||
import {bookmarks} from "tc-shared/Bookmarks";
|
||||
import {connectionHistory} from "tc-shared/connectionlog/History";
|
||||
import {RemoteIconInfo} from "tc-shared/file/Icons";
|
||||
import {spawnModalAddCurrentServerToBookmarks} from "tc-shared/ui/modal/bookmarks-add-server/Controller";
|
||||
|
||||
class InfoController {
|
||||
private readonly mode: ControlBarMode;
|
||||
|
@ -66,7 +59,7 @@ class InfoController {
|
|||
this.sendVideoState("screen");
|
||||
this.sendVideoState("camera");
|
||||
}));
|
||||
events.push(bookmarkEvents.on("notify_bookmarks_updated", () => this.sendBookmarks()));
|
||||
bookmarks.events.on(["notify_bookmark_edited", "notify_bookmark_created", "notify_bookmark_deleted", "notify_bookmarks_imported"], () => this.sendBookmarks());
|
||||
events.push(getVideoDriver().getEvents().on("notify_device_list_changed", () => this.sendCameraList()));
|
||||
events.push(getRecorderBackend().getDeviceList().getEvents().on("notify_list_updated", () => this.sendMicrophoneList()));
|
||||
events.push(defaultRecorderEvents.on("notify_default_recorder_changed", () => {
|
||||
|
@ -205,25 +198,54 @@ class InfoController {
|
|||
});
|
||||
}
|
||||
|
||||
public sendBookmarks() {
|
||||
const buildInfo = (bookmark: DirectoryBookmark | ServerBookmark) => {
|
||||
if(bookmark.type === BookmarkType.DIRECTORY) {
|
||||
return {
|
||||
uniqueId: bookmark.unique_id,
|
||||
label: bookmark.display_name,
|
||||
children: bookmark.content.map(buildInfo)
|
||||
} as Bookmark;
|
||||
} else {
|
||||
return {
|
||||
uniqueId: bookmark.unique_id,
|
||||
label: bookmark.display_name,
|
||||
icon: bookmark.last_icon_id ? { iconId: bookmark.last_icon_id, serverUniqueId: bookmark.last_icon_server_id } : undefined
|
||||
} as Bookmark;
|
||||
public async sendBookmarks() {
|
||||
const bookmarkList = bookmarks.getOrderedRegisteredBookmarks();
|
||||
|
||||
const parent: Bookmark[] = [];
|
||||
const parentStack: Bookmark[][] = [];
|
||||
|
||||
while(bookmarkList.length > 0) {
|
||||
const bookmark = bookmarkList.pop_front();
|
||||
const parentList = parentStack.pop() || parent;
|
||||
|
||||
if(bookmark.entry.type === "entry") {
|
||||
let icon: RemoteIconInfo;
|
||||
|
||||
try {
|
||||
const connectInfo = await connectionHistory.lastConnectInfo(bookmark.entry.serverAddress, "address");
|
||||
if(connectInfo) {
|
||||
const info = await connectionHistory.queryServerInfo(connectInfo.serverUniqueId);
|
||||
if(info && info.iconId > 0) {
|
||||
icon = { iconId: info.iconId, serverUniqueId: connectInfo.serverUniqueId };
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
/* no need for any error handling */
|
||||
}
|
||||
|
||||
parentList.push({
|
||||
children: undefined,
|
||||
icon: icon,
|
||||
label: bookmark.entry.displayName,
|
||||
uniqueId: bookmark.entry.uniqueId
|
||||
});
|
||||
} else if(bookmark.entry.type === "directory") {
|
||||
const children = [];
|
||||
parentList.push({
|
||||
children: children,
|
||||
icon: undefined,
|
||||
label: bookmark.entry.displayName,
|
||||
uniqueId: bookmark.entry.uniqueId
|
||||
});
|
||||
|
||||
for(let i = 0; i < bookmark.childCount; i++) {
|
||||
parentStack.push(children);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
this.events.fire_react("notify_bookmarks", {
|
||||
marks: bookmarks().content.map(buildInfo)
|
||||
marks: parent
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -380,16 +402,8 @@ export function initializeControlBarController(events: Registry<ControlBarEvents
|
|||
});
|
||||
|
||||
events.on("action_bookmark_manage", () => global_client_actions.fire("action_open_window", { window: "bookmark-manage" }));
|
||||
events.on("action_bookmark_add_current_server", () => add_server_to_bookmarks(infoHandler.getCurrentHandler()));
|
||||
events.on("action_bookmark_connect", event => {
|
||||
const bookmark = bookmarks_flat().find(mark => mark.unique_id === event.bookmarkUniqueId);
|
||||
if(!bookmark) {
|
||||
logWarn(LogCategory.BOOKMARKS, tr("Tried to connect to a non existing bookmark with id %s"), event.bookmarkUniqueId);
|
||||
return;
|
||||
}
|
||||
|
||||
boorkmak_connect(bookmark, event.newTab);
|
||||
});
|
||||
events.on("action_bookmark_add_current_server", () => spawnModalAddCurrentServerToBookmarks(infoHandler.getCurrentHandler()));
|
||||
events.on("action_bookmark_connect", event => bookmarks.executeConnect(event.bookmarkUniqueId, event.newTab));
|
||||
|
||||
events.on("action_toggle_away", event => {
|
||||
if(event.away) {
|
||||
|
|
|
@ -5,17 +5,12 @@ import {ClientIcon} from "svg-sprites/client-icons";
|
|||
import {global_client_actions} from "tc-shared/events/GlobalEvents";
|
||||
import {server_connections} from "tc-shared/ConnectionManager";
|
||||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||
import {
|
||||
add_server_to_bookmarks,
|
||||
Bookmark,
|
||||
bookmarkEvents,
|
||||
bookmarks,
|
||||
BookmarkType,
|
||||
boorkmak_connect,
|
||||
DirectoryBookmark
|
||||
} from "tc-shared/bookmarks";
|
||||
import {getBackend} from "tc-shared/backend";
|
||||
import {tr} from "tc-shared/i18n/localize";
|
||||
import {bookmarks} from "tc-shared/Bookmarks";
|
||||
import {RemoteIconInfo} from "tc-shared/file/Icons";
|
||||
import {connectionHistory} from "tc-shared/connectionlog/History";
|
||||
import {spawnModalAddCurrentServerToBookmarks} from "tc-shared/ui/modal/bookmarks-add-server/Controller";
|
||||
|
||||
function renderConnectionItems() {
|
||||
const items: MenuBarEntry[] = [];
|
||||
|
@ -57,27 +52,54 @@ function renderConnectionItems() {
|
|||
return items;
|
||||
}
|
||||
|
||||
function renderBookmarkItems() {
|
||||
const items: MenuBarEntry[] = [];
|
||||
async function renderBookmarkItems() {
|
||||
const bookmarkList = bookmarks.getOrderedRegisteredBookmarks();
|
||||
|
||||
const renderBookmark = (bookmark: Bookmark | DirectoryBookmark): MenuBarEntry => {
|
||||
if(bookmark.type === BookmarkType.ENTRY) {
|
||||
return {
|
||||
const bookmarkItems: MenuBarEntry[] = [];
|
||||
const parentStack: MenuBarEntry[][] = [];
|
||||
|
||||
while(bookmarkList.length > 0) {
|
||||
const bookmark = bookmarkList.pop_front();
|
||||
const parentList = parentStack.pop() || bookmarkItems;
|
||||
|
||||
if(bookmark.entry.type === "entry") {
|
||||
let icon: RemoteIconInfo;
|
||||
|
||||
try {
|
||||
const connectInfo = await connectionHistory.lastConnectInfo(bookmark.entry.serverAddress, "address");
|
||||
if(connectInfo) {
|
||||
const info = await connectionHistory.queryServerInfo(connectInfo.serverUniqueId);
|
||||
if(info && info.iconId > 0) {
|
||||
icon = { iconId: info.iconId, serverUniqueId: connectInfo.serverUniqueId };
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
/* no need for any error handling */
|
||||
}
|
||||
|
||||
parentList.push({
|
||||
type: "normal",
|
||||
label: bookmark.display_name,
|
||||
click: () => boorkmak_connect(bookmark),
|
||||
icon: bookmark.last_icon_id ? { serverUniqueId: bookmark.last_icon_server_id, iconId: bookmark.last_icon_id } : undefined
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
label: bookmark.entry.displayName,
|
||||
icon: icon,
|
||||
click: () => bookmarks.executeConnect(bookmark.entry.uniqueId, false),
|
||||
});
|
||||
} else if(bookmark.entry.type === "directory") {
|
||||
const children = [];
|
||||
|
||||
parentList.push({
|
||||
type: "normal",
|
||||
label: bookmark.display_name,
|
||||
label: bookmark.entry.displayName,
|
||||
children: children,
|
||||
icon: ClientIcon.Folder,
|
||||
children: bookmark.content.map(renderBookmark)
|
||||
});
|
||||
|
||||
for(let i = 0; i < bookmark.childCount; i++) {
|
||||
parentStack.push(children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const items: MenuBarEntry[] = [];
|
||||
items.push({
|
||||
type: "normal",
|
||||
icon: ClientIcon.BookmarkManager,
|
||||
|
@ -90,13 +112,12 @@ function renderBookmarkItems() {
|
|||
icon: ClientIcon.BookmarkAdd,
|
||||
label: tr("Add current server to bookmarks"),
|
||||
disabled: !server_connections.getActiveConnectionHandler()?.connected,
|
||||
click: () => add_server_to_bookmarks(server_connections.getActiveConnectionHandler())
|
||||
click: () => spawnModalAddCurrentServerToBookmarks(server_connections.getActiveConnectionHandler())
|
||||
});
|
||||
|
||||
const rootMarks = bookmarks().content;
|
||||
if(rootMarks.length !== 0) {
|
||||
if(bookmarkItems.length !== 0) {
|
||||
items.push({ type: "separator" });
|
||||
items.push(...rootMarks.map(renderBookmark));
|
||||
items.push(...bookmarkItems);
|
||||
}
|
||||
|
||||
return items;
|
||||
|
@ -286,6 +307,11 @@ function renderHelpItems() : MenuBarEntry[] {
|
|||
}
|
||||
|
||||
function updateMenuBar() {
|
||||
/* TODO: Only run one update per time */
|
||||
doUpdateMenuBar().then(undefined);
|
||||
}
|
||||
|
||||
async function doUpdateMenuBar() {
|
||||
const items: MenuBarEntry[] = [];
|
||||
|
||||
items.push({
|
||||
|
@ -297,7 +323,7 @@ function updateMenuBar() {
|
|||
items.push({
|
||||
type: "normal",
|
||||
label: tr("Favorites"),
|
||||
children: renderBookmarkItems()
|
||||
children: await renderBookmarkItems()
|
||||
});
|
||||
|
||||
items.push({
|
||||
|
@ -335,12 +361,8 @@ class MenuBarUpdateListener {
|
|||
this.registeredHandlerEvents[event.handlerId]?.forEach(callback => callback());
|
||||
delete this.registeredHandlerEvents[event.handlerId];
|
||||
}));
|
||||
this.generalHandlerEvents.push(server_connections.events().on("notify_active_handler_changed", () => {
|
||||
updateMenuBar();
|
||||
}));
|
||||
this.generalHandlerEvents.push(bookmarkEvents.on("notify_bookmarks_updated", () => {
|
||||
updateMenuBar();
|
||||
}))
|
||||
this.generalHandlerEvents.push(server_connections.events().on("notify_active_handler_changed", () => updateMenuBar()));
|
||||
this.generalHandlerEvents.push(bookmarks.events.on(["notify_bookmark_deleted", "notify_bookmark_created", "notify_bookmark_edited", "notify_bookmarks_imported"], () => updateMenuBar()));
|
||||
server_connections.getAllConnectionHandlers().forEach(handler => this.registerHandlerEvents(handler));
|
||||
}
|
||||
|
||||
|
@ -361,12 +383,14 @@ class MenuBarUpdateListener {
|
|||
}
|
||||
|
||||
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||
name: "menu bar entries init",
|
||||
name: "menu bar init",
|
||||
function: async () => {
|
||||
updateMenuBar();
|
||||
|
||||
updateListener = new MenuBarUpdateListener();
|
||||
updateListener.initializeListeners();
|
||||
},
|
||||
priority: 50
|
||||
|
||||
/* initialize after all other systems have been initialized */
|
||||
priority: 0
|
||||
});
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
|||
import {useContext, useMemo, useRef, useState} from "react";
|
||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {Layout} from "react-grid-layout";
|
||||
import * as GridLayout from "react-grid-layout";
|
||||
import GridLayout from "react-grid-layout";
|
||||
import {ErrorBoundary} from "tc-shared/ui/react-elements/ErrorBoundary";
|
||||
import * as _ from "lodash";
|
||||
import {FontSizeObserver} from "tc-shared/ui/react-elements/FontSize";
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as loader from "tc-loader";
|
||||
import * as moment from "moment";
|
||||
import moment from "moment";
|
||||
import {LogCategory, logError, logTrace} from "../log";
|
||||
import {tr} from "tc-shared/i18n/localize";
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import {ConnectionHandler} from "../../ConnectionHandler";
|
|||
import {base64_encode_ab} from "../../utils/buffers";
|
||||
import {spawnYesNo} from "../../ui/modal/ModalYesNo";
|
||||
import {ClientEntry} from "../../tree/Client";
|
||||
import * as moment from "moment";
|
||||
import moment from "moment";
|
||||
import {tr} from "tc-shared/i18n/localize";
|
||||
|
||||
const avatar_to_uid = (id: string) => {
|
||||
|
|
|
@ -7,7 +7,7 @@ import {LogCategory, logError, logInfo} from "../../log";
|
|||
import * as tooltip from "../../ui/elements/Tooltip";
|
||||
import * as htmltags from "../../ui/htmltags";
|
||||
import {format_time, formatMessage} from "../../ui/frames/chat";
|
||||
import * as moment from "moment";
|
||||
import moment from "moment";
|
||||
import {ErrorCode} from "../../connection/ErrorCode";
|
||||
import {tr} from "tc-shared/i18n/localize";
|
||||
|
||||
|
|
|
@ -1,421 +0,0 @@
|
|||
import {createInputModal, createModal, Modal} from "../../ui/elements/Modal";
|
||||
import {
|
||||
Bookmark,
|
||||
bookmarks,
|
||||
BookmarkType,
|
||||
boorkmak_connect,
|
||||
create_bookmark,
|
||||
create_bookmark_directory,
|
||||
delete_bookmark,
|
||||
DirectoryBookmark,
|
||||
save_bookmark
|
||||
} from "../../bookmarks";
|
||||
import {Regex} from "../../ui/modal/ModalConnect";
|
||||
import {availableConnectProfiles} from "../../profiles/ConnectionProfile";
|
||||
import {spawnYesNo} from "../../ui/modal/ModalYesNo";
|
||||
import {Settings, settings} from "../../settings";
|
||||
import {LogCategory, logWarn} from "../../log";
|
||||
import * as i18nc from "../../i18n/country";
|
||||
import {formatMessage} from "../../ui/frames/chat";
|
||||
import {generateIconJQueryTag, getIconManager} from "tc-shared/file/Icons";
|
||||
import {tr} from "tc-shared/i18n/localize";
|
||||
import {connectionHistory} from "tc-shared/connectionlog/History";
|
||||
|
||||
export function spawnBookmarkModal() {
|
||||
let modal: Modal;
|
||||
modal = createModal({
|
||||
header: tr("Manage bookmarks"),
|
||||
body: () => {
|
||||
let template = $("#tmpl_manage_bookmarks").renderTag({});
|
||||
let selected_bookmark: Bookmark | DirectoryBookmark | undefined;
|
||||
|
||||
const button_delete = template.find(".button-delete");
|
||||
const button_add_folder = template.find(".button-add-folder");
|
||||
const button_add_bookmark = template.find(".button-add-bookmark");
|
||||
|
||||
const button_duplicate = template.find(".button-duplicate");
|
||||
const button_connect = template.find(".button-connect");
|
||||
const button_connect_tab = template.find(".button-connect-tab");
|
||||
|
||||
const label_bookmark_name = template.find(".header .container-name");
|
||||
const label_server_address = template.find(".header .container-address");
|
||||
|
||||
const input_bookmark_name = template.find(".input-bookmark-name");
|
||||
const input_connect_profile = template.find(".input-connect-profile");
|
||||
|
||||
const input_server_address = template.find(".input-server-address");
|
||||
const input_server_password = template.find(".input-server-password");
|
||||
|
||||
const label_server_name = template.find(".server-name");
|
||||
const label_server_region = template.find(".server-region");
|
||||
const label_last_ping = template.find(".server-ping");
|
||||
const label_client_count = template.find(".server-client-count");
|
||||
const label_connection_count = template.find(".server-connection-count");
|
||||
|
||||
const update_buttons = () => {
|
||||
button_delete.prop("disabled", !selected_bookmark);
|
||||
button_connect.prop("disabled", !selected_bookmark || selected_bookmark.type !== BookmarkType.ENTRY);
|
||||
button_connect_tab.prop("disabled", !selected_bookmark || selected_bookmark.type !== BookmarkType.ENTRY);
|
||||
button_duplicate.prop("disabled", !selected_bookmark || selected_bookmark.type !== BookmarkType.ENTRY);
|
||||
};
|
||||
|
||||
const update_connect_info = () => {
|
||||
if (selected_bookmark && selected_bookmark.type === BookmarkType.ENTRY) {
|
||||
const entry = selected_bookmark as Bookmark;
|
||||
|
||||
label_server_name.text(tr("Unknown"));
|
||||
label_server_region.empty().text(tr("Unknown"));
|
||||
label_client_count.text(tr("Unknown"));
|
||||
label_connection_count.empty().append(
|
||||
...formatMessage(tr("You {} connected to that server address"), $.spawn("div").addClass("connect-never").text("never"))
|
||||
);
|
||||
|
||||
const targetAddress = entry.server_properties.server_address + (entry.server_properties.server_port === 9987 ? "" : ":" + entry.server_properties.server_port);
|
||||
connectionHistory.lastConnectInfo(targetAddress, "address", true).then(async connectionInfo => {
|
||||
if(!connectionInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const info = await connectionHistory.queryServerInfo(connectionInfo.serverUniqueId);
|
||||
if(info) {
|
||||
label_server_name.text(info.name);
|
||||
label_server_region.empty().append(
|
||||
$.spawn("div").addClass("country flag-" + (info.country || "xx").toLowerCase()),
|
||||
$.spawn("div").text(i18nc.country_name(info.country || "xx", tr("Global")))
|
||||
);
|
||||
label_client_count.text(info.clientsOnline + "/" + info.clientsMax);
|
||||
}
|
||||
|
||||
{
|
||||
const count = await connectionHistory.countConnectCount(targetAddress, "address");
|
||||
if(count > 0) {
|
||||
label_connection_count.empty().append(
|
||||
...formatMessage(tr("You've connected {} times"), $.spawn("div").addClass("connect-count").text(count))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
label_last_ping.text(tr("Average ping isn't yet supported"));
|
||||
} else {
|
||||
label_server_name.text("--");
|
||||
label_server_region.text("--");
|
||||
label_last_ping.text("--");
|
||||
label_client_count.text("--");
|
||||
label_connection_count.text("--");
|
||||
}
|
||||
};
|
||||
|
||||
const update_selected = () => {
|
||||
input_bookmark_name.prop("disabled", !selected_bookmark);
|
||||
input_connect_profile.prop("disabled", !selected_bookmark || selected_bookmark.type !== BookmarkType.ENTRY);
|
||||
input_server_address.prop("disabled", !selected_bookmark || selected_bookmark.type !== BookmarkType.ENTRY);
|
||||
input_server_password.prop("disabled", !selected_bookmark || selected_bookmark.type !== BookmarkType.ENTRY);
|
||||
|
||||
if (selected_bookmark) {
|
||||
input_bookmark_name.val(selected_bookmark.display_name);
|
||||
label_bookmark_name.text(selected_bookmark.display_name);
|
||||
}
|
||||
|
||||
if (selected_bookmark && selected_bookmark.type === BookmarkType.ENTRY) {
|
||||
const entry = selected_bookmark as Bookmark;
|
||||
|
||||
const address = entry.server_properties.server_address + (entry.server_properties.server_port == 9987 ? "" : (":" + entry.server_properties.server_port));
|
||||
label_server_address.text(address);
|
||||
input_server_address.val(address);
|
||||
|
||||
let profile = input_connect_profile.find("option[value='" + entry.connect_profile + "']");
|
||||
if (profile.length == 0) {
|
||||
logWarn(LogCategory.GENERAL, tr("Failed to find bookmark profile %s. Displaying default one."), entry.connect_profile);
|
||||
profile = input_connect_profile.find("option[value=default]");
|
||||
}
|
||||
profile.prop("selected", true);
|
||||
|
||||
input_server_password.val(entry.server_properties.server_password_hash || entry.server_properties.server_password ? "WolverinDEV" : "");
|
||||
} else {
|
||||
input_server_password.val("");
|
||||
input_server_address.val("");
|
||||
input_connect_profile.find("option[value='no-value']").prop('selected', true);
|
||||
label_server_address.text(" ");
|
||||
}
|
||||
|
||||
update_connect_info();
|
||||
};
|
||||
|
||||
const container_bookmarks = template.find(".container-bookmarks");
|
||||
const update_bookmark_list = (_current_selected: string) => {
|
||||
container_bookmarks.empty();
|
||||
selected_bookmark = undefined;
|
||||
update_selected();
|
||||
|
||||
const hide_links: boolean[] = [];
|
||||
const build_entry = (entry: Bookmark | DirectoryBookmark, sibling_data: { first: boolean; last: boolean; }, index: number) => {
|
||||
let container = $.spawn("div")
|
||||
.addClass(entry.type === BookmarkType.ENTRY ? "bookmark" : "directory")
|
||||
.addClass(index > 0 ? "linked" : "")
|
||||
.addClass(sibling_data.first ? "link-start" : "");
|
||||
for (let i = 0; i < index; i++) {
|
||||
container.append(
|
||||
$.spawn("div")
|
||||
.addClass("link")
|
||||
.addClass(i + 1 === index ? " connected" : "")
|
||||
.addClass(hide_links[i + 1] ? "hidden" : "")
|
||||
);
|
||||
}
|
||||
|
||||
if (entry.type === BookmarkType.ENTRY) {
|
||||
const bookmark = entry as Bookmark;
|
||||
container.append(
|
||||
bookmark.last_icon_id && bookmark.last_icon_server_id ?
|
||||
generateIconJQueryTag(getIconManager().resolveIcon(bookmark.last_icon_id, bookmark.last_icon_server_id), {animate: false}) :
|
||||
$.spawn("div").addClass("icon-container icon_em")
|
||||
);
|
||||
} else {
|
||||
container.append(
|
||||
$.spawn("div").addClass("icon-container icon_em client-folder")
|
||||
);
|
||||
}
|
||||
|
||||
container.append(
|
||||
$.spawn("div").addClass("name").attr("title", entry.display_name).text(entry.display_name)
|
||||
);
|
||||
|
||||
container.appendTo(container_bookmarks);
|
||||
container.on('click', () => {
|
||||
if (selected_bookmark === entry)
|
||||
return;
|
||||
|
||||
selected_bookmark = entry;
|
||||
container_bookmarks.find(".selected").removeClass("selected");
|
||||
container.addClass("selected");
|
||||
update_buttons();
|
||||
update_selected();
|
||||
});
|
||||
if (entry.unique_id === _current_selected)
|
||||
container.trigger('click');
|
||||
|
||||
hide_links.push(sibling_data.last);
|
||||
let cindex = 0;
|
||||
const children = (entry as DirectoryBookmark).content || [];
|
||||
for (const child of children)
|
||||
build_entry(child, {first: cindex++ == 0, last: cindex == children.length}, index + 1);
|
||||
hide_links.pop();
|
||||
};
|
||||
|
||||
let cindex = 0;
|
||||
const children = bookmarks().content;
|
||||
for (const bookmark of children)
|
||||
build_entry(bookmark, {first: cindex++ == 0, last: cindex == children.length}, 0);
|
||||
};
|
||||
|
||||
/* generate profile list */
|
||||
{
|
||||
input_connect_profile.append(
|
||||
$.spawn("option")
|
||||
.attr("value", "no-value")
|
||||
.text("")
|
||||
.css("display", "none")
|
||||
);
|
||||
for (const profile of availableConnectProfiles()) {
|
||||
input_connect_profile.append(
|
||||
$.spawn("option")
|
||||
.attr("value", profile.id)
|
||||
.text(profile.profileName)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* buttons */
|
||||
{
|
||||
button_delete.on('click', () => {
|
||||
if (!selected_bookmark) return;
|
||||
|
||||
if (selected_bookmark.type === BookmarkType.DIRECTORY && (selected_bookmark as DirectoryBookmark).content.length > 0) {
|
||||
spawnYesNo(tr("Are you sure"), tr("Do you really want to delete this non empty directory?"), answer => {
|
||||
if (answer) {
|
||||
delete_bookmark(selected_bookmark);
|
||||
save_bookmark(selected_bookmark);
|
||||
update_bookmark_list(undefined);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
delete_bookmark(selected_bookmark);
|
||||
save_bookmark(selected_bookmark);
|
||||
update_bookmark_list(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
button_add_folder.on('click', () => {
|
||||
createInputModal(tr("Enter a folder name"), tr("Enter the folder name"), () => {
|
||||
return true;
|
||||
}, result => {
|
||||
if (result) {
|
||||
const mark = create_bookmark_directory(
|
||||
selected_bookmark ?
|
||||
selected_bookmark.type === BookmarkType.DIRECTORY ?
|
||||
selected_bookmark as DirectoryBookmark :
|
||||
selected_bookmark.parent :
|
||||
bookmarks(),
|
||||
result as string
|
||||
);
|
||||
save_bookmark(mark);
|
||||
update_bookmark_list(mark.unique_id);
|
||||
}
|
||||
}).open();
|
||||
});
|
||||
|
||||
button_add_bookmark.on('click', () => {
|
||||
createInputModal(tr("Enter a bookmark name"), tr("Enter the bookmark name"), () => {
|
||||
return true;
|
||||
}, result => {
|
||||
if (result) {
|
||||
const mark = create_bookmark(result as string,
|
||||
selected_bookmark ?
|
||||
selected_bookmark.type === BookmarkType.DIRECTORY ?
|
||||
selected_bookmark as DirectoryBookmark :
|
||||
selected_bookmark.parent :
|
||||
bookmarks(), {
|
||||
server_password: "",
|
||||
server_port: 9987,
|
||||
server_address: "",
|
||||
server_password_hash: ""
|
||||
}, "");
|
||||
save_bookmark(mark);
|
||||
update_bookmark_list(mark.unique_id);
|
||||
}
|
||||
}).open();
|
||||
});
|
||||
|
||||
button_connect_tab.on('click', () => {
|
||||
boorkmak_connect(selected_bookmark as Bookmark, true);
|
||||
modal.close();
|
||||
}).toggle(!settings.getValue(Settings.KEY_DISABLE_MULTI_SESSION));
|
||||
|
||||
button_connect.on('click', () => {
|
||||
boorkmak_connect(selected_bookmark as Bookmark, false);
|
||||
modal.close();
|
||||
});
|
||||
|
||||
button_duplicate.on('click', () => {
|
||||
createInputModal(tr("Enter a bookmark name"), tr("Enter the bookmark name for the duplicate"), text => text.length > 0, result => {
|
||||
if (result) {
|
||||
if (!selected_bookmark) return;
|
||||
|
||||
const original = selected_bookmark as Bookmark;
|
||||
const mark = create_bookmark(result as string,
|
||||
selected_bookmark ?
|
||||
selected_bookmark.parent :
|
||||
bookmarks(), original.server_properties, original.nickname);
|
||||
save_bookmark(mark);
|
||||
update_bookmark_list(mark.unique_id);
|
||||
}
|
||||
}).open();
|
||||
});
|
||||
}
|
||||
|
||||
/* inputs */
|
||||
{
|
||||
input_bookmark_name.on('change keydown', event => {
|
||||
const name = input_bookmark_name.val() as string;
|
||||
const valid = name.length > 3;
|
||||
input_bookmark_name.firstParent(".input-boxed").toggleClass("is-invalid", !valid);
|
||||
|
||||
if (event.type === "change" && valid) {
|
||||
selected_bookmark.display_name = name;
|
||||
label_bookmark_name.text(name);
|
||||
save_bookmark(selected_bookmark);
|
||||
}
|
||||
});
|
||||
|
||||
input_server_address.on('change keyup', () => {
|
||||
const address = input_server_address.val() as string;
|
||||
const valid = !!address.match(Regex.IP_V4) || !!address.match(Regex.IP_V6) || !!address.match(Regex.DOMAIN);
|
||||
input_server_address.firstParent(".input-boxed").toggleClass("is-invalid", !valid);
|
||||
|
||||
if (valid) {
|
||||
const entry = selected_bookmark as Bookmark;
|
||||
let _v6_end = address.indexOf(']');
|
||||
let idx = address.lastIndexOf(':');
|
||||
if (idx != -1 && idx > _v6_end) {
|
||||
entry.server_properties.server_port = parseInt(address.substr(idx + 1));
|
||||
entry.server_properties.server_address = address.substr(0, idx);
|
||||
} else {
|
||||
entry.server_properties.server_address = address;
|
||||
entry.server_properties.server_port = 9987;
|
||||
}
|
||||
save_bookmark(selected_bookmark);
|
||||
|
||||
label_server_address.text(entry.server_properties.server_address + (entry.server_properties.server_port == 9987 ? "" : (":" + entry.server_properties.server_port)));
|
||||
update_connect_info();
|
||||
}
|
||||
});
|
||||
|
||||
input_server_password.on("change keydown", () => {
|
||||
const password = input_server_password.val() as string;
|
||||
(selected_bookmark as Bookmark).server_properties.server_password = password;
|
||||
save_bookmark(selected_bookmark);
|
||||
});
|
||||
|
||||
input_connect_profile.on('change', () => {
|
||||
const id = input_connect_profile.val() as string;
|
||||
const profile = availableConnectProfiles().find(e => e.id === id);
|
||||
if (profile) {
|
||||
(selected_bookmark as Bookmark).connect_profile = id;
|
||||
save_bookmark(selected_bookmark);
|
||||
} else {
|
||||
logWarn(LogCategory.GENERAL, tr("Failed to change connect profile for profile %s to %s"), selected_bookmark.unique_id, id);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/* Arrow key navigation for the bookmark list */
|
||||
{
|
||||
let _focused = false;
|
||||
let _focus_listener;
|
||||
let _key_listener;
|
||||
|
||||
_focus_listener = event => {
|
||||
_focused = false;
|
||||
let element = event.target as HTMLElement;
|
||||
while (element) {
|
||||
if (element === container_bookmarks[0]) {
|
||||
_focused = true;
|
||||
break;
|
||||
}
|
||||
element = element.parentNode as HTMLElement;
|
||||
}
|
||||
};
|
||||
|
||||
_key_listener = event => {
|
||||
if (!_focused) return;
|
||||
|
||||
if (event.key.toLowerCase() === "arrowdown") {
|
||||
container_bookmarks.find(".selected").next().trigger('click');
|
||||
} else if (event.key.toLowerCase() === "arrowup") {
|
||||
container_bookmarks.find(".selected").prev().trigger('click');
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', _focus_listener);
|
||||
document.addEventListener('keydown', _key_listener);
|
||||
modal.close_listener.push(() => {
|
||||
document.removeEventListener('click', _focus_listener);
|
||||
document.removeEventListener('keydown', _key_listener);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
update_bookmark_list(undefined);
|
||||
update_buttons();
|
||||
|
||||
template.find(".button-close").on('click', () => modal.close());
|
||||
return template.children();
|
||||
},
|
||||
footer: undefined,
|
||||
width: "40em"
|
||||
});
|
||||
|
||||
modal.htmlTag.dividerfy().find(".modal-body").addClass("modal-bookmarks");
|
||||
modal.open();
|
||||
}
|
|
@ -4,7 +4,7 @@ import {createInfoModal, createModal, Modal} from "../../ui/elements/Modal";
|
|||
import {copyToClipboard} from "../../utils/helpers";
|
||||
import * as i18nc from "../../i18n/country";
|
||||
import * as tooltip from "../../ui/elements/Tooltip";
|
||||
import * as moment from "moment";
|
||||
import moment from "moment";
|
||||
import {format_number, network} from "../../ui/frames/chat";
|
||||
import {generateIconJQueryTag, getIconManager} from "tc-shared/file/Icons";
|
||||
import {tr} from "tc-shared/i18n/localize";
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {ConnectionHandler} from "../../ConnectionHandler";
|
||||
import {createModal, Modal} from "../../ui/elements/Modal";
|
||||
import * as htmltags from "../../ui/htmltags";
|
||||
import * as moment from "moment";
|
||||
import moment from "moment";
|
||||
import {renderBBCodeAsJQuery} from "../../text/bbcode";
|
||||
import {tr} from "tc-shared/i18n/localize";
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import {LogCategory, logWarn} from "../../log";
|
|||
import * as tooltip from "../../ui/elements/Tooltip";
|
||||
import * as i18nc from "../../i18n/country";
|
||||
import {format_time, formatMessage} from "../../ui/frames/chat";
|
||||
import * as moment from "moment";
|
||||
import moment from "moment";
|
||||
import {ErrorCode} from "../../connection/ErrorCode";
|
||||
import {tr} from "tc-shared/i18n/localize";
|
||||
|
||||
|
|
|
@ -12,7 +12,6 @@ import {NameIdentity} from "tc-shared/profiles/identities/NameIdentity";
|
|||
import {LogCategory, logDebug, logError, logTrace, logWarn} from "tc-shared/log";
|
||||
import * as i18n from "tc-shared/i18n/localize";
|
||||
import {RepositoryTranslation, TranslationRepository} from "tc-shared/i18n/localize";
|
||||
import * as events from "tc-shared/events";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
|
||||
import * as i18nc from "tc-shared/i18n/country";
|
||||
|
@ -1905,7 +1904,7 @@ export namespace modal_settings {
|
|||
|
||||
event_registry.on("reload-profile", event => {
|
||||
event_registry.fire("query-profile-list");
|
||||
event_registry.fire("select-profile", event.profile_id || selected_profile);
|
||||
event_registry.fire("select-profile", { profile_id: event.profile_id || selected_profile });
|
||||
});
|
||||
}
|
||||
|
||||
|
|
5
shared/js/ui/modal/bookmarks-add-server/Controller.ts
Normal file
5
shared/js/ui/modal/bookmarks-add-server/Controller.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||
|
||||
export function spawnModalAddCurrentServerToBookmarks(handler: ConnectionHandler) {
|
||||
/* TODO! */
|
||||
}
|
7
shared/js/ui/modal/bookmarks-add-server/Definitions.ts
Normal file
7
shared/js/ui/modal/bookmarks-add-server/Definitions.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export interface ModalBookmarksAddServerVariables {
|
||||
|
||||
}
|
||||
|
||||
export interface ModalBookmarksAddServerEvents {
|
||||
|
||||
}
|
0
shared/js/ui/modal/bookmarks-add-server/Renderer.scss
Normal file
0
shared/js/ui/modal/bookmarks-add-server/Renderer.scss
Normal file
13
shared/js/ui/modal/bookmarks-add-server/Renderer.tsx
Normal file
13
shared/js/ui/modal/bookmarks-add-server/Renderer.tsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
import {AbstractModal} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||
import React from "react";
|
||||
|
||||
class ModalBookmarksAddServer extends AbstractModal {
|
||||
renderBody(): React.ReactElement {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
renderTitle(): string | React.ReactElement {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
}
|
568
shared/js/ui/modal/bookmarks/Controller.ts
Normal file
568
shared/js/ui/modal/bookmarks/Controller.ts
Normal file
|
@ -0,0 +1,568 @@
|
|||
import {spawnModal} from "tc-shared/ui/react-elements/modal";
|
||||
import {IpcUiVariableProvider} from "tc-shared/ui/utils/IpcVariable";
|
||||
import {
|
||||
BookmarkConnectInfo,
|
||||
BookmarkListEntry,
|
||||
CurrentClientChannel,
|
||||
ModalBookmarkEvents,
|
||||
ModalBookmarkVariables
|
||||
} from "tc-shared/ui/modal/bookmarks/Definitions";
|
||||
import {Registry} from "tc-events";
|
||||
import {BookmarkEntry, BookmarkInfo, bookmarks} from "tc-shared/Bookmarks";
|
||||
import {connectionHistory} from "tc-shared/connectionlog/History";
|
||||
import {RemoteIconInfo} from "tc-shared/file/Icons";
|
||||
import {availableConnectProfiles} from "tc-shared/profiles/ConnectionProfile";
|
||||
import {hashPassword} from "tc-shared/utils/helpers";
|
||||
import {server_connections} from "tc-shared/ConnectionManager";
|
||||
import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler";
|
||||
import {LocalClientEntry} from "tc-shared/tree/Client";
|
||||
import _ from "lodash";
|
||||
import {LogCategory, logError} from "tc-shared/log";
|
||||
|
||||
class BookmarkModalController {
|
||||
readonly events: Registry<ModalBookmarkEvents>;
|
||||
readonly variables: IpcUiVariableProvider<ModalBookmarkVariables>;
|
||||
|
||||
private selectedBookmark: BookmarkEntry;
|
||||
private bookmarkUniqueServerIds: { [key: string]: Promise<string | undefined> } = {};
|
||||
private currentClientChannels: { [key: string]: CurrentClientChannel } = {};
|
||||
|
||||
private registeredListeners: (() => void)[];
|
||||
private registeredHandlerListeners: { [key: string]: (() => void)[] } = {};
|
||||
|
||||
constructor() {
|
||||
this.events = new Registry<ModalBookmarkEvents>();
|
||||
this.variables = new IpcUiVariableProvider<ModalBookmarkVariables>();
|
||||
this.registeredListeners = [];
|
||||
|
||||
//this.variables.setArtificialDelay(1500);
|
||||
|
||||
this.variables.setVariableProvider("bookmarks", async () => {
|
||||
const orderedBookmarks: BookmarkListEntry[] = [];
|
||||
|
||||
for(const entry of bookmarks.getOrderedRegisteredBookmarks()) {
|
||||
let icon: RemoteIconInfo = undefined;
|
||||
|
||||
try {
|
||||
const serverUniqueId = await this.getBookmarkServerUniqueId(entry.entry);
|
||||
if(serverUniqueId) {
|
||||
const serverInfo = await connectionHistory.queryServerInfo(serverUniqueId);
|
||||
if(serverInfo.iconId > 0) {
|
||||
icon = {
|
||||
iconId: serverInfo.iconId,
|
||||
serverUniqueId: serverUniqueId
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
orderedBookmarks.push({
|
||||
icon,
|
||||
depth: entry.depth,
|
||||
type: entry.entry.type === "directory" ? "directory" : "bookmark",
|
||||
displayName: entry.entry.displayName,
|
||||
uniqueId: entry.entry.uniqueId,
|
||||
childCount: entry.childCount
|
||||
});
|
||||
}
|
||||
|
||||
return orderedBookmarks;
|
||||
});
|
||||
this.variables.setVariableProvider("connectProfiles", () => {
|
||||
return availableConnectProfiles().map(entry => ({
|
||||
id: entry.id,
|
||||
name: entry.profileName
|
||||
}));
|
||||
});
|
||||
|
||||
this.variables.setVariableProvider("currentClientChannel", async bookmarkId => {
|
||||
const serverUniqueId = await this.getBookmarkServerUniqueId(bookmarkId);
|
||||
if(!serverUniqueId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handler = server_connections.getAllConnectionHandlers().filter(handler => handler.connected && handler.getCurrentServerUniqueId() === serverUniqueId);
|
||||
return this.currentClientChannels[handler[0]?.handlerId];
|
||||
});
|
||||
|
||||
this.variables.setVariableProvider("bookmarkInfo", bookmarkId => {
|
||||
if(!bookmarkId || this.selectedBookmark?.uniqueId !== bookmarkId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
|
||||
this.variables.setVariableProvider("bookmarkSelected", () => {
|
||||
if(!this.selectedBookmark) {
|
||||
return { id: undefined, type: "empty" };
|
||||
} else if(this.selectedBookmark.type === "directory") {
|
||||
return { id: this.selectedBookmark.uniqueId, type: "directory" };
|
||||
} else {
|
||||
return { id: this.selectedBookmark.uniqueId, type: "bookmark" };
|
||||
}
|
||||
});
|
||||
this.variables.setVariableEditor("bookmarkSelected", newValue => {
|
||||
this.selectBookmark(newValue.id);
|
||||
});
|
||||
|
||||
this.variables.setVariableProvider("bookmarkInfo", this.bookmarkInfoProvider(undefined, async (bookmark): Promise<BookmarkConnectInfo> => {
|
||||
const serverUniqueId = await this.getBookmarkServerUniqueId(bookmark);
|
||||
if(!serverUniqueId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const serverInfo = await connectionHistory.queryServerInfo(serverUniqueId);
|
||||
if(!serverInfo) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
clientsOnline: serverInfo.clientsOnline,
|
||||
clientsMax: serverInfo.clientsMax,
|
||||
|
||||
connectCountUniqueId: await connectionHistory.countConnectCount(serverUniqueId, "server-unique-id"),
|
||||
connectCountAddress: await connectionHistory.countConnectCount(bookmark.serverAddress, "address"),
|
||||
|
||||
hostBannerMode: serverInfo.hostBannerMode,
|
||||
hostBannerUrl: serverInfo.hostBannerUrl,
|
||||
|
||||
serverName: serverInfo.name,
|
||||
serverRegion: serverInfo.country
|
||||
};
|
||||
}));
|
||||
|
||||
this.variables.setVariableProvider("bookmarkName", bookmarkId => {
|
||||
return bookmarks.findBookmark(bookmarkId)?.displayName;
|
||||
});
|
||||
this.variables.setVariableEditor("bookmarkName", (newValue, bookmarkId) => {
|
||||
const bookmark = bookmarks.findBookmark(bookmarkId);
|
||||
switch(bookmark?.type) {
|
||||
case "directory":
|
||||
bookmarks.editDirectory(bookmark.uniqueId, { displayName: newValue });
|
||||
return true;
|
||||
|
||||
case "entry":
|
||||
bookmarks.editBookmark(bookmark.uniqueId, { displayName: newValue });
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
this.variables.setVariableProvider("bookmarkServerAddress", this.bookmarkInfoProvider(undefined, bookmark => bookmark.serverAddress));
|
||||
this.variables.setVariableEditorAsync("bookmarkServerAddress", this.bookmarkEditor(async (updates, newValue) => {
|
||||
updates.serverAddress = newValue;
|
||||
}));
|
||||
|
||||
this.variables.setVariableProvider("bookmarkServerPassword", this.bookmarkInfoProvider(undefined, bookmark => bookmark.serverPasswordHash || ""));
|
||||
this.variables.setVariableEditorAsync("bookmarkServerPassword", this.bookmarkEditor(async (updates, newValue) => {
|
||||
if(newValue) {
|
||||
updates.serverPasswordHash = await hashPassword(newValue);
|
||||
} else {
|
||||
updates.serverPasswordHash = "";
|
||||
}
|
||||
return updates.serverPasswordHash;
|
||||
}));
|
||||
|
||||
this.variables.setVariableProvider("bookmarkConnectProfile", this.bookmarkInfoProvider(undefined, bookmark => bookmark.connectProfile));
|
||||
this.variables.setVariableEditorAsync("bookmarkConnectProfile", this.bookmarkEditor(async (updates, newValue) => {
|
||||
updates.connectProfile = newValue;
|
||||
}));
|
||||
|
||||
this.variables.setVariableProvider("bookmarkConnectOnStartup", this.bookmarkInfoProvider(undefined, bookmark => bookmark.connectOnStartup));
|
||||
this.variables.setVariableEditorAsync("bookmarkConnectOnStartup", this.bookmarkEditor(async (updates, newValue) => {
|
||||
updates.connectOnStartup = newValue;
|
||||
}));
|
||||
|
||||
this.variables.setVariableProvider("bookmarkDefaultChannel", this.bookmarkInfoProvider(undefined, bookmark => bookmark.defaultChannel));
|
||||
this.variables.setVariableEditorAsync("bookmarkDefaultChannel", this.bookmarkEditor(async (updates, newValue) => {
|
||||
updates.defaultChannel = newValue;
|
||||
updates.defaultChannelPasswordHash = undefined;
|
||||
}));
|
||||
|
||||
this.variables.setVariableProvider("bookmarkDefaultChannelPassword", this.bookmarkInfoProvider(undefined, bookmark => bookmark.defaultChannelPasswordHash || ""));
|
||||
this.variables.setVariableEditorAsync("bookmarkDefaultChannelPassword", this.bookmarkEditor(async (updates, newValue) => {
|
||||
if(newValue) {
|
||||
updates.defaultChannelPasswordHash = await hashPassword(newValue);
|
||||
} else {
|
||||
updates.defaultChannelPasswordHash = "";
|
||||
}
|
||||
return updates.defaultChannelPasswordHash;
|
||||
}));
|
||||
|
||||
/* events */
|
||||
this.events.on("action_delete_bookmark", event => bookmarks.deleteEntry(event.uniqueId));
|
||||
this.events.on("action_create_bookmark", event => {
|
||||
if(!event.displayName) {
|
||||
return;
|
||||
}
|
||||
|
||||
let parentBookmark, previousBookmark;
|
||||
switch (event.order.type) {
|
||||
case "parent":
|
||||
parentBookmark = event.order.entry;
|
||||
previousBookmark = undefined;
|
||||
break;
|
||||
|
||||
case "previous": {
|
||||
const previous = bookmarks.findBookmark(event.order.entry);
|
||||
previousBookmark = event.order.entry;
|
||||
parentBookmark = previous?.parentEntry;
|
||||
break;
|
||||
}
|
||||
|
||||
case "selected":
|
||||
default:
|
||||
parentBookmark = this.selectedBookmark?.parentEntry;
|
||||
previousBookmark = this.selectedBookmark?.previousEntry;
|
||||
break;
|
||||
}
|
||||
|
||||
if(event.entryType === "bookmark") {
|
||||
bookmarks.createBookmark({
|
||||
displayName: event.displayName,
|
||||
|
||||
parentEntry: parentBookmark,
|
||||
previousEntry: previousBookmark,
|
||||
|
||||
connectOnStartup: false,
|
||||
connectProfile: "default",
|
||||
|
||||
defaultChannelPasswordHash: undefined,
|
||||
defaultChannel: undefined,
|
||||
|
||||
serverAddress: "",
|
||||
serverPasswordHash: undefined
|
||||
});
|
||||
} else {
|
||||
bookmarks.createDirectory({
|
||||
displayName: event.displayName,
|
||||
|
||||
parentEntry: parentBookmark,
|
||||
previousEntry: previousBookmark,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.events.on("action_duplicate_bookmark", event => {
|
||||
if(!event.displayName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bookmark = bookmarks.findBookmark(event.uniqueId);
|
||||
if(!bookmark || bookmark.type !== "entry") {
|
||||
return;
|
||||
}
|
||||
|
||||
const newBookmark = bookmarks.createBookmark({
|
||||
serverAddress: bookmark.serverAddress,
|
||||
serverPasswordHash: bookmark.serverPasswordHash,
|
||||
|
||||
defaultChannel: bookmark.defaultChannel,
|
||||
defaultChannelPasswordHash: bookmark.defaultChannelPasswordHash,
|
||||
|
||||
connectOnStartup: bookmark.connectOnStartup,
|
||||
connectProfile: bookmark.connectProfile,
|
||||
|
||||
displayName: event.displayName,
|
||||
|
||||
previousEntry: bookmark.uniqueId,
|
||||
parentEntry: bookmark.parentEntry
|
||||
});
|
||||
this.selectBookmark(newBookmark.uniqueId);
|
||||
});
|
||||
|
||||
this.events.on("action_connect", event => {
|
||||
bookmarks.executeConnect(event.uniqueId, event.newTab);
|
||||
});
|
||||
|
||||
this.events.on("action_export", () => {
|
||||
this.events.fire("notify_export_data", { payload: bookmarks.exportBookmarks() });
|
||||
});
|
||||
|
||||
this.events.on("action_import", event => {
|
||||
if(!event.payload) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const importedBookmarks = bookmarks.importBookmarks(event.payload);
|
||||
this.events.fire("notify_import_result", { status: "success", importedBookmarks: importedBookmarks });
|
||||
} catch (error) {
|
||||
if(typeof error !== "string") {
|
||||
logError(LogCategory.BOOKMARKS, tr("Failed to import bookmarks: %o"), error);
|
||||
error = tr("lookup the console for more details");
|
||||
}
|
||||
|
||||
this.events.fire("notify_import_result", { status: "error", message: error });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.registeredListeners.push(bookmarks.events.on("notify_bookmark_deleted", event => {
|
||||
this.variables.sendVariable("bookmarks");
|
||||
if(this.selectedBookmark?.uniqueId === event.bookmark.uniqueId) {
|
||||
this.selectBookmark(bookmarks.getRegisteredBookmarks()[0]?.uniqueId);
|
||||
}
|
||||
}));
|
||||
|
||||
this.registeredListeners.push(bookmarks.events.on("notify_bookmark_created", event => {
|
||||
this.variables.sendVariable("bookmarks");
|
||||
this.selectBookmark(event.bookmark.uniqueId);
|
||||
}));
|
||||
|
||||
this.registeredListeners.push(bookmarks.events.on("notify_bookmarks_imported", () => this.variables.sendVariable("bookmarks")));
|
||||
|
||||
this.registeredListeners.push(bookmarks.events.on("notify_bookmark_edited", event => {
|
||||
if(event.keys.indexOf("serverAddress") !== -1) {
|
||||
delete this.bookmarkUniqueServerIds[event.bookmark.uniqueId];
|
||||
}
|
||||
|
||||
if(event.keys.indexOf("displayName") !== -1 ||
|
||||
event.keys.indexOf("parentEntry") !== -1 ||
|
||||
event.keys.indexOf("previousEntry") !== -1 ||
|
||||
event.keys.indexOf("serverAddress") !== -1) {
|
||||
this.variables.sendVariable("bookmarks");
|
||||
}
|
||||
|
||||
if(event.bookmark.uniqueId === this.selectedBookmark?.uniqueId) {
|
||||
if(event.keys.indexOf("displayName") !== -1) {
|
||||
this.variables.sendVariable("bookmarkName", this.selectedBookmark.uniqueId);
|
||||
}
|
||||
|
||||
if(event.keys.indexOf("connectProfile") !== -1) {
|
||||
this.variables.sendVariable("bookmarkConnectProfile", this.selectedBookmark.uniqueId);
|
||||
}
|
||||
|
||||
if(event.keys.indexOf("connectOnStartup") !== -1) {
|
||||
this.variables.sendVariable("bookmarkConnectOnStartup", this.selectedBookmark.uniqueId);
|
||||
}
|
||||
|
||||
if(event.keys.indexOf("serverAddress") !== -1) {
|
||||
this.variables.sendVariable("bookmarkServerAddress", this.selectedBookmark.uniqueId);
|
||||
this.variables.sendVariable("bookmarkInfo", this.selectedBookmark.uniqueId);
|
||||
this.variables.sendVariable("currentClientChannel", this.selectedBookmark.uniqueId);
|
||||
}
|
||||
|
||||
if(event.keys.indexOf("serverPasswordHash") !== -1) {
|
||||
this.variables.sendVariable("bookmarkServerPassword", this.selectedBookmark.uniqueId);
|
||||
}
|
||||
|
||||
if(event.keys.indexOf("defaultChannel") !== -1) {
|
||||
this.variables.sendVariable("bookmarkDefaultChannel", this.selectedBookmark.uniqueId);
|
||||
}
|
||||
|
||||
if(event.keys.indexOf("defaultChannelPasswordHash") !== -1) {
|
||||
this.variables.sendVariable("bookmarkDefaultChannelPassword", this.selectedBookmark.uniqueId);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
this.registeredListeners.push(server_connections.events().on("notify_handler_created", event => {
|
||||
this.registerHandlerListener(event.handler);
|
||||
}));
|
||||
|
||||
this.registeredListeners.push(server_connections.events().on("notify_handler_deleted", event => {
|
||||
this.unregisterHandlerListener(event.handler);
|
||||
}));
|
||||
|
||||
server_connections.getAllConnectionHandlers().forEach(handler => this.registerHandlerListener(handler));
|
||||
this.selectBookmark(bookmarks.getRegisteredBookmarks()[0].uniqueId);
|
||||
}
|
||||
|
||||
private registerHandlerListener(handler: ConnectionHandler) {
|
||||
const events = this.registeredHandlerListeners[handler.handlerId] = [];
|
||||
events.push(handler.events().on("notify_connection_state_changed", event => {
|
||||
if(event.newState !== ConnectionState.CONNECTED) {
|
||||
this.updateHandlerClientChannel(handler, undefined);
|
||||
}
|
||||
}));
|
||||
|
||||
events.push(handler.channelTree.events.on(["notify_client_moved", "notify_client_enter_view"], event => {
|
||||
if(!(event.client instanceof LocalClientEntry)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetChannel = event.client.currentChannel();
|
||||
if(!targetChannel) {
|
||||
this.updateHandlerClientChannel(handler, undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
/* TODO: Register to channel rename events maybe? */
|
||||
this.updateHandlerClientChannel(handler);
|
||||
}));
|
||||
|
||||
this.updateHandlerClientChannel(handler);
|
||||
}
|
||||
|
||||
private unregisterHandlerListener(handler: ConnectionHandler) {
|
||||
this.updateHandlerClientChannel(handler, undefined);
|
||||
this.registeredHandlerListeners[handler.handlerId]?.forEach(callback => callback());
|
||||
delete this.registeredHandlerListeners[handler.handlerId];
|
||||
}
|
||||
|
||||
private updateHandlerClientChannel(handler: ConnectionHandler, newChannel?: CurrentClientChannel) {
|
||||
if(arguments.length === 1 && handler.connected) {
|
||||
const channel = handler.getClient().currentChannel();
|
||||
if(channel) {
|
||||
let path;
|
||||
{
|
||||
path = "";
|
||||
let current = channel;
|
||||
while(current) {
|
||||
path = "/" + current.channelName() + path;
|
||||
current = current.parent_channel();
|
||||
}
|
||||
}
|
||||
|
||||
newChannel = {
|
||||
name: channel.channelName(),
|
||||
passwordHash: channel.getCachedPasswordHash(),
|
||||
path: path,
|
||||
channelId: channel.getChannelId()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const oldChannel = this.registeredHandlerListeners[handler.handlerId];
|
||||
if(_.isEqual(oldChannel, newChannel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!newChannel) {
|
||||
delete this.currentClientChannels[handler.handlerId];
|
||||
} else {
|
||||
this.currentClientChannels[handler.handlerId] = newChannel;
|
||||
}
|
||||
|
||||
/* Only update the current bookmark */
|
||||
const handlerServerUniqueId = handler.getCurrentServerUniqueId();
|
||||
this.getBookmarkServerUniqueId(this.selectedBookmark).then(serverUniqueId => {
|
||||
if(serverUniqueId !== handlerServerUniqueId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.variables.sendVariable("currentClientChannel", this.selectedBookmark.uniqueId);
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.registeredListeners.forEach(callback => callback());
|
||||
this.registeredListeners = [];
|
||||
|
||||
Object.keys(this.registeredHandlerListeners)
|
||||
.forEach(handlerId => this.registeredHandlerListeners[handlerId].forEach(callback => callback()));
|
||||
this.registeredHandlerListeners = {};
|
||||
|
||||
this.events.destroy();
|
||||
this.variables.destroy();
|
||||
}
|
||||
|
||||
selectBookmark(bookmarkUniqueId: string) {
|
||||
if(this.selectedBookmark?.uniqueId === bookmarkUniqueId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedBookmark = bookmarks.findBookmark(bookmarkUniqueId);
|
||||
this.variables.sendVariable("bookmarkSelected");
|
||||
}
|
||||
|
||||
private bookmarkInfoProvider<T>(defaultValue: T, callback: (bookmark: BookmarkInfo) => T | Promise<T>) : (bookmarkId: any) => T | Promise<T> {
|
||||
return bookmarkId => {
|
||||
if(!bookmarkId) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if(bookmarkId === this.selectedBookmark?.uniqueId) {
|
||||
if(this.selectedBookmark.type === "entry") {
|
||||
return callback(this.selectedBookmark);
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
} else {
|
||||
const bookmark = bookmarks.findBookmark(bookmarkId);
|
||||
if(bookmark?.type === "entry") {
|
||||
return callback(bookmark);
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bookmarkEditor<T, R>(callback: (updates: Partial<BookmarkInfo>, newValue: T, bookmark: BookmarkInfo) => Promise<R>) : (newValue: T, bookmarkId: any) => Promise<R | false> {
|
||||
return async (newValue, bookmarkId) => {
|
||||
if(!bookmarkId) {
|
||||
return;
|
||||
}
|
||||
|
||||
let bookmark: BookmarkInfo;
|
||||
if(bookmarkId === this.selectedBookmark?.uniqueId) {
|
||||
if(this.selectedBookmark.type === "entry") {
|
||||
bookmark = this.selectedBookmark;
|
||||
}
|
||||
} else {
|
||||
const foundBookmark = bookmarks.findBookmark(bookmarkId);
|
||||
if(foundBookmark?.type === "entry") {
|
||||
bookmark = foundBookmark;
|
||||
}
|
||||
}
|
||||
|
||||
if(!bookmark) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const updates = {};
|
||||
const result = await callback(updates, newValue, bookmark);
|
||||
|
||||
if(Object.keys(updates).length > 0) {
|
||||
bookmarks.editBookmark(bookmark.uniqueId, updates);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private getBookmarkServerUniqueId(bookmarkOrId: string | BookmarkEntry) : Promise<string | undefined> {
|
||||
let bookmarkId = typeof bookmarkOrId === "string" ? bookmarkOrId : bookmarkOrId?.uniqueId;
|
||||
let bookmark = typeof bookmarkOrId === "string" ? bookmarks.findBookmark(bookmarkOrId) : bookmarkOrId;
|
||||
if(!bookmark || bookmark.type !== "entry") {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
if(this.bookmarkUniqueServerIds[bookmarkId]) {
|
||||
return this.bookmarkUniqueServerIds[bookmarkId];
|
||||
}
|
||||
|
||||
return this.bookmarkUniqueServerIds[bookmarkId] = (async () => {
|
||||
const info = await connectionHistory.lastConnectInfo(bookmark.serverAddress, "address");
|
||||
if(!info) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return info.serverUniqueId;
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
export function spawnBookmarkModal() {
|
||||
const controller = new BookmarkModalController();
|
||||
controller.initialize();
|
||||
|
||||
const modal = spawnModal("modal-bookmarks", [controller.events.generateIpcDescription(), controller.variables.generateConsumerDescription()], { popoutable: true, popedOut: false });
|
||||
modal.getEvents().on("destroy", () => controller.destroy());
|
||||
|
||||
controller.events.on("action_connect", event => {
|
||||
if(!event.closeModal) {
|
||||
return;
|
||||
}
|
||||
|
||||
modal.destroy();
|
||||
});
|
||||
|
||||
modal.show().then(undefined);
|
||||
}
|
77
shared/js/ui/modal/bookmarks/Definitions.ts
Normal file
77
shared/js/ui/modal/bookmarks/Definitions.ts
Normal file
|
@ -0,0 +1,77 @@
|
|||
import {RemoteIconInfo} from "tc-shared/file/Icons";
|
||||
|
||||
export type BookmarkListEntry = {
|
||||
uniqueId: string,
|
||||
type: "bookmark" | "directory",
|
||||
|
||||
displayName: string,
|
||||
icon: RemoteIconInfo | undefined,
|
||||
|
||||
depth: number,
|
||||
childCount: number,
|
||||
}
|
||||
|
||||
export interface BookmarkConnectInfo {
|
||||
serverName: string,
|
||||
serverRegion: string,
|
||||
|
||||
clientsOnline: number,
|
||||
clientsMax: number,
|
||||
|
||||
connectCountUniqueId: number,
|
||||
connectCountAddress: number,
|
||||
|
||||
hostBannerUrl: string,
|
||||
hostBannerMode: number
|
||||
}
|
||||
|
||||
export type CurrentClientChannel = { name: string, channelId: number, path: string, passwordHash: string };
|
||||
|
||||
export interface ModalBookmarkVariables {
|
||||
readonly bookmarks: BookmarkListEntry[],
|
||||
bookmarkSelected: { type?: "empty" | "bookmark" | "directory", id: string | undefined },
|
||||
|
||||
readonly connectProfiles: { id: string, name: string }[],
|
||||
readonly currentClientChannel: CurrentClientChannel | undefined,
|
||||
|
||||
bookmarkName: string,
|
||||
bookmarkConnectProfile: string,
|
||||
bookmarkConnectOnStartup: boolean,
|
||||
bookmarkServerAddress: string,
|
||||
bookmarkServerPassword: string | undefined,
|
||||
bookmarkDefaultChannel: string | undefined,
|
||||
bookmarkDefaultChannelPassword: string | undefined,
|
||||
bookmarkInfo: BookmarkConnectInfo | undefined,
|
||||
}
|
||||
|
||||
export interface ModalBookmarkEvents {
|
||||
action_create_bookmark: {
|
||||
entryType: "bookmark" | "directory",
|
||||
order: {
|
||||
type: "previous",
|
||||
entry: string
|
||||
} | {
|
||||
type: "parent",
|
||||
entry: string
|
||||
} | {
|
||||
type: "selected",
|
||||
},
|
||||
displayName: string | undefined
|
||||
},
|
||||
action_duplicate_bookmark: { uniqueId: string, displayName: string | undefined, originalName: string },
|
||||
action_delete_bookmark: { uniqueId: string },
|
||||
|
||||
action_connect: { uniqueId: string, newTab: boolean, closeModal: boolean },
|
||||
|
||||
action_export: {},
|
||||
action_import: { payload: string | undefined },
|
||||
|
||||
notify_export_data: { payload: string }
|
||||
notify_import_result: {
|
||||
status: "success",
|
||||
importedBookmarks: number
|
||||
} | {
|
||||
status: "error",
|
||||
message: string
|
||||
}
|
||||
}
|
670
shared/js/ui/modal/bookmarks/Renderer.scss
Normal file
670
shared/js/ui/modal/bookmarks/Renderer.scss
Normal file
|
@ -0,0 +1,670 @@
|
|||
@import "../../../../css/static/mixin";
|
||||
@import "../../../../css/static/properties";
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
height: 45em;
|
||||
|
||||
min-width: 30em;
|
||||
width: 80em;
|
||||
max-width: 100%;
|
||||
|
||||
flex-shrink: 1;
|
||||
|
||||
@include user-select(none);
|
||||
|
||||
.inputBoxed {
|
||||
height: 2em;
|
||||
}
|
||||
}
|
||||
|
||||
.listContainer {
|
||||
min-width: 12em;
|
||||
width: 30%;
|
||||
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
padding: .5em;
|
||||
background-color: #212125;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
.title {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
.text {
|
||||
flex-shrink: 1;
|
||||
min-width: 1em;
|
||||
|
||||
text-align: left;
|
||||
|
||||
font-size: 1.5em;
|
||||
color: #557edc;
|
||||
text-transform: uppercase;
|
||||
|
||||
@include text-dotdotdot();
|
||||
}
|
||||
|
||||
.containerButton {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.button {
|
||||
cursor: pointer;
|
||||
|
||||
font-size: 1.2em;
|
||||
|
||||
padding: .2em;
|
||||
border-radius: .2em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
margin-right: .2em;
|
||||
|
||||
&:hover {
|
||||
background-color: #0000004f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.containerBookmarks {
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
|
||||
min-height: 6em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
|
||||
@include chat-scrollbar();
|
||||
|
||||
.bookmark, .directory {
|
||||
position: relative;
|
||||
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
border-radius: $border_radius_middle;
|
||||
padding: .25em .5em;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
align-self: center;
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
.name {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
min-width: 5em;
|
||||
align-self: center;
|
||||
|
||||
@include text-dotdotdot();
|
||||
}
|
||||
|
||||
.bookmarkButtons {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
background: inherit;
|
||||
|
||||
@include transition(opacity ease-in-out $button_hover_animation_time);
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
align-self: center;
|
||||
|
||||
margin-right: .2em;
|
||||
|
||||
padding: .2em;
|
||||
border-radius: .2em;
|
||||
|
||||
&:hover {
|
||||
background-color: #0000004f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #2c2d2f;
|
||||
|
||||
.bookmarkButtons {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: #1a1a1b;
|
||||
}
|
||||
|
||||
.link {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
position: relative;
|
||||
width: 1.5em;
|
||||
|
||||
$line_width: 2px;
|
||||
$color: hsla(0, 0%, 35%, 1);
|
||||
|
||||
&:not(.hidden) {
|
||||
&:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
|
||||
height: 2.25em; /* connect with the previous one */
|
||||
width: .75em;
|
||||
|
||||
left: .5em; /* icons have a width of 1em */
|
||||
bottom: calc(.75em - #{$line_width / 2});
|
||||
|
||||
border-left: $line_width solid $color;
|
||||
}
|
||||
|
||||
&.connected {
|
||||
&:before {
|
||||
border-bottom: $line_width solid $color;
|
||||
|
||||
border-bottom-left-radius: .3em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.linkStart + .bookmark, .linkStart + .directory {
|
||||
.link.connected {
|
||||
&:before {
|
||||
height: 1.25em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
padding-top: .5em;
|
||||
|
||||
button {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-left: .5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
background-color: #212125;
|
||||
}
|
||||
}
|
||||
|
||||
.infoContainer {
|
||||
min-width: 25em;
|
||||
width: 30%;
|
||||
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
background-color: #2f2f35;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
.header {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
height: 10em;
|
||||
overflow: hidden;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
|
||||
padding: .5em;
|
||||
position: relative;
|
||||
|
||||
.containerName {
|
||||
z-index: 1;
|
||||
|
||||
position: relative;
|
||||
padding-right: 1.5em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.name {
|
||||
flex-shrink: 1;
|
||||
flex-grow: 0;
|
||||
|
||||
font-size: 2em;
|
||||
color: #fcfcfc;
|
||||
|
||||
@include text-dotdotdot();
|
||||
text-shadow: 2px 2px #666666;
|
||||
|
||||
@include transition(border-color ease-in-out $button_hover_animation_time);
|
||||
}
|
||||
|
||||
.edit {
|
||||
flex-shrink: 0;
|
||||
|
||||
padding-left: .5em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.button {
|
||||
padding: .2em;
|
||||
border-radius: .2em;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
|
||||
opacity: .5;
|
||||
|
||||
@include transition(all ease-in-out $button_hover_animation_time);
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background: #0000004f;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.editing {
|
||||
padding-right: 0;
|
||||
|
||||
.name {
|
||||
width: 100%;
|
||||
|
||||
text-shadow: none;
|
||||
|
||||
border: .04em solid white;
|
||||
border-radius: .15em;
|
||||
|
||||
padding-left: .25em;
|
||||
padding-right: .25em;
|
||||
|
||||
background: #0000006f;
|
||||
|
||||
overflow: auto;
|
||||
text-overflow: unset;
|
||||
|
||||
@include chat-scrollbar(.5em / 2em);
|
||||
|
||||
&.invalid {
|
||||
border-color: #721c1c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.containerAddress {
|
||||
z-index: 1;
|
||||
|
||||
font-size: 1.5em;
|
||||
color: #fcfcfc;
|
||||
|
||||
@include text-dotdotdot();
|
||||
}
|
||||
|
||||
.hostBanner {
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
&.individual {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
padding: .5em;
|
||||
|
||||
&:after {
|
||||
content: ' ';
|
||||
background: #00000020;
|
||||
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.renderer {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
//background: url("./header_background.png") no-repeat;
|
||||
}
|
||||
|
||||
.containerSettings {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-height: 10em;
|
||||
|
||||
padding: .5em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
@include chat-scrollbar-vertical();
|
||||
|
||||
.group {
|
||||
padding: .5em;
|
||||
|
||||
border-radius: .2em;
|
||||
border: 1px solid #1f2122;
|
||||
|
||||
background-color: #28292b;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
> .row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
.key {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
|
||||
width: 15em;
|
||||
min-width: 2em;
|
||||
|
||||
align-self: center;
|
||||
|
||||
color: #557edc;
|
||||
|
||||
text-transform: uppercase;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.value {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
min-width: 2em;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
&.connectInfoContainer {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
padding: .5em;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
|
||||
.buttonDuplicate {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
button {
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
|
||||
min-width: 2em;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-left: .5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.connectInfoContainer {
|
||||
position: relative;
|
||||
|
||||
.containerImage {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
max-width: 15em;
|
||||
max-height: 9em; /* minus one padding */
|
||||
width: 15em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
object-fit: contain;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@include transition(.25s ease-in-out);
|
||||
}
|
||||
|
||||
.containerProperties {
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
|
||||
min-width: 23em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
height: inherit;
|
||||
|
||||
.row {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
height: 1.8em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
|
||||
.key {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
color: #557edc;
|
||||
text-transform: uppercase;
|
||||
align-self: center;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
width: 15em;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #d6d6d7;
|
||||
align-self: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&.serverRegion {
|
||||
> div {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.country {
|
||||
margin-right: .25em;
|
||||
}
|
||||
}
|
||||
|
||||
&.valueConnectCount {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
|
||||
.text {
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
> span {
|
||||
display: flex;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
.connectCount, .connectNever {
|
||||
display: inline-block;
|
||||
|
||||
color: #7a3131;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tooltipIcon {
|
||||
text-align: left;
|
||||
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
background: #28292b;
|
||||
}
|
||||
}
|
||||
|
||||
.inputIconContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.iconContainer {
|
||||
display: flex;
|
||||
|
||||
padding: .2em;
|
||||
margin-right: .2em;
|
||||
}
|
||||
|
||||
&.enabled {
|
||||
.iconContainer {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #ffffff10;
|
||||
border-radius: .2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
&.shown {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: #666;
|
||||
text-align: center;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.buttonCreate {
|
||||
margin-top: .5em;
|
||||
max-width: 100%;
|
||||
|
||||
flex-grow: 0;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
797
shared/js/ui/modal/bookmarks/Renderer.tsx
Normal file
797
shared/js/ui/modal/bookmarks/Renderer.tsx
Normal file
|
@ -0,0 +1,797 @@
|
|||
import {AbstractModal} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||
import {createIpcUiVariableConsumer, IpcVariableDescriptor} from "tc-shared/ui/utils/IpcVariable";
|
||||
import {
|
||||
BookmarkConnectInfo,
|
||||
BookmarkListEntry,
|
||||
ModalBookmarkEvents,
|
||||
ModalBookmarkVariables
|
||||
} from "tc-shared/ui/modal/bookmarks/Definitions";
|
||||
import {IpcRegistryDescription, Registry} from "tc-events";
|
||||
import {UiVariableConsumer} from "tc-shared/ui/utils/Variable";
|
||||
import {Translatable, VariadicTranslatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {useContext, useEffect, useRef} from "react";
|
||||
import {ContextDivider} from "tc-shared/ui/react-elements/ContextDivider";
|
||||
import {joinClassList, useDependentState, useTr} from "tc-shared/ui/react-elements/Helper";
|
||||
import {Button} from "tc-shared/ui/react-elements/Button";
|
||||
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
||||
import {IconRenderer, RemoteIconRenderer} from "tc-shared/ui/react-elements/Icon";
|
||||
import {getIconManager} from "tc-shared/file/Icons";
|
||||
import {ClientIcon} from "svg-sprites/client-icons";
|
||||
import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons";
|
||||
import {spawnContextMenu} from "tc-shared/ui/ContextMenu";
|
||||
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
|
||||
import {formatMessage} from "tc-shared/ui/frames/chat";
|
||||
import {createErrorModal, createInfoModal, createInputModal} from "tc-shared/ui/elements/Modal";
|
||||
import {HostBannerRenderer} from "tc-shared/ui/frames/HostBannerRenderer";
|
||||
import {ControlledBoxedInputField, ControlledSelect} from "tc-shared/ui/react-elements/InputField";
|
||||
import {Checkbox} from "tc-shared/ui/react-elements/Checkbox";
|
||||
import * as React from "react";
|
||||
|
||||
import DefaultHeaderImage from "./header_background.png";
|
||||
import ServerInfoImage from "./serverinfo.png";
|
||||
import {IconTooltip} from "tc-shared/ui/react-elements/Tooltip";
|
||||
import {CountryIcon} from "tc-shared/ui/react-elements/CountryIcon";
|
||||
import {downloadTextAsFile, requestFileAsText} from "tc-shared/file/Utils";
|
||||
|
||||
const EventContext = React.createContext<Registry<ModalBookmarkEvents>>(undefined);
|
||||
const VariableContext = React.createContext<UiVariableConsumer<ModalBookmarkVariables>>(undefined);
|
||||
const SelectedBookmarkIdContext = React.createContext<{ type: "empty" | "bookmark" | "directory", id: string | undefined }>({ type: "empty", id: undefined });
|
||||
const SelectedBookmarkInfoContext = React.createContext<BookmarkConnectInfo>(undefined);
|
||||
|
||||
const cssStyle = require("./Renderer.scss");
|
||||
|
||||
const Link = (props: { connected: boolean }) => (
|
||||
<div className={joinClassList(
|
||||
cssStyle.link,
|
||||
props.connected ? cssStyle.connected : undefined
|
||||
)} />
|
||||
);
|
||||
|
||||
const BookmarkListEntryRenderer = React.memo((props: { entry: BookmarkListEntry }) => {
|
||||
const variables = useContext(VariableContext);
|
||||
const events = useContext(EventContext);
|
||||
const selectedItem = variables.useVariable("bookmarkSelected", undefined, undefined);
|
||||
|
||||
const tryDelete = () => {
|
||||
if(props.entry.type === "directory" && props.entry.childCount > 0) {
|
||||
spawnYesNo(tr("Are you sure?"), formatMessage(
|
||||
tr("Do you really want to delete the directory \"{0}\"?{:br:}The directory contains {1} entries."),
|
||||
props.entry.displayName, props.entry.childCount
|
||||
), result => {
|
||||
if(result) {
|
||||
events.fire("action_delete_bookmark", { uniqueId: props.entry.uniqueId });
|
||||
}
|
||||
}).open();
|
||||
} else {
|
||||
events.fire("action_delete_bookmark", { uniqueId: props.entry.uniqueId });
|
||||
}
|
||||
};
|
||||
|
||||
let icon;
|
||||
if(props.entry.icon) {
|
||||
icon = <RemoteIconRenderer key={"icon-" + props.entry.icon.iconId} icon={getIconManager().resolveIconInfo(props.entry.icon)} className={cssStyle.icon} />;
|
||||
} else if(props.entry.type === "directory") {
|
||||
icon = <IconRenderer key={"directory"} icon={ClientIcon.Folder} className={cssStyle.icon} />;
|
||||
} else {
|
||||
icon = <IconRenderer key={"no-icon"} icon={ClientIcon.ServerGreen} className={cssStyle.icon} />;
|
||||
}
|
||||
|
||||
let links = [];
|
||||
for(let i = 0; i < props.entry.depth; i++) {
|
||||
links.push(<Link connected={i + 1 === props.entry.depth} key={"link-" + i} />);
|
||||
}
|
||||
|
||||
let buttons = [];
|
||||
if(props.entry.type === "bookmark") {
|
||||
buttons.push(
|
||||
<div
|
||||
className={cssStyle.button}
|
||||
key={"bookmark-duplicate"}
|
||||
title={tr("Duplicate entry")}
|
||||
onClick={() => events.fire("action_duplicate_bookmark", { uniqueId: props.entry.uniqueId, displayName: undefined, originalName: props.entry.displayName })}
|
||||
>
|
||||
<ClientIconRenderer icon={ClientIcon.BookmarkDuplicate} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
buttons.push(
|
||||
<div className={cssStyle.button} key={"bookmark-remove"} title={tr("Delete entry")} onClick={tryDelete}>
|
||||
<ClientIconRenderer icon={ClientIcon.Delete} />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={"entry-" + props.entry.uniqueId}
|
||||
className={joinClassList(
|
||||
props.entry.type === "directory" ? cssStyle.directory : cssStyle.bookmark,
|
||||
props.entry.childCount > 0 ? cssStyle.linkStart : undefined,
|
||||
selectedItem.remoteValue?.id === props.entry.uniqueId ? cssStyle.selected : undefined,
|
||||
)}
|
||||
onClick={() => {
|
||||
if(selectedItem.remoteValue?.id === props.entry.uniqueId) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectedItem.setValue({id: props.entry.uniqueId});
|
||||
}}
|
||||
onDoubleClick={() => {
|
||||
if(props.entry.type !== "bookmark") {
|
||||
return;
|
||||
}
|
||||
|
||||
events.fire("action_connect", { uniqueId: props.entry.uniqueId, newTab: false, closeModal: true });
|
||||
}}
|
||||
onContextMenu={event => {
|
||||
event.preventDefault();
|
||||
|
||||
if(selectedItem.remoteValue?.id !== props.entry.uniqueId) {
|
||||
selectedItem.setValue({ id: props.entry.uniqueId });
|
||||
}
|
||||
|
||||
spawnContextMenu({ pageX: event.pageX, pageY: event.pageY }, [
|
||||
{
|
||||
type: "normal",
|
||||
label: tr("Connect to server"),
|
||||
visible: props.entry.type === "bookmark",
|
||||
icon: ClientIcon.Connect,
|
||||
click: () => events.fire("action_connect", { uniqueId: props.entry.uniqueId, newTab: false, closeModal: true })
|
||||
},
|
||||
{
|
||||
type: "normal",
|
||||
label: tr("Connect in a new tab"),
|
||||
visible: props.entry.type === "bookmark",
|
||||
icon: ClientIcon.Connect,
|
||||
click: () => events.fire("action_connect", { uniqueId: props.entry.uniqueId, newTab: true, closeModal: true })
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
visible: props.entry.type === "bookmark",
|
||||
},
|
||||
{
|
||||
type: "normal",
|
||||
label: tr("Duplicate Bookmark"),
|
||||
visible: props.entry.type === "bookmark",
|
||||
icon: ClientIcon.BookmarkDuplicate,
|
||||
click: () => events.fire("action_duplicate_bookmark", { uniqueId: props.entry.uniqueId, displayName: undefined, originalName: props.entry.displayName })
|
||||
},
|
||||
{
|
||||
type: "normal",
|
||||
label: tr("Add bookmark"),
|
||||
icon: ClientIcon.BookmarkAdd,
|
||||
click: () => events.fire("action_create_bookmark", { entryType: "bookmark", order: { type: "parent", entry: props.entry.uniqueId }, displayName: undefined })
|
||||
},
|
||||
{
|
||||
type: "normal",
|
||||
label: tr("Add directory"),
|
||||
icon: ClientIcon.BookmarkAddFolder,
|
||||
click: () => events.fire("action_create_bookmark", { entryType: "directory", order: { type: "previous", entry: props.entry.uniqueId }, displayName: undefined })
|
||||
},
|
||||
{
|
||||
type: "normal",
|
||||
label: tr("Add sub directory"),
|
||||
visible: props.entry.type === "directory",
|
||||
icon: ClientIcon.BookmarkAddFolder,
|
||||
click: () => events.fire("action_create_bookmark", { entryType: "directory", order: { type: "parent", entry: props.entry.uniqueId }, displayName: undefined })
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
type: "normal",
|
||||
label: props.entry.type === "bookmark" ? tr("Delete bookmark") : tr("Delete directory"),
|
||||
icon: ClientIcon.BookmarkRemove,
|
||||
click: tryDelete
|
||||
}
|
||||
]);
|
||||
}}
|
||||
>
|
||||
{...links}
|
||||
{icon}
|
||||
<div className={cssStyle.name} title={props.entry.displayName}>
|
||||
{props.entry.displayName}
|
||||
</div>
|
||||
<div className={cssStyle.bookmarkButtons}>
|
||||
{...buttons}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const BookmarkList = React.memo(() => {
|
||||
const events = useContext(EventContext);
|
||||
const variables = useContext(VariableContext);
|
||||
const bookmarksInfo = variables.useReadOnly("bookmarks");
|
||||
|
||||
const bookmarks = bookmarksInfo.status === "loaded" ? bookmarksInfo.value : [];
|
||||
|
||||
return (
|
||||
<div className={cssStyle.containerBookmarks}>
|
||||
{bookmarks.map(entry => <BookmarkListEntryRenderer entry={entry} key={"entry-" + entry.uniqueId} />)}
|
||||
<div key={"overlay-loading"} className={cssStyle.overlay + " " + (bookmarksInfo.status === "loaded" ? "" : cssStyle.shown)}>
|
||||
<div className={cssStyle.text}><Translatable>loading</Translatable> <LoadingDots /></div>
|
||||
</div>
|
||||
<div key={"overlay-no-entries"} className={cssStyle.overlay + " " + (bookmarksInfo.status === "loaded" && bookmarksInfo.value.length === 0 ? cssStyle.shown : "")}>
|
||||
<div className={cssStyle.text}>
|
||||
<Translatable>You don't have any bookmarks</Translatable>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => events.fire("action_create_bookmark", { order: { type: "selected" }, displayName: undefined, entryType: "bookmark" })}
|
||||
className={cssStyle.buttonCreate}
|
||||
>
|
||||
<Translatable>Create new bookmark</Translatable>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const BookmarkListContainer = React.memo(() => {
|
||||
const events = useContext(EventContext);
|
||||
|
||||
return (
|
||||
<div className={cssStyle.listContainer}>
|
||||
<div className={cssStyle.title} title={useTr("Your bookmarks")}>
|
||||
<div className={cssStyle.text}><Translatable>Your bookmarks</Translatable></div>
|
||||
<div className={cssStyle.containerButton}>
|
||||
<div
|
||||
className={cssStyle.button}
|
||||
title={useTr("Add new bookmark")}
|
||||
onClick={() => events.fire("action_create_bookmark", { entryType: "bookmark", order: { type: "selected" }, displayName: undefined })}
|
||||
>
|
||||
<ClientIconRenderer icon={ClientIcon.BookmarkAdd} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BookmarkList />
|
||||
<div className={cssStyle.buttons}>
|
||||
<Button onClick={() => events.fire("action_export")}>
|
||||
<Translatable>Export</Translatable>
|
||||
</Button>
|
||||
<Button onClick={() => events.fire("action_import")}>
|
||||
<Translatable>Import</Translatable>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const SelectedBookmarkBanner = React.memo(() => {
|
||||
const bookmarkInfo = useContext(SelectedBookmarkInfoContext);
|
||||
|
||||
if(!bookmarkInfo?.hostBannerUrl) {
|
||||
return (
|
||||
<img key={"default"} alt={""} src={DefaultHeaderImage} className={cssStyle.hostBanner} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cssStyle.hostBanner + " " + cssStyle.individual} key={"server"}>
|
||||
<HostBannerRenderer
|
||||
key={"hostbanner"}
|
||||
banner={{
|
||||
imageUrl: bookmarkInfo.hostBannerUrl,
|
||||
linkUrl: undefined,
|
||||
mode: "resize-ratio",
|
||||
updateInterval: 0
|
||||
}}
|
||||
className={cssStyle.renderer}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const SelectedBookmarkName = React.memo(() => {
|
||||
const refEditPanel = useRef<HTMLDivElement>();
|
||||
const selectedBookmarkId = useContext(SelectedBookmarkIdContext);
|
||||
const variables = useContext(VariableContext);
|
||||
const nameVariable = variables.useVariable("bookmarkName", selectedBookmarkId.id);
|
||||
let [ editMode, setEditMode ] = useDependentState(() => false, [ selectedBookmarkId.id ]);
|
||||
if(selectedBookmarkId.type === "empty") {
|
||||
editMode = false;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if(refEditPanel.current) {
|
||||
refEditPanel.current.textContent = nameVariable.localValue;
|
||||
refEditPanel.current.focus();
|
||||
}
|
||||
}, [ editMode ]);
|
||||
|
||||
if(nameVariable.status === "loading") {
|
||||
return (
|
||||
<div key={"name-loading"} className={cssStyle.containerName}>
|
||||
<div className={cssStyle.name}><Translatable>loading</Translatable> <LoadingDots /></div>
|
||||
</div>
|
||||
);
|
||||
} else if(editMode) {
|
||||
return (
|
||||
<div key={"name-edit"} className={cssStyle.containerName + " " + cssStyle.editing}>
|
||||
<div
|
||||
ref={refEditPanel}
|
||||
className={cssStyle.name}
|
||||
contentEditable={true}
|
||||
onKeyDown={event => {
|
||||
if(event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
refEditPanel.current?.blur();
|
||||
} else if(event.key === "Backspace" || event.key === "Delete") {
|
||||
/* never prevent these */
|
||||
} else if(event.ctrlKey) {
|
||||
/* don't prevent this */
|
||||
} else if(event.currentTarget.textContent?.length >= 32) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
onInput={event => {
|
||||
const value = event.currentTarget.textContent;
|
||||
const valid = typeof value === "string" && value.length > 0 && value.length <= 32;
|
||||
refEditPanel.current?.classList.toggle(cssStyle.invalid, !valid);
|
||||
}}
|
||||
onBlur={() => {
|
||||
const value = refEditPanel.current?.textContent;
|
||||
setEditMode(false);
|
||||
|
||||
if(!value || value.length > 32) { return; }
|
||||
nameVariable.setValue(value);
|
||||
}}
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div key={"name-value"} className={cssStyle.containerName}>
|
||||
<div className={cssStyle.name}>{nameVariable.status === "applying" ? tr("applying") : nameVariable.localValue}</div>
|
||||
<div className={cssStyle.edit} onClick={() => setEditMode(true)}>
|
||||
<div className={cssStyle.button}>
|
||||
<ClientIconRenderer className={cssStyle.icon} icon={ClientIcon.BookmarkEditName} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})
|
||||
|
||||
const SelectedBookmarkHeader = React.memo(() => {
|
||||
const selectedBookmarkId = useContext(SelectedBookmarkIdContext);
|
||||
const variables = useContext(VariableContext);
|
||||
const addressVariable = variables.useReadOnly("bookmarkServerAddress", selectedBookmarkId.id);
|
||||
|
||||
let address;
|
||||
if(selectedBookmarkId.type === "bookmark") {
|
||||
if(addressVariable.status === "loading") {
|
||||
address = <React.Fragment key={"address-loading"}><Translatable>loading</Translatable> <LoadingDots /></React.Fragment>
|
||||
} else {
|
||||
address = <React.Fragment key={"address-value"}>{addressVariable.value}</React.Fragment>;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cssStyle.header}>
|
||||
<SelectedBookmarkName />
|
||||
<div className={cssStyle.containerAddress}>{address}</div>
|
||||
<SelectedBookmarkBanner />
|
||||
</div>
|
||||
)
|
||||
});
|
||||
|
||||
const BookmarkSettingsGroup = React.memo((props: { children, className?: string }) => {
|
||||
return (
|
||||
<div className={cssStyle.group + " " + props.className}>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
});
|
||||
|
||||
const BookmarkSetting = React.memo((props: { children: [React.ReactNode, React.ReactNode] }) => {
|
||||
return (
|
||||
<div className={cssStyle.row}>
|
||||
<div className={cssStyle.key}>
|
||||
{props.children[0]}
|
||||
</div>
|
||||
<div className={cssStyle.value}>
|
||||
{props.children[1]}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
});
|
||||
|
||||
const BookmarkSettingConnectProfile = () => {
|
||||
const selectedBookmark = useContext(SelectedBookmarkIdContext);
|
||||
const variables = useContext(VariableContext);
|
||||
const selectedProfile = variables.useVariable("bookmarkConnectProfile", selectedBookmark.id);
|
||||
const availableProfiles = variables.useReadOnly("connectProfiles");
|
||||
|
||||
let value;
|
||||
const profiles = [];
|
||||
let invalid = false;
|
||||
|
||||
if(selectedBookmark.type !== "bookmark") {
|
||||
value = "empty";
|
||||
} else if(availableProfiles.status !== "loaded") {
|
||||
value = "loading";
|
||||
} else if(selectedProfile.status === "loading") {
|
||||
value = "loading";
|
||||
} else {
|
||||
value = selectedProfile.localValue;
|
||||
|
||||
profiles.push(...availableProfiles.value.map(entry => (
|
||||
<option key={"profile-" + entry.id} value={entry.id}>{entry.name}</option>
|
||||
)));
|
||||
|
||||
if(availableProfiles.value.findIndex(entry => entry.id === selectedProfile.localValue) === -1) {
|
||||
invalid = true;
|
||||
profiles.push(
|
||||
<option key={"profile-" + selectedProfile.localValue} value={selectedProfile.localValue} style={{ display: "none" }}>{useTr("Unknown profile") + ": " + selectedProfile.localValue}</option>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ControlledSelect
|
||||
type={"boxed"}
|
||||
value={value}
|
||||
disabled={availableProfiles.status !== "loaded" || selectedProfile.status !== "loaded" || selectedBookmark.type !== "bookmark"}
|
||||
onChange={event => selectedProfile.setValue(event.target.value)}
|
||||
invalid={invalid}
|
||||
>
|
||||
<option key={"empty"} value={"empty"} style={{ display: "none" }} />
|
||||
<option key={"loading"} value={"loading"} style={{ display: "none" }}>{useTr("loading")}</option>
|
||||
<option key={"applying-value"} value={"applying-value"} style={{ display: "none" }}>{useTr("applying")}</option>
|
||||
{profiles as any}
|
||||
</ControlledSelect>
|
||||
);
|
||||
};
|
||||
|
||||
const BookmarkSettingAutoConnect = () => {
|
||||
const selectedBookmark = useContext(SelectedBookmarkIdContext);
|
||||
const variables = useContext(VariableContext);
|
||||
const value = variables.useVariable("bookmarkConnectOnStartup", selectedBookmark.id, false);
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
onChange={newValue => value.setValue(newValue)}
|
||||
value={value.localValue}
|
||||
disabled={value.status !== "loaded" || selectedBookmark.type !== "bookmark"}
|
||||
label={<Translatable>Automatically connect to server on client start</Translatable>}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const BookmarkSettingServerAddress = React.memo(() => {
|
||||
const selectedBookmark = useContext(SelectedBookmarkIdContext);
|
||||
const variables = useContext(VariableContext);
|
||||
const value = variables.useVariable("bookmarkServerAddress", selectedBookmark.id);
|
||||
|
||||
return (
|
||||
<ControlledBoxedInputField
|
||||
value={value.localValue}
|
||||
disabled={selectedBookmark.type !== "bookmark" || value.status !== "loaded"}
|
||||
onChange={newValue => value.setValue(newValue, true)}
|
||||
onBlur={() => value.setValue(value.localValue)}
|
||||
finishOnEnter={true}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const BookmarkSettingPassword = React.memo((props: { field: "bookmarkServerPassword" | "bookmarkDefaultChannelPassword", disabled: boolean }) => {
|
||||
const selectedBookmark = useContext(SelectedBookmarkIdContext);
|
||||
const variables = useContext(VariableContext);
|
||||
const value = variables.useVariable(props.field, selectedBookmark.id);
|
||||
|
||||
let placeholder = "", inputValue = "";
|
||||
if(props.disabled) {
|
||||
/* disabled, show nothing */
|
||||
} else if(value.status === "loaded") {
|
||||
if(value.localValue && value.localValue === value.remoteValue) {
|
||||
placeholder = tr("password hidden");
|
||||
} else {
|
||||
inputValue = value.localValue;
|
||||
}
|
||||
} else if(value.status === "applying") {
|
||||
if(value.localValue) {
|
||||
placeholder = tr("hashing password");
|
||||
} else {
|
||||
/* we've resetted the password. Don't show "hashing password" */
|
||||
}
|
||||
}
|
||||
|
||||
const disabled = props.disabled || selectedBookmark.type !== "bookmark" || value.status !== "loaded";
|
||||
return (
|
||||
<ControlledBoxedInputField
|
||||
value={inputValue}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
onChange={newValue => value.setValue(newValue, true)}
|
||||
onBlur={() => value.setValue(value.localValue)}
|
||||
rightIcon={() => (
|
||||
<div className={cssStyle.inputIconContainer + " " + (disabled ? "" : cssStyle.enabled)}>
|
||||
<div className={cssStyle.iconContainer} onClick={() => !disabled && value.setValue("")}>
|
||||
<ClientIconRenderer icon={ClientIcon.Refresh} title={useTr("Reset password")} className={cssStyle.icon} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
finishOnEnter={true}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const BookmarkSettingChannel = React.memo(() => {
|
||||
const selectedBookmark = useContext(SelectedBookmarkIdContext);
|
||||
const variables = useContext(VariableContext);
|
||||
const defaultChannel = variables.useVariable("bookmarkDefaultChannel", selectedBookmark.id);
|
||||
const currentClientChannel = variables.useReadOnly("currentClientChannel", selectedBookmark.id);
|
||||
|
||||
const inputDisabled = selectedBookmark.type !== "bookmark" || defaultChannel.status !== "loaded";
|
||||
const channelSelectDisabled = inputDisabled || currentClientChannel.status !== "loaded" || !currentClientChannel.value;
|
||||
|
||||
let selectCurrentTitle;
|
||||
if(channelSelectDisabled) {
|
||||
selectCurrentTitle = tr("Select current channel.\nYou're not connected to the target server.");
|
||||
} else {
|
||||
selectCurrentTitle = tr("Select current channel") + ":\n" + currentClientChannel.value?.name;
|
||||
selectCurrentTitle += "\n\n" + tr("Shift click to use the channel name path.");
|
||||
}
|
||||
|
||||
return (
|
||||
<ControlledBoxedInputField
|
||||
value={defaultChannel.localValue}
|
||||
disabled={inputDisabled}
|
||||
onChange={newValue => defaultChannel.setValue(newValue, true)}
|
||||
onBlur={() => defaultChannel.setValue(defaultChannel.localValue)}
|
||||
rightIcon={() => (
|
||||
<div className={cssStyle.inputIconContainer + " " + (channelSelectDisabled ? "" : cssStyle.enabled)}>
|
||||
<div
|
||||
title={selectCurrentTitle}
|
||||
className={cssStyle.iconContainer}
|
||||
onClick={event => {
|
||||
if(currentClientChannel.status !== "loaded") {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!currentClientChannel.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(event.shiftKey) {
|
||||
defaultChannel.setValue(currentClientChannel.value.path);
|
||||
} else {
|
||||
defaultChannel.setValue("/" + currentClientChannel.value.channelId);
|
||||
}
|
||||
variables.setVariable("bookmarkDefaultChannelPassword", selectedBookmark.id, currentClientChannel.value.passwordHash);
|
||||
}}
|
||||
>
|
||||
<ClientIconRenderer icon={ClientIcon.ChannelEdit} className={cssStyle.icon} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
finishOnEnter={true}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const BookmarkSettingChannelPassword = () => {
|
||||
const selectedBookmark = useContext(SelectedBookmarkIdContext);
|
||||
const variables = useContext(VariableContext);
|
||||
const value = variables.useReadOnly("bookmarkDefaultChannel", selectedBookmark.id, undefined);
|
||||
return <BookmarkSettingPassword field={"bookmarkDefaultChannelPassword"} disabled={!value} />;
|
||||
}
|
||||
|
||||
const BookmarkInfoRenderer = React.memo(() => {
|
||||
const bookmarkInfo = useContext(SelectedBookmarkInfoContext);
|
||||
let connectCount = bookmarkInfo ? Math.max(bookmarkInfo.connectCountUniqueId, bookmarkInfo.connectCountAddress) : -1;
|
||||
|
||||
return (
|
||||
<div className={cssStyle.group + " " + cssStyle.connectInfoContainer}>
|
||||
<div className={cssStyle.containerImage}>
|
||||
<img src={ServerInfoImage} alt={""} />
|
||||
</div>
|
||||
<div className={cssStyle.containerProperties}>
|
||||
<div className={cssStyle.row}>
|
||||
<div className={cssStyle.key}>{useTr("Server name")}</div>
|
||||
<div className={cssStyle.value}>
|
||||
{bookmarkInfo?.serverName}
|
||||
</div>
|
||||
</div>
|
||||
<div className={cssStyle.row}>
|
||||
<div className={cssStyle.key}>{useTr("Server region")}</div>
|
||||
<div className={cssStyle.value}>
|
||||
<CountryIcon country={bookmarkInfo?.serverRegion} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={cssStyle.row}>
|
||||
<div className={cssStyle.key}>{useTr("Last ping")}</div>
|
||||
<div className={cssStyle.value}>
|
||||
{useTr("Not yet supported")}
|
||||
</div>
|
||||
</div>
|
||||
<div className={cssStyle.row}>
|
||||
<div className={cssStyle.key}>{useTr("Last client count")}</div>
|
||||
<div className={cssStyle.value}>
|
||||
{bookmarkInfo?.clientsOnline} / {bookmarkInfo?.clientsMax}
|
||||
</div>
|
||||
</div>
|
||||
<div className={cssStyle.row}>
|
||||
<div className={cssStyle.key}>{useTr("Connection count")}</div>
|
||||
<div className={cssStyle.value + " " + cssStyle.valueConnectCount}>
|
||||
<div className={cssStyle.text}>{connectCount === -1 ? tr("fetch error") : connectCount}</div>
|
||||
<IconTooltip className={cssStyle.tooltipIcon}>
|
||||
<div style={{ width: "20em" }}>
|
||||
<VariadicTranslatable text={"Connections to the server unique id: {}"}>
|
||||
{bookmarkInfo?.connectCountUniqueId}
|
||||
</VariadicTranslatable>
|
||||
<br />
|
||||
<VariadicTranslatable text={"Connections to the address: {}"}>
|
||||
{bookmarkInfo?.connectCountAddress}
|
||||
</VariadicTranslatable>
|
||||
</div>
|
||||
</IconTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cssStyle.overlay + " " + (connectCount === -1 ? cssStyle.shown : "")}>
|
||||
<div className={cssStyle.text}>
|
||||
<Translatable>You never connected to that server.</Translatable>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const BookmarkInfoContainerInner = React.memo(() => (
|
||||
<div className={cssStyle.infoContainer}>
|
||||
<SelectedBookmarkHeader />
|
||||
<div className={cssStyle.containerSettings}>
|
||||
<BookmarkSettingsGroup>
|
||||
<BookmarkSetting>
|
||||
<Translatable>Connect profile</Translatable>
|
||||
<BookmarkSettingConnectProfile />
|
||||
</BookmarkSetting>
|
||||
<BookmarkSettingAutoConnect />
|
||||
</BookmarkSettingsGroup>
|
||||
<BookmarkSettingsGroup>
|
||||
<BookmarkSetting>
|
||||
<Translatable>Server Address</Translatable>
|
||||
<BookmarkSettingServerAddress />
|
||||
</BookmarkSetting>
|
||||
<BookmarkSetting>
|
||||
<Translatable>Server Password</Translatable>
|
||||
<BookmarkSettingPassword field={"bookmarkServerPassword"} disabled={false} />
|
||||
</BookmarkSetting>
|
||||
<BookmarkSetting>
|
||||
<Translatable>Default Channel</Translatable>
|
||||
<BookmarkSettingChannel />
|
||||
</BookmarkSetting>
|
||||
<BookmarkSetting>
|
||||
<Translatable>Channel password</Translatable>
|
||||
<BookmarkSettingChannelPassword />
|
||||
</BookmarkSetting>
|
||||
</BookmarkSettingsGroup>
|
||||
<BookmarkInfoRenderer />
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
|
||||
const BookmarkInfoContainer = React.memo(() => {
|
||||
const variables = useContext(VariableContext);
|
||||
const selectedBookmark = variables.useReadOnly("bookmarkSelected", undefined, { type: "empty", id: undefined });
|
||||
const selectedBookmarkInfo = variables.useReadOnly("bookmarkInfo", selectedBookmark.id, undefined);
|
||||
|
||||
return (
|
||||
<SelectedBookmarkIdContext.Provider value={selectedBookmark as any}>
|
||||
<SelectedBookmarkInfoContext.Provider value={selectedBookmarkInfo}>
|
||||
<BookmarkInfoContainerInner />
|
||||
</SelectedBookmarkInfoContext.Provider>
|
||||
</SelectedBookmarkIdContext.Provider>
|
||||
)
|
||||
});
|
||||
|
||||
class ModalBookmarks extends AbstractModal {
|
||||
readonly events: Registry<ModalBookmarkEvents>;
|
||||
readonly variables: UiVariableConsumer<ModalBookmarkVariables>;
|
||||
|
||||
constructor(events: IpcRegistryDescription<ModalBookmarkEvents>, variables: IpcVariableDescriptor<ModalBookmarkVariables>) {
|
||||
super();
|
||||
|
||||
this.events = Registry.fromIpcDescription(events);
|
||||
this.variables = createIpcUiVariableConsumer(variables);
|
||||
|
||||
this.events.on("action_create_bookmark", event => {
|
||||
if(event.displayName) {
|
||||
return;
|
||||
}
|
||||
|
||||
createInputModal(tr("Please enter a name"), tr("Please enter the bookmark name"), input => input.length > 0, value => {
|
||||
if(typeof value !== "string" || !value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.events.fire("action_create_bookmark", {
|
||||
entryType: event.entryType,
|
||||
order: event.order,
|
||||
displayName: value
|
||||
});
|
||||
}).open();
|
||||
});
|
||||
|
||||
this.events.on("action_duplicate_bookmark", event => {
|
||||
if(event.displayName) {
|
||||
return;
|
||||
}
|
||||
|
||||
createInputModal(tr("Please enter a name"), tr("Please enter the new bookmark name"), input => input.length > 0, value => {
|
||||
if(typeof value !== "string" || !value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.events.fire("action_duplicate_bookmark", {
|
||||
displayName: value,
|
||||
uniqueId: event.uniqueId,
|
||||
originalName: event.originalName
|
||||
});
|
||||
}, {
|
||||
defaultValue: event.originalName + " (Copy)"
|
||||
}).open();
|
||||
});
|
||||
|
||||
this.events.on("notify_export_data", event => {
|
||||
downloadTextAsFile(event.payload, "bookmarks.json");
|
||||
});
|
||||
|
||||
this.events.on("action_import", event => {
|
||||
if(event.payload) {
|
||||
return;
|
||||
}
|
||||
|
||||
requestFileAsText().then(payload => {
|
||||
if(payload.length === 0) {
|
||||
this.events.fire("notify_import_result", { status: "error", message: tr("File payload is empty") });
|
||||
return;
|
||||
}
|
||||
|
||||
this.events.fire("action_import", { payload: payload });
|
||||
});
|
||||
})
|
||||
|
||||
this.events.on("notify_import_result", event => {
|
||||
switch (event.status) {
|
||||
case "error":
|
||||
createErrorModal(tr("Failed to import bookmarks"), tr("Failed to import bookmarks:") + "\n" + event.message).open();
|
||||
break;
|
||||
|
||||
case "success":
|
||||
createInfoModal(tr("Successfully imported"), formatMessage(tr("Successfully imported {0} bookmarks."), event.importedBookmarks)).open();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
this.events.destroy();
|
||||
this.variables.destroy();
|
||||
}
|
||||
|
||||
renderBody(): React.ReactElement {
|
||||
return (
|
||||
<EventContext.Provider value={this.events}>
|
||||
<VariableContext.Provider value={this.variables}>
|
||||
<div className={cssStyle.container}>
|
||||
<BookmarkListContainer />
|
||||
<ContextDivider id={"separator-bookmarks"} direction={"horizontal"} defaultValue={25} />
|
||||
<BookmarkInfoContainer />
|
||||
</div>
|
||||
</VariableContext.Provider>
|
||||
</EventContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
renderTitle(): string | React.ReactElement {
|
||||
return <Translatable>Manage bookmarks</Translatable>;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export = ModalBookmarks;
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
BIN
shared/js/ui/modal/bookmarks/serverinfo.png
Normal file
BIN
shared/js/ui/modal/bookmarks/serverinfo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 79 KiB |
|
@ -17,7 +17,7 @@ import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
|||
import {server_connections} from "tc-shared/ConnectionManager";
|
||||
import {parseServerAddress} from "tc-shared/tree/Server";
|
||||
import {spawnSettingsModal} from "tc-shared/ui/modal/ModalSettings";
|
||||
import * as ipRegex from "ip-regex";
|
||||
import ipRegex from "ip-regex";
|
||||
import {UiVariableProvider} from "tc-shared/ui/utils/Variable";
|
||||
import {createIpcUiVariableProvider} from "tc-shared/ui/utils/IpcVariable";
|
||||
import {spawnModal} from "tc-shared/ui/react-elements/modal";
|
||||
|
|
|
@ -311,17 +311,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.countryContainer {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
|
||||
:global(.country) {
|
||||
align-self: center;
|
||||
margin-right: .25em;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 55rem) {
|
||||
.container {
|
||||
padding: .5em!important;
|
||||
|
|
|
@ -17,6 +17,7 @@ import {RemoteIconRenderer} from "tc-shared/ui/react-elements/Icon";
|
|||
import {UiVariableConsumer} from "tc-shared/ui/utils/Variable";
|
||||
import {createIpcUiVariableConsumer, IpcVariableDescriptor} from "tc-shared/ui/utils/IpcVariable";
|
||||
import {AbstractModal} from "tc-shared/ui/react-elements/ModalDefinitions";
|
||||
import {CountryIcon} from "tc-shared/ui/react-elements/CountryIcon";
|
||||
|
||||
const EventContext = React.createContext<Registry<ConnectUiEvents>>(undefined);
|
||||
const VariablesContext = React.createContext<UiVariableConsumer<ConnectUiVariables>>(undefined);
|
||||
|
@ -241,15 +242,6 @@ const ButtonContainer = () => (
|
|||
</div>
|
||||
);
|
||||
|
||||
const CountryIcon = (props: { country: string }) => {
|
||||
return (
|
||||
<div className={cssStyle.countryContainer}>
|
||||
<div className={"country flag-" + props.country} />
|
||||
{i18n.country_name(props.country, useTr("Global"))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const HistoryTableEntryConnectCount = React.memo((props: { entry: ConnectHistoryEntry }) => {
|
||||
const targetType = props.entry.uniqueServerId === kUnknownHistoryServerUniqueId ? "address" : "server-unique-id";
|
||||
const target = targetType === "address" ? props.entry.targetAddress : props.entry.uniqueServerId;
|
||||
|
|
|
@ -9,6 +9,7 @@ import {Checkbox} from "tc-shared/ui/react-elements/Checkbox";
|
|||
import {Button} from "tc-shared/ui/react-elements/Button";
|
||||
import {createErrorModal, createInfoModal} from "tc-shared/ui/elements/Modal";
|
||||
import {AbstractModal} from "tc-shared/ui/react-elements/ModalDefinitions";
|
||||
import {downloadTextAsFile, requestFileAsText} from "tc-shared/file/Utils";
|
||||
|
||||
const cssStyle = require("./Renderer.scss");
|
||||
|
||||
|
@ -358,36 +359,6 @@ const CssVariableEditor = (props: { events: Registry<CssEditorEvents> }) => {
|
|||
</div>
|
||||
)
|
||||
};
|
||||
const downloadTextAsFile = (text, name) => {
|
||||
const element = document.createElement("a");
|
||||
element.text = "download";
|
||||
element.href = "data:test/plain;charset=utf-8," + encodeURIComponent(text);
|
||||
element.download = name;
|
||||
element.style.display = "none";
|
||||
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
element.remove();
|
||||
};
|
||||
|
||||
const requestFileAsText = async (): Promise<string> => {
|
||||
const element = document.createElement("input");
|
||||
element.style.display = "none";
|
||||
element.type = "file";
|
||||
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
await new Promise(resolve => {
|
||||
element.onchange = resolve;
|
||||
});
|
||||
|
||||
if (element.files.length !== 1)
|
||||
return undefined;
|
||||
const file = element.files[0];
|
||||
element.remove();
|
||||
|
||||
return await file.text();
|
||||
};
|
||||
|
||||
class PopoutConversationUI extends AbstractModal {
|
||||
private readonly events: Registry<CssEditorEvents>;
|
||||
|
|
|
@ -14,7 +14,7 @@ import {copyToClipboard} from "tc-shared/utils/helpers";
|
|||
import {ControlledBoxedInputField, ControlledSelect} from "tc-shared/ui/react-elements/InputField";
|
||||
import {useTr} from "tc-shared/ui/react-elements/Helper";
|
||||
import {Checkbox} from "tc-shared/ui/react-elements/Checkbox";
|
||||
import * as moment from 'moment';
|
||||
import moment from 'moment';
|
||||
|
||||
const cssStyle = require("./Renderer.scss");
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import {network} from "tc-shared/ui/frames/chat";
|
|||
import {Table, TableColumn, TableRow, TableRowElement} from "tc-shared/ui/react-elements/Table";
|
||||
import {ReactComponentBase} from "tc-shared/ui/react-elements/ReactComponentBase";
|
||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import * as Moment from "moment";
|
||||
import moment from "moment";
|
||||
import {MenuEntryType, spawn_context_menu} from "tc-shared/ui/elements/ContextMenu";
|
||||
import {BoxedInputField} from "tc-shared/ui/react-elements/InputField";
|
||||
import {LogCategory, logWarn} from "tc-shared/log";
|
||||
|
@ -1043,7 +1043,7 @@ export class FileBrowserRenderer extends ReactComponentBase<FileListTablePropert
|
|||
"name": () => <FileName path={this.currentPath} file={directory}/>,
|
||||
"type": () => <a key={"type"}><Translatable>Directory</Translatable></a>,
|
||||
"change-date": () => directory.datetime ?
|
||||
<a>{Moment(directory.datetime).format("DD/MM/YYYY HH:mm")}</a> : undefined
|
||||
<a>{moment(directory.datetime).format("DD/MM/YYYY HH:mm")}</a> : undefined
|
||||
},
|
||||
className: cssStyle.directoryEntry,
|
||||
userData: directory
|
||||
|
@ -1057,7 +1057,7 @@ export class FileBrowserRenderer extends ReactComponentBase<FileListTablePropert
|
|||
"size": () => <FileSize path={this.currentPath} file={file}/>,
|
||||
"type": () => <a key={"type"}><Translatable>File</Translatable></a>,
|
||||
"change-date": () => file.datetime ?
|
||||
<a key={"date"}>{Moment(file.datetime).format("DD/MM/YYYY HH:mm")}</a> : undefined
|
||||
<a key={"date"}>{moment(file.datetime).format("DD/MM/YYYY HH:mm")}</a> : undefined
|
||||
},
|
||||
className: cssStyle.directoryEntry,
|
||||
userData: file
|
||||
|
|
|
@ -5,7 +5,7 @@ html:root {
|
|||
--checkbox-checkmark: #46c0ec;
|
||||
|
||||
--checkbox-background: #272626;
|
||||
--checkbox-disabled-background: #1a1a1e;
|
||||
--checkbox-disabled-background: #1a1819;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
|
@ -77,6 +77,9 @@ html:root {
|
|||
label.disabled > .checkbox, .checkbox:disabled, .checkbox.disabled {
|
||||
&.checkbox, > .checkbox {
|
||||
pointer-events: none!important;
|
||||
box-shadow: none;
|
||||
|
||||
border: 1px solid var(--boxed-input-field-border);
|
||||
background-color: var(--checkbox-disabled-background);
|
||||
}
|
||||
}
|
10
shared/js/ui/react-elements/CountryIcon.scss
Normal file
10
shared/js/ui/react-elements/CountryIcon.scss
Normal file
|
@ -0,0 +1,10 @@
|
|||
.countryContainer {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
|
||||
:global(.country) {
|
||||
align-self: center;
|
||||
margin-right: .25em;
|
||||
}
|
||||
}
|
15
shared/js/ui/react-elements/CountryIcon.tsx
Normal file
15
shared/js/ui/react-elements/CountryIcon.tsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
import * as i18n from "tc-shared/i18n/country";
|
||||
import {joinClassList, useTr} from "tc-shared/ui/react-elements/Helper";
|
||||
import * as React from "react";
|
||||
|
||||
const cssStyle = require("./CountryIcon.scss");
|
||||
|
||||
export const CountryIcon = (props: { country: string, className?: string }) => {
|
||||
const country = props.country || "xx";
|
||||
return (
|
||||
<div className={joinClassList(cssStyle.countryContainer, props.className)}>
|
||||
<div className={"country flag-" + country} />
|
||||
{i18n.country_name(country, useTr("Global"))}
|
||||
</div>
|
||||
)
|
||||
};
|
|
@ -50,7 +50,7 @@ export const ControlledBoxedInputField = (props: {
|
|||
props.className
|
||||
}
|
||||
onFocus={props.onFocus}
|
||||
onBlur={() => props.onBlur()}
|
||||
onBlur={props.onBlur}
|
||||
>
|
||||
{props.leftIcon ? props.leftIcon() : ""}
|
||||
{props.prefix ? <a key={"prefix"} className={cssStyle.prefix}>{props.prefix}</a> : undefined}
|
||||
|
@ -462,7 +462,8 @@ export const ControlledSelect = (props: {
|
|||
cssStyle["size-normal"],
|
||||
props.invalid ? cssStyle.isInvalid : undefined,
|
||||
props.className,
|
||||
cssStyle.noLeftIcon, cssStyle.noRightIcon
|
||||
cssStyle.noLeftIcon, cssStyle.noRightIcon,
|
||||
props.disabled ? cssStyle.disabled : undefined
|
||||
)}
|
||||
>
|
||||
{!props.label ? undefined :
|
||||
|
|
|
@ -51,4 +51,9 @@ html:root {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
|
@ -151,8 +151,8 @@ export class Tooltip extends React.Component<TooltipProperties, TooltipState> {
|
|||
|
||||
export const IconTooltip = (props: { children?: React.ReactElement | React.ReactElement[], className?: string }) => (
|
||||
<Tooltip tooltip={() => props.children}>
|
||||
<div className={cssStyle.tooltip + " " + props.className}>
|
||||
<img src="img/icon_tooltip.svg"/>
|
||||
<div className={cssStyle.iconTooltip + " " + props.className}>
|
||||
<img src="img/icon_tooltip.svg" alt={""} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
|
|
@ -26,11 +26,22 @@ type UiVariableEditorPromise<Variables extends UiVariableMap, T extends keyof Va
|
|||
export abstract class UiVariableProvider<Variables extends UiVariableMap> {
|
||||
private variableProvider: {[key: string]: (customData: any) => any | Promise<any>} = {};
|
||||
private variableEditor: {[key: string]: (newValue, customData: any) => any | Promise<any>} = {};
|
||||
private artificialDelay: number;
|
||||
|
||||
protected constructor() { }
|
||||
protected constructor() {
|
||||
this.artificialDelay = 0;
|
||||
}
|
||||
|
||||
destroy() { }
|
||||
|
||||
getArtificialDelay() : number {
|
||||
return this.artificialDelay;
|
||||
}
|
||||
|
||||
setArtificialDelay(value: number) {
|
||||
this.artificialDelay = value;
|
||||
}
|
||||
|
||||
setVariableProvider<T extends keyof Variables>(variable: T, provider: (customData: any) => Variables[T] | Promise<Variables[T]>) {
|
||||
this.variableProvider[variable as any] = provider;
|
||||
}
|
||||
|
@ -60,14 +71,23 @@ export abstract class UiVariableProvider<Variables extends UiVariableMap> {
|
|||
}
|
||||
|
||||
const result = providers(customData);
|
||||
if(result instanceof Promise) {
|
||||
return result
|
||||
.then(result => this.doSendVariable(variable as any, customData, result))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
const handleResult = result => {
|
||||
if(result instanceof Promise) {
|
||||
return result
|
||||
.then(result => this.doSendVariable(variable as any, customData, result))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
} else {
|
||||
this.doSendVariable(variable as any, customData, result);
|
||||
}
|
||||
};
|
||||
|
||||
if(this.artificialDelay > 0) {
|
||||
return new Promise(resolve => setTimeout(resolve, this.artificialDelay)).then(() => handleResult(result));
|
||||
} else {
|
||||
this.doSendVariable(variable as any, customData, result);
|
||||
return handleResult(result);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -229,6 +249,14 @@ export abstract class UiVariableConsumer<Variables extends UiVariableMap> {
|
|||
}
|
||||
}
|
||||
|
||||
setVariable<T extends keyof WriteableVariables<Variables>>(
|
||||
variable: T,
|
||||
customData: any,
|
||||
newValue: Variables[T]
|
||||
) {
|
||||
this.doEditVariable(variable as any, customData, newValue);
|
||||
}
|
||||
|
||||
useVariable<T extends keyof WriteableVariables<Variables>>(
|
||||
variable: T,
|
||||
customData?: any,
|
||||
|
|
5
shared/svg-sprites/client-icons.d.ts
vendored
5
shared/svg-sprites/client-icons.d.ts
vendored
File diff suppressed because one or more lines are too long
|
@ -7,6 +7,7 @@
|
|||
"declarationDir": "../../declarations/shared-app/",
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"esModuleInterop": true,
|
||||
"baseUrl": "../../",
|
||||
"paths": {
|
||||
"tc-shared/*": ["shared/js/*"],
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
"module": "commonjs",
|
||||
"sourceMap": true,
|
||||
"experimentalDecorators": true,
|
||||
"esModuleInterop": true,
|
||||
"plugins": [ /* ttypescript */
|
||||
{
|
||||
"transform": "../../tools/trgen/ttsc_transformer.js",
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
"lib": ["ES7", "dom", "dom.iterable"],
|
||||
"removeComments": true, /* we dont really need them within the target files */
|
||||
"jsx": "react",
|
||||
"esModuleInterop": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"tc-shared/*": ["shared/js/*"],
|
||||
|
|
|
@ -224,7 +224,11 @@ export const config = async (target: "web" | "client"): Promise<Configuration> =
|
|||
options: {
|
||||
esModule: false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg|jpeg|gif)?$/,
|
||||
loader: 'file-loader',
|
||||
},
|
||||
]
|
||||
} as any,
|
||||
resolve: {
|
||||
|
|
Loading…
Add table
Reference in a new issue