import {spawnBookmarkModal} from "tc-shared/ui/modal/ModalBookmarks"; import { add_server_to_bookmarks, Bookmark, bookmarks, BookmarkType, boorkmak_connect, DirectoryBookmark } from "tc-shared/bookmarks"; import {ConnectionHandler, DisconnectReason} from "tc-shared/ConnectionHandler"; import {Sound} from "tc-shared/sound/Sounds"; import {spawnConnectModal} from "tc-shared/ui/modal/ModalConnect"; import {spawnPermissionEdit} from "tc-shared/ui/modal/permission/ModalPermissionEdit"; import {createErrorModal, createInfoModal, createInputModal} from "tc-shared/ui/elements/Modal"; import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration"; import {PermissionType} from "tc-shared/permission/PermissionType"; import {openBanList} from "tc-shared/ui/modal/ModalBanList"; import {spawnQueryManage} from "tc-shared/ui/modal/ModalQueryManage"; import {spawnQueryCreate} from "tc-shared/ui/modal/ModalQuery"; import {spawnSettingsModal} from "tc-shared/ui/modal/ModalSettings"; import {spawnAbout} from "tc-shared/ui/modal/ModalAbout"; import {server_connections} from "tc-shared/ui/frames/connection_handlers"; import * as loader from "tc-loader"; import {formatMessage} from "tc-shared/ui/frames/chat"; import {control_bar_instance} from "tc-shared/ui/frames/control-bar"; import {icon_cache_loader, IconManager, LocalIcon} 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 | LocalIcon) : string; //FIXME: Native client must work as well! 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; 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.html_tag.is(':visible'); //FIXME! this.html_tag.toggle(!!value); return value; } click(callback: () => any): this { this._callback_click = callback; return this; } icon(klass?: string | LocalIcon): string { this._label_icon_tag.children().remove(); if(typeof(klass) === "string") $.spawn("div").addClass("icon_em " + klass).appendTo(this._label_icon_tag); else IconManager.generate_tag(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(icon_cache_loader.load_icon(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(); control_bar_instance()?.events().fire("update_state", { state: "connect-state" }); 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(() => { spawnPermissionEdit(server_connections.active_connection(), "sg").open(); }); _state_updater["permission.sg"] = { item: item, conditions: [condition_connected]}; item = menu.append_item(tr("Client Permissions")); item.icon("client-permission_client"); item.click(() => { spawnPermissionEdit(server_connections.active_connection(), "clp").open(); }); _state_updater["permission.clp"] = { item: item, conditions: [condition_connected]}; item = menu.append_item(tr("Channel Client Permissions")); item.icon("client-permission_client"); item.click(() => { spawnPermissionEdit(server_connections.active_connection(), "clchp").open(); }); _state_updater["permission.chclp"] = { item: item, conditions: [condition_connected]}; item = menu.append_item(tr("Channel Groups")); item.icon("client-permission_channel"); item.click(() => { spawnPermissionEdit(server_connections.active_connection(), "cg").open(); }); _state_updater["permission.cg"] = { item: item, conditions: [condition_connected]}; item = menu.append_item(tr("Channel Permissions")); item.icon("client-permission_channel"); item.click(() => { spawnPermissionEdit(server_connections.active_connection(), "chp").open(); }); _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("Settings")); item.icon("client-settings"); item.click(() => spawnSettingsModal()); } { 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" });