Reworked the menu bar and added a backend specific interface for backend specific functions

canary
WolverinDEV 2020-10-05 12:49:13 +02:00
parent df73618bd0
commit 82a5b1127f
20 changed files with 773 additions and 595 deletions

View File

@ -1,6 +1,6 @@
import {ConnectionHandler, DisconnectReason} from "./ConnectionHandler";
import {Registry} from "./events";
import * as top_menu from "./ui/frames/MenuBar";
import * as top_menu from "./ui/frames/MenuBarOld";
import * as loader from "tc-loader";
import {Stage} from "tc-loader";
@ -107,8 +107,6 @@ export class ConnectionManager {
});
old_handler?.events().fire("notify_visibility_changed", { visible: false });
handler?.events().fire("notify_visibility_changed", { visible: true });
top_menu.update_state(); //FIXME: Top menu should listen to our events!
}
findConnection(handlerId: string) : ConnectionHandler | undefined {

View File

@ -0,0 +1,9 @@
export interface NativeClientBackend {
openChangeLog() : void;
openClientUpdater() : void;
quit() : void;
showDeveloperOptions() : boolean;
openDeveloperTools() : void;
reloadWindow() : void;
}

View File

@ -0,0 +1,3 @@
export interface WebClientBackend {
}

View File

@ -0,0 +1,26 @@
import {NativeClientBackend} from "tc-shared/backend/NativeClient";
import {WebClientBackend} from "tc-shared/backend/WebClient";
let backend;
export function getBackend(target: "native") : NativeClientBackend;
export function getBackend(target: "web") : WebClientBackend;
export function getBackend(target) {
if(__build.target === "client") {
if(target !== "native") {
throw "invalid target, expected native";
}
} else if(__build.target === "web") {
if(target !== "web") {
throw "invalid target, expected web";
}
} else {
throw "invalid/unexpected build target";
}
return backend;
}
export function setBackend(instance: NativeClientBackend | WebClientBackend) {
backend = instance;
}

View File

@ -4,7 +4,7 @@ import {guid} from "./crypto/uid";
import {createErrorModal, createInfoModal, createInputModal} from "./ui/elements/Modal";
import {defaultConnectProfile, findConnectProfile} from "./profiles/ConnectionProfile";
import {spawnConnectModal} from "./ui/modal/ModalConnect";
import * as top_menu from "./ui/frames/MenuBar";
import * as top_menu from "./ui/frames/MenuBarOld";
import {ConnectionHandler} from "./ConnectionHandler";
import {server_connections} from "tc-shared/ConnectionManager";
import {Registry} from "tc-shared/events";
@ -257,7 +257,6 @@ export function add_server_to_bookmarks(server: ConnectionHandler) {
}, name);
save_bookmark(bookmark);
top_menu.rebuild_bookmarks();
createInfoModal(tr("Server added"), tr("Server has been successfully added to your bookmarks.")).open();
}
}).open();

View File

@ -15,6 +15,7 @@ import {tr} from "../i18n/localize";
import {spawnGlobalSettingsEditor} from "tc-shared/ui/modal/global-settings-editor/Controller";
import {spawnModalCssVariableEditor} from "tc-shared/ui/modal/css-editor/Controller";
import {server_connections} from "tc-shared/ConnectionManager";
import {spawnAbout} from "tc-shared/ui/modal/ModalAbout";
/*
function initialize_sounds(event_registry: Registry<ClientGlobalControlEvents>) {
@ -150,6 +151,10 @@ export function initialize(event_registry: Registry<ClientGlobalControlEvents>)
spawnGlobalSettingsEditor();
break;
case "about":
spawnAbout();
break;
default:
console.warn(tr("Received open window event for an unknown window: %s"), event.window);
}
@ -164,4 +169,8 @@ export function initialize(event_registry: Registry<ClientGlobalControlEvents>)
event_registry.on("action_open_window_settings", event => {
spawnSettingsModal(event.defaultCategory);
});
event_registry.on("action_open_window_permissions", event => {
spawnPermissionEditorModal(event.connection ? event.connection : server_connections.active_connection(), event.defaultTab);
});
}

View File

@ -1,11 +1,13 @@
import {ConnectionHandler} from "../ConnectionHandler";
import {Registry} from "../events";
export type PermissionEditorTab = "groups-server" | "groups-channel" | "channel" | "client" | "client-channel";
export interface ClientGlobalControlEvents {
/* open a basic window */
action_open_window: {
window:
"settings" | /* use action_open_window_settings! */
"about" |
"settings-registry" |
"css-variable-editor" |
"bookmark-manage" |
@ -29,10 +31,15 @@ export interface ClientGlobalControlEvents {
/* some more specific window openings */
action_open_window_connect: {
newTab: boolean
}
},
action_open_window_settings: {
defaultCategory?: string
},
action_open_window_permissions: {
connection?: ConnectionHandler,
defaultTab: PermissionEditorTab
}
}

View File

@ -13,7 +13,7 @@ import * as stats from "./stats";
import * as fidentity from "./profiles/identities/TeaForumIdentity";
import {defaultRecorder, RecorderProfile, setDefaultRecorder} from "tc-shared/voice/RecorderProfile";
import {spawnConnectModal} from "tc-shared/ui/modal/ModalConnect";
import * as top_menu from "./ui/frames/MenuBar";
import * as top_menu from "./ui/frames/MenuBarOld";
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
import {formatMessage} from "tc-shared/ui/frames/chat";
import {openModalNewcomer} from "tc-shared/ui/modal/ModalNewcomer";
@ -29,6 +29,7 @@ import {MenuEntryType, spawn_context_menu} from "tc-shared/ui/elements/ContextMe
import {copy_to_clipboard} from "tc-shared/utils/helpers";
import {checkForUpdatedApp} from "tc-shared/update";
import {setupJSRender} from "tc-shared/ui/jsrender";
import {ConnectRequestData} from "tc-shared/ipc/ConnectHandler";
import "svg-sprites/client-icons";
/* required import for init */
@ -38,11 +39,11 @@ import "./ui/elements/ContextDivider";
import "./ui/elements/Tab";
import "./connection/CommandHandler";
import "./connection/ConnectionBase";
import {ConnectRequestData} from "tc-shared/ipc/ConnectHandler";
import "./video-viewer/Controller";
import "./profiles/ConnectionProfile";
import "./update/UpdaterWeb";
import "./file/LocalIcons";
import "./ui/frames/menu-bar/MainMenu";
import {defaultConnectProfile, findConnectProfile} from "tc-shared/profiles/ConnectionProfile";
import {server_connections} from "tc-shared/ConnectionManager";
@ -189,7 +190,7 @@ function main() {
});
window.removeLoaderContextMenuHook();
top_menu.initialize();
//top_menu.initialize();
const initial_handler = server_connections.spawn_server_connection();
initial_handler.acquireInputHardware().then(() => {});

View File

@ -11,7 +11,7 @@ import {createServerModal} from "../ui/modal/ModalServerEdit";
import {spawnIconSelect} from "../ui/modal/ModalIconSelect";
import {spawnAvatarList} from "../ui/modal/ModalAvatarList";
import {connection_log} from "../ui/modal/ModalConnect";
import * as top_menu from "../ui/frames/MenuBar";
import * as top_menu from "../ui/frames/MenuBarOld";
import {Registry} from "../events";
import {ChannelTreeEntry, ChannelTreeEntryEvents} from "./ChannelTreeEntry";
@ -311,7 +311,6 @@ export class ServerEntry extends ChannelTreeEntry<ServerEvents> {
e.last_icon_server_id = this.properties.virtualserver_unique_identifier;
});
bookmarks.save_bookmark();
top_menu.rebuild_bookmarks();
}
}

View File

@ -1,578 +0,0 @@
import {spawnBookmarkModal} from "../../ui/modal/ModalBookmarks";
import {
add_server_to_bookmarks,
Bookmark,
bookmarks,
BookmarkType,
boorkmak_connect,
DirectoryBookmark
} from "../../bookmarks";
import {ConnectionHandler} from "../../ConnectionHandler";
import {Sound} from "../../sound/Sounds";
import {spawnConnectModal} from "../../ui/modal/ModalConnect";
import {createErrorModal, createInfoModal, createInputModal} from "../../ui/elements/Modal";
import {CommandResult} from "../../connection/ServerConnectionDeclaration";
import {PermissionType} from "../../permission/PermissionType";
import {openBanList} from "../../ui/modal/ModalBanList";
import {spawnQueryManage} from "../../ui/modal/ModalQueryManage";
import {spawnQueryCreate} from "../../ui/modal/ModalQuery";
import {spawnAbout} from "../../ui/modal/ModalAbout";
import * as loader from "tc-loader";
import {formatMessage} from "../../ui/frames/chat";
import {spawnPermissionEditorModal} from "../../ui/modal/permission/ModalPermissionEditor";
import {global_client_actions} from "tc-shared/events/GlobalEvents";
import {server_connections} from "tc-shared/ConnectionManager";
import {generateIconJQueryTag, getIconManager, RemoteIcon} from "tc-shared/file/Icons";
export interface HRItem { }
export interface MenuItem {
append_item(label: string): MenuItem;
append_hr(): HRItem;
delete_item(item: MenuItem | HRItem);
items() : (MenuItem | HRItem)[];
icon(klass?: string | RemoteIcon) : string;
label(value?: string) : string;
visible(value?: boolean) : boolean;
disabled(value?: boolean) : boolean;
click(callback: () => any) : this;
}
export interface MenuBarDriver {
initialize();
append_item(label: string) : MenuItem;
delete_item(item: MenuItem);
items() : MenuItem[];
flush_changes();
}
let _driver: MenuBarDriver;
export function driver() : MenuBarDriver {
return _driver;
}
export function set_driver(driver: MenuBarDriver) {
_driver = driver;
}
export interface NativeActions {
open_dev_tools();
reload_page();
check_native_update();
open_change_log();
quit();
show_dev_tools(): boolean;
}
export let native_actions: NativeActions;
namespace html {
class HTMLHrItem implements HRItem {
readonly html_tag: JQuery;
constructor() {
this.html_tag = $.spawn("hr");
}
}
class HTMLMenuItem implements MenuItem {
readonly html_tag: JQuery;
readonly _label_tag: JQuery;
readonly _label_icon_tag: JQuery;
readonly _label_text_tag: JQuery;
readonly _submenu_tag: JQuery;
private _items: (MenuItem | HRItem)[] = [];
private _label: string;
private _callback_click: () => any;
private visible_: boolean = true;
constructor(label: string, mode: "side" | "down") {
this._label = label;
this.html_tag = $.spawn("div").addClass("container-menu-item type-" + mode);
this._label_tag = $.spawn("div").addClass("menu-item");
this._label_icon_tag = $.spawn("div").addClass("container-icon").appendTo(this._label_tag);
$.spawn("div").addClass("container-label").append(
this._label_text_tag = $.spawn("a").text(label)
).appendTo(this._label_tag);
this._label_tag.on('click', event => {
if(event.isDefaultPrevented())
return;
const disabled = this.html_tag.hasClass("disabled");
if(this._callback_click && !disabled) {
this._callback_click();
}
event.preventDefault();
if(disabled)
event.stopPropagation();
else
HTMLMenuBarDriver.instance().close();
});
this._submenu_tag = $.spawn("div").addClass("sub-menu");
this.html_tag.append(this._label_tag);
this.html_tag.append(this._submenu_tag);
}
append_item(label: string): MenuItem {
const item = new HTMLMenuItem(label, "side");
this._items.push(item);
this._submenu_tag.append(item.html_tag);
this.html_tag.addClass('sub-entries');
return item;
}
append_hr(): HRItem {
const item = new HTMLHrItem();
this._items.push(item);
this._submenu_tag.append(item.html_tag);
return item;
}
delete_item(item: MenuItem | HRItem) {
this._items.remove(item);
(item as any).html_tag.detach();
this.html_tag.toggleClass('sub-entries', this._items.length > 0);
}
disabled(value?: boolean): boolean {
if(typeof(value) === "undefined")
return this.html_tag.hasClass("disabled");
this.html_tag.toggleClass("disabled", value);
return value;
}
items(): (MenuItem | HRItem)[] {
return this._items;
}
label(value?: string): string {
if(typeof(value) === "undefined" || this._label === value)
return this._label;
return this._label;
}
visible(value?: boolean): boolean {
if(typeof(value) === "undefined")
return this.visible_;
this.html_tag.toggleClass("hidden", this.visible_ = !!value);
return value;
}
click(callback: () => any): this {
this._callback_click = callback;
return this;
}
icon(klass?: string | RemoteIcon): string {
this._label_icon_tag.children().remove();
if(typeof(klass) === "string") {
$.spawn("div").addClass("icon_em " + klass).appendTo(this._label_icon_tag);
} else {
generateIconJQueryTag(klass).appendTo(this._label_icon_tag);
}
return "";
}
}
export class HTMLMenuBarDriver implements MenuBarDriver {
private static _instance: HTMLMenuBarDriver;
public static instance() : HTMLMenuBarDriver {
if(!this._instance)
this._instance = new HTMLMenuBarDriver();
return this._instance;
}
readonly html_tag: JQuery;
private _items: MenuItem[] = [];
constructor() {
this.html_tag = $.spawn("div").addClass("top-menu-bar");
}
append_item(label: string): MenuItem {
const item = new HTMLMenuItem(label, "down");
this._items.push(item);
this.html_tag.append(item.html_tag);
item._label_tag.on('click', enable_event => {
enable_event.preventDefault();
this.close();
item.html_tag.addClass("active");
setTimeout(() => {
$(document).one('click focusout', event => {
if(event.isDefaultPrevented()) return;
event.preventDefault();
item.html_tag.removeClass("active");
});
}, 0);
});
return item;
}
close() {
this.html_tag.find(".active").removeClass("active");
}
delete_item(item: MenuItem) {
return undefined;
}
items(): MenuItem[] {
return this._items;
}
flush_changes() { /* unused, all changed were made instantly */ }
initialize() {
$("#top-menu-bar").replaceWith(this.html_tag);
}
}
}
let _items_bookmark: {
root: MenuItem,
manage: MenuItem,
add_current: MenuItem
};
export function rebuild_bookmarks() {
if(!_items_bookmark) {
_items_bookmark = {
root: driver().append_item(tr("Favorites")),
add_current: undefined,
manage: undefined
};
_items_bookmark.manage = _items_bookmark.root.append_item(tr("Manage bookmarks"));
_items_bookmark.manage.icon("client-bookmark_manager");
_items_bookmark.manage.click(() => spawnBookmarkModal());
_items_bookmark.add_current = _items_bookmark.root.append_item(tr("Add current server to bookmarks"));
_items_bookmark.add_current.icon('client-bookmark_add');
_items_bookmark.add_current.click(() => add_server_to_bookmarks(server_connections.active_connection()));
_state_updater["bookmarks.ac"] = { item: _items_bookmark.add_current, conditions: [condition_connected]};
}
_items_bookmark.root.items().filter(e => e !== _items_bookmark.add_current && e !== _items_bookmark.manage).forEach(e => {
_items_bookmark.root.delete_item(e);
});
_items_bookmark.root.append_hr();
const build_bookmark = (root: MenuItem, entry: DirectoryBookmark | Bookmark) => {
if(entry.type == BookmarkType.DIRECTORY) {
const directory = entry as DirectoryBookmark;
const item = root.append_item(directory.display_name);
item.icon('client-folder');
for(const entry of directory.content)
build_bookmark(item, entry);
if(directory.content.length == 0)
item.disabled(true);
} else {
const bookmark = entry as Bookmark;
const item = root.append_item(bookmark.display_name);
item.icon(getIconManager().resolveIcon(bookmark.last_icon_id, bookmark.last_icon_server_id));
item.click(() => boorkmak_connect(bookmark));
}
};
for(const entry of bookmarks().content)
build_bookmark(_items_bookmark.root, entry);
driver().flush_changes();
}
/* will be called on connection handler change or on client connect state or mic state change etc... */
let _state_updater: {[key: string]:{ item: MenuItem; conditions: (() => boolean)[], update_handler?: (item: MenuItem) => any }} = {};
export function update_state() {
for(const _key of Object.keys(_state_updater)) {
const item = _state_updater[_key];
if(item.update_handler) {
if(item.update_handler(item.item))
continue;
}
let enabled = true;
for(const condition of item.conditions)
if(!condition()) {
enabled = false;
break;
}
item.item.disabled(!enabled);
}
driver().flush_changes();
}
const condition_connected = () => {
const scon = server_connections ? server_connections.active_connection() : undefined;
return scon && scon.connected;
};
declare namespace native {
export function initialize();
}
export function initialize() {
const driver_ = driver();
driver_.initialize();
/* build connection */
let item: MenuItem;
{
const menu = driver_.append_item(tr("Connection"));
item = menu.append_item(tr("Connect to a server"));
item.icon('client-connect');
item.click(() => spawnConnectModal({}));
const do_disconnect = (handlers: ConnectionHandler[]) => {
for(const handler of handlers)
handler.disconnectFromServer();
update_state();
};
item = menu.append_item(tr("Disconnect from current server"));
item.icon('client-disconnect');
item.disabled(true);
item.click(() => {
const handler = server_connections.active_connection();
do_disconnect([handler]);
});
_state_updater["connection.dc"] = { item: item, conditions: [() => condition_connected()]};
item = menu.append_item(tr("Disconnect from all servers"));
item.icon('client-disconnect');
item.click(() => {
do_disconnect(server_connections.all_connections());
});
_state_updater["connection.dca"] = { item: item, conditions: [], update_handler: (item) => {
item.visible(server_connections && server_connections.all_connections().length > 1);
return true;
}};
if(__build.target !== "web") {
menu.append_hr();
item = menu.append_item(tr("Quit"));
item.icon('client-close_button');
item.click(() => native_actions.quit());
}
}
{
rebuild_bookmarks();
}
if(false) {
const menu = driver_.append_item(tr("Self"));
/* Microphone | Sound | Away */
}
{
const menu = driver_.append_item(tr("Permissions"));
item = menu.append_item(tr("Server Groups"));
item.icon("client-permission_server_groups");
item.click(() => {
spawnPermissionEditorModal(server_connections.active_connection(), "groups-server");
});
_state_updater["permission.sg"] = { item: item, conditions: [condition_connected]};
item = menu.append_item(tr("Client Permissions"));
item.icon("client-permission_client");
item.click(() => {
spawnPermissionEditorModal(server_connections.active_connection(), "client");
});
_state_updater["permission.clp"] = { item: item, conditions: [condition_connected]};
item = menu.append_item(tr("Channel Client Permissions"));
item.icon("client-permission_client");
item.click(() => {
spawnPermissionEditorModal(server_connections.active_connection(), "client-channel");
});
_state_updater["permission.chclp"] = { item: item, conditions: [condition_connected]};
item = menu.append_item(tr("Channel Groups"));
item.icon("client-permission_channel");
item.click(() => {
spawnPermissionEditorModal(server_connections.active_connection(), "groups-channel");
});
_state_updater["permission.cg"] = { item: item, conditions: [condition_connected]};
item = menu.append_item(tr("Channel Permissions"));
item.icon("client-permission_channel");
item.click(() => {
spawnPermissionEditorModal(server_connections.active_connection(), "channel");
});
_state_updater["permission.cp"] = { item: item, conditions: [condition_connected]};
menu.append_hr();
item = menu.append_item(tr("List Privilege Keys"));
item.icon("client-token");
item.click(() => {
createErrorModal(tr("Not implemented"), tr("Privilege key list is not implemented yet!")).open();
});
_state_updater["permission.pk"] = { item: item, conditions: [condition_connected]};
item = menu.append_item(tr("Use Privilege Key"));
item.icon("client-token_use");
item.click(() => {
//TODO: Fixeme use one method for the control bar and here!
createInputModal(tr("Use token"), tr("Please enter your token/privilege key"), message => message.length > 0, result => {
if(!result) return;
const scon = server_connections.active_connection();
if(scon.serverConnection.connected)
scon.serverConnection.send_command("tokenuse", {
token: result
}).then(() => {
createInfoModal(tr("Use token"), tr("Toke successfully used!")).open();
}).catch(error => {
//TODO tr
createErrorModal(tr("Use token"), formatMessage(tr("Failed to use token: {}"), error instanceof CommandResult ? error.message : error)).open();
});
}).open();
});
_state_updater["permission.upk"] = { item: item, conditions: [condition_connected]};
}
{
const menu = driver_.append_item(tr("Tools"));
/*
item = menu.append_item(tr("Manage Playlists"));
item.icon('client-music');
item.click(() => {
const scon = server_connections.active_connection_handler();
if(scon && scon.connected) {
Modals.spawnPlaylistManage(scon);
} else {
createErrorModal(tr("You have to be connected"), tr("You have to be connected to use this function!")).open();
}
});
_state_updater["tools.pl"] = { item: item, conditions: [condition_connected]};
*/
item = menu.append_item(tr("Ban List"));
item.icon('client-ban_list');
item.click(() => {
const scon = server_connections.active_connection();
if(scon && scon.connected) {
if(scon.permissions.neededPermission(PermissionType.B_CLIENT_BAN_LIST).granted(1)) {
openBanList(scon);
} else {
createErrorModal(tr("You dont have the permission"), tr("You dont have the permission to view the ban list")).open();
scon.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS);
}
} else {
createErrorModal(tr("You have to be connected"), tr("You have to be connected to use this function!")).open();
}
});
_state_updater["tools.bl"] = { item: item, conditions: [condition_connected]};
item = menu.append_item(tr("Query List"));
item.icon('client-server_query');
item.click(() => {
const scon = server_connections.active_connection();
if(scon && scon.connected) {
if(scon.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_LIST).granted(1) || scon.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_LIST_OWN).granted(1)) {
spawnQueryManage(scon);
} else {
createErrorModal(tr("You dont have the permission"), tr("You dont have the permission to view the server query list")).open();
scon.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS);
}
} else {
createErrorModal(tr("You have to be connected"), tr("You have to be connected to use this function!")).open();
}
});
_state_updater["tools.ql"] = { item: item, conditions: [condition_connected]};
item = menu.append_item(tr("Query Create"));
item.icon('client-server_query');
item.click(() => {
const scon = server_connections.active_connection();
if(scon && scon.connected) {
if(scon.permissions.neededPermission(PermissionType.B_CLIENT_CREATE_MODIFY_SERVERQUERY_LOGIN).granted(1) || scon.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_CREATE).granted(1)) {
spawnQueryCreate(scon);
} else {
createErrorModal(tr("You dont have the permission"), tr("You dont have the permission to create a server query login")).open();
scon.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS);
}
} else {
createErrorModal(tr("You have to be connected"), tr("You have to be connected to use this function!")).open();
}
});
_state_updater["tools.qc"] = { item: item, conditions: [condition_connected]};
menu.append_hr();
item = menu.append_item(tr("Modify CSS variables"));
item.click(() => global_client_actions.fire("action_open_window", { window: "css-variable-editor" }));
item = menu.append_item(tr("Open Registry"));
item.click(() => global_client_actions.fire("action_open_window", { window: "settings-registry" }));
menu.append_hr();
item = menu.append_item(tr("Settings"));
item.icon("client-settings");
item.click(() => global_client_actions.fire("action_open_window_settings"));
}
{
const menu = driver_.append_item(tr("Help"));
if(__build.target !== "web") {
item = menu.append_item(tr("Check for updates"));
item.click(() => native_actions.check_native_update());
item = menu.append_item(tr("Open changelog"));
item.click(() => native_actions.open_change_log());
}
item = menu.append_item(tr("Visit TeaSpeak.de"));
item.click(() => window.open('https://teaspeak.de/', '_blank'));
item = menu.append_item(tr("Visit TeaSpeak forum"));
item.click(() => window.open('https://forum.teaspeak.de/', '_blank'));
if(__build.target !== "web" && typeof(native_actions.show_dev_tools) === "function" && native_actions.show_dev_tools()) {
menu.append_hr();
item = menu.append_item(tr("Open developer tools"));
item.click(() => native_actions.open_dev_tools());
item = menu.append_item(tr("Reload UI"));
item.click(() => native_actions.reload_page());
}
menu.append_hr();
item = menu.append_item(__build.target === "web" ? tr("About TeaWeb") : tr("About TeaClient"));
item.click(() => spawnAbout())
}
update_state();
}
/* default is HTML, the client will override this */
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
function: async () => {
if(!driver())
set_driver(html.HTMLMenuBarDriver.instance());
},
priority: 100,
name: "Menu bar init"
});

View File

@ -0,0 +1,361 @@
import * as loader from "tc-loader";
import {Stage} from "tc-loader";
import {getMenuBarDriver, MenuBarEntry} from "tc-shared/ui/frames/menu-bar/index";
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";
function renderConnectionItems() {
const items: MenuBarEntry[] = [];
const currentConnectionConnected = !!server_connections.active_connection()?.connected;
items.push({
type: "normal",
label: tr("Connect to a server"),
icon: ClientIcon.Connect,
click: () => global_client_actions.fire("action_open_window_connect", { newTab: currentConnectionConnected })
});
items.push({
type: "normal",
label: tr("Disconnect from current server"),
icon: ClientIcon.Disconnect,
disabled: !currentConnectionConnected,
click: () => server_connections.active_connection()?.disconnectFromServer()
});
items.push({
type: "normal",
label: tr("Disconnect from all servers"),
icon: ClientIcon.Disconnect,
disabled: server_connections.all_connections().findIndex(e => e.connected) === -1,
click: () => server_connections.all_connections().forEach(connection => connection.disconnectFromServer())
});
if(__build.target === "client") {
items.push({ type: "separator" });
items.push({
type: "normal",
label: tr("Quit"),
icon: ClientIcon.CloseButton,
click: () => getBackend("native").quit()
});
}
return items;
}
function renderBookmarkItems() {
const items: MenuBarEntry[] = [];
const renderBookmark = (bookmark: Bookmark | DirectoryBookmark): MenuBarEntry => {
if(bookmark.type === BookmarkType.ENTRY) {
return {
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 {
type: "normal",
label: bookmark.display_name,
icon: ClientIcon.Folder,
children: bookmark.content.map(renderBookmark)
}
}
}
items.push({
type: "normal",
icon: ClientIcon.BookmarkManager,
label: tr("Manage bookmarks"),
click: () => global_client_actions.fire("action_open_window", { window: "bookmark-manage" })
});
items.push({
type: "normal",
icon: ClientIcon.BookmarkAdd,
label: tr("Add current server to bookmarks"),
disabled: !server_connections.active_connection()?.connected,
click: () => add_server_to_bookmarks(server_connections.active_connection())
});
const rootMarks = bookmarks().content;
if(rootMarks.length !== 0) {
items.push({ type: "separator" });
items.push(...rootMarks.map(renderBookmark));
}
return items;
}
function renderPermissionItems() : MenuBarEntry[] {
const items: MenuBarEntry[] = [];
const currentConnectionConnected = !!server_connections.active_connection()?.connected;
items.push({
type: "normal",
label: tr("Server Groups"),
icon: ClientIcon.PermissionServerGroups,
click: () => global_client_actions.fire("action_open_window_permissions", { defaultTab: "groups-server" }),
disabled: !currentConnectionConnected
});
items.push({
type: "normal",
label: tr("Client Permissions"),
icon: ClientIcon.PermissionClient,
click: () => global_client_actions.fire("action_open_window_permissions", { defaultTab: "client" }),
disabled: !currentConnectionConnected
});
items.push({
type: "normal",
label: tr("Channel Client Permissions"),
icon: ClientIcon.PermissionClient,
click: () => global_client_actions.fire("action_open_window_permissions", { defaultTab: "client-channel" }),
disabled: !currentConnectionConnected
});
items.push({
type: "normal",
label: tr("Channel Groups"),
icon: ClientIcon.PermissionChannel,
click: () => global_client_actions.fire("action_open_window_permissions", { defaultTab: "groups-channel" }),
disabled: !currentConnectionConnected
});
items.push({
type: "normal",
label: tr("Channel Permissions"),
icon: ClientIcon.PermissionChannel,
click: () => global_client_actions.fire("action_open_window_permissions", { defaultTab: "channel" }),
disabled: !currentConnectionConnected
});
items.push({ type: "separator" });
items.push({
type: "normal",
label: tr("List Privilege Keys"),
icon: ClientIcon.Token,
click: () => global_client_actions.fire("action_open_window", { window: "token-list" }),
disabled: !currentConnectionConnected
});
items.push({
type: "normal",
label: tr("Use Privilege Key"),
icon: ClientIcon.TokenUse,
click: () => global_client_actions.fire("action_open_window", { window: "token-use" }),
disabled: !currentConnectionConnected
});
return items;
}
function renderToolItems() : MenuBarEntry[] {
const items: MenuBarEntry[] = [];
const currentConnectionConnected = !!server_connections.active_connection()?.connected;
items.push({
type: "normal",
label: tr("Ban List"),
icon: ClientIcon.BanList,
click: () => global_client_actions.fire("action_open_window", { window: "ban-list" }),
disabled: !currentConnectionConnected
});
items.push({
type: "normal",
label: tr("Query List"),
icon: ClientIcon.ServerQuery,
click: () => global_client_actions.fire("action_open_window", { window: "query-manage" }),
disabled: !currentConnectionConnected
});
items.push({
type: "normal",
label: tr("Query Create"),
icon: ClientIcon.ServerQuery,
click: () => global_client_actions.fire("action_open_window", { window: "query-create" }),
disabled: !currentConnectionConnected
});
items.push({ type: "separator" });
items.push({
type: "normal",
label: tr("Modify CSS variables"),
click: () => global_client_actions.fire("action_open_window", { window: "css-variable-editor" })
});
items.push({
type: "normal",
label: tr("Open Registry"),
click: () => global_client_actions.fire("action_open_window", { window: "settings-registry" })
});
items.push({ type: "separator" });
items.push({
type: "normal",
label: tr("Settings"),
click: () => global_client_actions.fire("action_open_window_settings")
});
return items;
}
function renderHelpItems() : MenuBarEntry[] {
const items: MenuBarEntry[] = [];
if(__build.target === "client") {
items.push({
type: "normal",
label: tr("Check for updates"),
icon: ClientIcon.CheckUpdate,
click: () => getBackend("native").openClientUpdater()
});
items.push({
type: "normal",
label: tr("Open client changelog"),
click: () => getBackend("native").openChangeLog()
});
}
items.push({
type: "normal",
label: tr("Visit TeaSpeak.de"),
click: () => window.open('https://teaspeak.de/', '_blank')
});
items.push({
type: "normal",
label: tr("Visit TeaSpeak forum"),
click: () => window.open('https://forum.teaspeak.de/', '_blank')
});
if(__build.target === "client" && getBackend("native").showDeveloperOptions()) {
items.push({ type: "separator" });
items.push({
type: "normal",
label: tr("Open developer tools"),
click: () => getBackend("native").openDeveloperTools()
});
items.push({
type: "normal",
label: tr("Reload UI"),
click: () => getBackend("native").reloadWindow()
});
}
items.push({ type: "separator" });
items.push({
type: "normal",
label: __build.target === "web" ? tr("About TeaWeb") : tr("About TeaClient"),
click: () => global_client_actions.fire("action_open_window", { window: "about" })
});
return items;
}
function updateMenuBar() {
const items: MenuBarEntry[] = [];
items.push({
type: "normal",
label: tr("Connection"),
children: renderConnectionItems()
});
items.push({
type: "normal",
label: tr("Favorites"),
children: renderBookmarkItems()
});
items.push({
type: "normal",
label: tr("Permissions"),
children: renderPermissionItems()
});
items.push({
type: "normal",
label: tr("Tools"),
children: renderToolItems()
});
items.push({
type: "normal",
label: tr("Help"),
children: renderHelpItems()
});
/* TODO: Check if it's not exactly the same menu bar */
getMenuBarDriver().setEntries(items);
}
let updateListener: MenuBarUpdateListener;
class MenuBarUpdateListener {
private generalHandlerEvents: (() => void)[] = [];
private registeredHandlerEvents: {[key: string]: (() => void)[]} = {};
initializeListeners() {
this.generalHandlerEvents.push(server_connections.events().on("notify_handler_created", event => {
this.registerHandlerEvents(event.handler);
}));
this.generalHandlerEvents.push(server_connections.events().on("notify_handler_deleted", event => {
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();
}))
server_connections.all_connections().forEach(handler => this.registerHandlerEvents(handler));
}
destroy() {
this.generalHandlerEvents.forEach(callback => callback());
Object.keys(this.registeredHandlerEvents).forEach(id => this.registeredHandlerEvents[id].forEach(callback => callback()));
this.registeredHandlerEvents = {};
this.generalHandlerEvents = [];
}
private registerHandlerEvents(handler: ConnectionHandler) {
const events = this.registeredHandlerEvents[handler.handlerId] = [];
events.push(handler.events().on("notify_connection_state_changed", () => {
updateMenuBar();
}));
}
}
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
name: "menu bar entries init",
function: async () => {
updateMenuBar();
updateListener = new MenuBarUpdateListener();
updateListener.initializeListeners();
},
priority: 50
});

View File

@ -0,0 +1,44 @@
import {ClientIcon} from "svg-sprites/client-icons";
import {RemoteIconInfo} from "tc-shared/file/Icons";
export type MenuBarEntrySeparator = {
uniqueId?: string,
type: "separator"
};
export type MenuBarEntryNormal = {
uniqueId?: string,
type: "normal",
label: string,
disabled?: boolean,
visible?: boolean,
icon?: ClientIcon | RemoteIconInfo,
click?: () => void,
children?: MenuBarEntry[]
};
export type MenuBarEntry = MenuBarEntrySeparator | MenuBarEntryNormal;
export interface MenuBarDriver {
/**
* Separators on top level might not be rendered.
* @param entries
*/
setEntries(entries: MenuBarEntry[]);
/**
* Removes the menu bar
*/
clearEntries();
}
let driver: MenuBarDriver;
export function getMenuBarDriver() : MenuBarDriver {
return driver;
}
export function setMenuBarDriver(driver_: MenuBarDriver) {
driver = driver_;
}

View File

@ -18,7 +18,7 @@ import * as log from "../../log";
import {LogCategory} from "../../log";
import * as i18nc from "../../i18n/country";
import {formatMessage} from "../../ui/frames/chat";
import * as top_menu from "../frames/MenuBar";
import * as top_menu from "../frames/MenuBarOld";
import {generateIconJQueryTag, getIconManager} from "tc-shared/file/Icons";
export function spawnBookmarkModal() {
@ -402,9 +402,5 @@ export function spawnBookmarkModal() {
});
modal.htmlTag.dividerfy().find(".modal-body").addClass("modal-bookmarks");
modal.close_listener.push(() => {
top_menu.rebuild_bookmarks();
});
modal.open();
}

View File

@ -32,10 +32,10 @@ import {spawnGroupCreate} from "tc-shared/ui/modal/ModalGroupCreate";
import {spawnModalGroupPermissionCopy} from "tc-shared/ui/modal/ModalGroupPermissionCopy";
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
import {ErrorCode} from "tc-shared/connection/ErrorCode";
import {PermissionEditorTab} from "tc-shared/events/GlobalEvents";
const cssStyle = require("./ModalPermissionEditor.scss");
export type PermissionEditorTab = "groups-server" | "groups-channel" | "channel" | "client" | "client-channel";
export type PermissionEditorSubject =
"groups-server"
| "groups-channel"

4
web/app/hooks/Backend.ts Normal file
View File

@ -0,0 +1,4 @@
import {setBackend} from "tc-shared/backend";
import {WebClientBackend} from "tc-shared/backend/WebClient";
setBackend(new class implements WebClientBackend {});

4
web/app/hooks/MenuBar.ts Normal file
View File

@ -0,0 +1,4 @@
import {setMenuBarDriver} from "tc-shared/ui/frames/menu-bar";
import {WebMenuBarDriver} from "tc-backend/web/ui/menu-bar/Controller";
setMenuBarDriver(new WebMenuBarDriver());

View File

@ -9,6 +9,7 @@ import "./audio-lib";
import "./hooks/ServerConnection";
import "./hooks/ExternalModal";
import "./hooks/AudioRecorder";
import "./hooks/MenuBar";
import "./UnloadHandler";

View File

@ -0,0 +1,43 @@
import {MenuBarDriver, MenuBarEntry} from "tc-shared/ui/frames/menu-bar";
import * as React from "react";
import * as ReactDOM from "react-dom";
import {MenuBarRenderer} from "tc-backend/web/ui/menu-bar/Renderer";
const cssStyle = require("./Renderer.scss");
let uniqueMenuEntryId = 0;
export class WebMenuBarDriver implements MenuBarDriver {
private readonly htmlContainer: HTMLDivElement;
private currentEntries: MenuBarEntry[] = [];
constructor() {
this.htmlContainer = document.createElement("div");
this.htmlContainer.classList.add(cssStyle.container);
document.body.append(this.htmlContainer);
}
clearEntries() {
this.currentEntries = [];
this.renderMenu();
}
setEntries(entries: MenuBarEntry[]) {
if(this.currentEntries === entries) { return; }
this.currentEntries = entries.slice(0);
this.currentEntries.forEach(WebMenuBarDriver.fixupUniqueIds);
this.renderMenu();
}
private static fixupUniqueIds(entry: MenuBarEntry) {
if(!entry.uniqueId) {
entry.uniqueId = "item-" + (++uniqueMenuEntryId);
}
if(entry.type === "normal") {
entry.children?.forEach(WebMenuBarDriver.fixupUniqueIds);
}
}
private renderMenu() {
ReactDOM.render(React.createElement(MenuBarRenderer, { items: this.currentEntries }), this.htmlContainer);
}
}

View File

@ -0,0 +1,137 @@
.container {
user-select: none;
-webkit-user-select: none;
height: 1.5em;
width: 100%;
background: #fafafa;
display: flex;
flex-direction: row;
justify-content: flex-start;
position: fixed;
top: 0;
z-index: 201;
font-family: Arial, serif;
> .containerMenuItem {
> .menuItem {
.containerIcon {
display: none;
}
}
}
hr {
margin-top: .125em;
margin-bottom: .125em;
}
}
.containerMenuItem {
position: relative;
.menuItem {
cursor: pointer;
padding-left: .4em;
padding-right: .4em;
height: 100%;
display: flex;
flex-direction: row;
width: max-content;
> * {
vertical-align: middle;
}
.containerIcon {
height: 1.2em;
width: 1.2em;
padding: .1em;
font-size: 1em;
margin-right: .2em;
display: inline-block;
}
.containerLabel {
display: inline-block;
align-self: center;
a {
white-space: nowrap;
}
}
}
&:hover:not(.disabled) {
background-color: rgba(0, 0, 0, 0.27);
}
&.disabled {
background-color: rgba(0, 0, 0, 0.13);
pointer-events: none;
}
&.hidden {
display: none;
}
.subMenu {
z-index: 1000;
display: none;
background: white;
position: absolute;
top: 100%;
border: 1px solid black;
> .container-menu-item {
padding-right: .5em;
}
}
&.typeSide {
&.subEntries {
padding-right: .8em;
}
&.subEntries:after {
position: absolute;
display: block;
content: '>';
top: 0;
bottom: 0;
right: .4em;
}
> .subMenu {
top: -1px; /* border */
left: 100%;
}
&:hover {
> .subMenu {
display: block;
}
}
}
&.active {
background-color: rgba(0, 0, 0, 0.27);
> .subMenu {
display: block;
}
}
}

View File

@ -0,0 +1,115 @@
import {MenuBarEntry, MenuBarEntryNormal} from "tc-shared/ui/frames/menu-bar";
import * as React from "react";
import {useEffect, useState} from "react";
import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons";
import {RemoteIconRenderer} from "tc-shared/ui/react-elements/Icon";
import {getIconManager} from "tc-shared/file/Icons";
const cssStyle = require("./Renderer.scss");
const SubMenuRenderer = (props: { entry: MenuBarEntryNormal }) => {
if(!props.entry.children) { return null; }
return (
<div className={cssStyle.subMenu}>
{props.entry.children.map(child => {
if(child.type === "separator") {
return <hr key={child.uniqueId} />
} else {
return <EntryRenderer entry={child} key={child.uniqueId} />;
}
})}
</div>
);
};
const MenuItemRenderer = (props: { entry: MenuBarEntryNormal }) => {
let icon;
if(typeof props.entry.icon === "string") {
icon = <ClientIconRenderer icon={props.entry.icon} key={"client-icon"} />;
} else if(typeof props.entry.icon === "object") {
let remoteIcon = getIconManager().resolveIcon(props.entry.icon.iconId, props.entry.icon.serverUniqueId, props.entry.icon.handlerId);
icon = <RemoteIconRenderer icon={remoteIcon} key={"remote-icon"} />;
}
return (
<div className={cssStyle.menuItem} onClick={props.entry.click}>
<div className={cssStyle.containerIcon}>{icon}</div>
<div className={cssStyle.containerLabel}>{props.entry.label}</div>
</div>
);
}
const EntryRenderer = React.memo((props: { entry: MenuBarEntryNormal }) => {
let classList = [cssStyle.containerMenuItem, cssStyle.typeSide];
if(props.entry.children?.length) {
classList.push(cssStyle.subEntries);
}
if(props.entry.disabled) {
classList.push(cssStyle.disabled);
}
if(typeof props.entry.visible === "boolean" && !props.entry.visible) {
return null;
}
return (
<div className={classList.join(" ")}>
<MenuItemRenderer entry={props.entry} />
<SubMenuRenderer entry={props.entry} />
</div>
);
});
const MainEntryRenderer = React.memo((props: { entry: MenuBarEntry }) => {
const [ active, setActive ] = useState(false);
if(props.entry.type !== "normal") { return null; }
if(typeof props.entry.visible === "boolean" && !props.entry.visible) {
return null;
}
let classList = [cssStyle.containerMenuItem];
if(props.entry.children?.length) {
classList.push(cssStyle.subEntries);
}
if(props.entry.disabled) {
classList.push(cssStyle.disabled);
}
if(active) {
classList.push(cssStyle.active);
}
useEffect(() => {
if(!active) { return; }
const listener = (event: MouseEvent | FocusEvent) => {
event.preventDefault();
setActive(false);
};
document.addEventListener("click", listener);
document.addEventListener("focusout", listener);
return () => {
document.removeEventListener("click", listener);
document.removeEventListener("focusout", listener);
};
}, [ active ])
return (
<div className={classList.join(" ")}
onClick={() => setActive(true)}
>
<MenuItemRenderer entry={props.entry} />
<SubMenuRenderer entry={props.entry} />
</div>
);
});
export const MenuBarRenderer = (props: { items: MenuBarEntry[] }) => {
return (
<React.Fragment>
{props.items.map(item => <MainEntryRenderer entry={item} key={item.uniqueId} />)}
</React.Fragment>
);
}