#!/usr/bin/env python3 """Builds the Material icon pack the client ships (core/src/main/resources/gfx/material). The pack is in TeamSpeak's own format (settings.ini + SVGs), so both frontends draw it through the same code as any TeamSpeak pack. The art is Google's Material Icons (Apache 2.0), the set the Android frontend was designed with. Monochrome icons are filled with `currentColor`, which the frontends replace with the interface's text colour; status icons carry the fixed colours TeamSpeak gives them. """ import pathlib import urllib.request SOURCE = "https://cdn.jsdelivr.net/npm/@material-design-icons/svg@0.14.15/{style}/{name}.svg" OUT = pathlib.Path(__file__).resolve().parent.parent / "core/src/main/resources/gfx/material" TALKING = "#2E9BF0" MUTED = "#D9443A" AWAY = "#9E8A2E" IDLE = "#888888" YELLOW = "#C9A227" GREEN = "#3A9D4A" UNSUBSCRIBED = 0.5 # key: (icon, style, colour, opacity); style "o" is Material's outlined variant. ICONS = { "SERVER_GREEN": ("dns",), "CHANNEL_GREEN": ("chat", "o", None, UNSUBSCRIBED), "CHANNEL_GREEN_SUBSCRIBED": ("chat", "o"), "CHANNEL_PRIVATE": ("lock", "", None, UNSUBSCRIBED), "CHANNEL_PRIVATE_SUBSCRIBED": ("lock",), "CHANNEL_RED": ("block", "", MUTED, UNSUBSCRIBED), "CHANNEL_RED_SUBSCRIBED": ("block", "", MUTED), "CHANNEL_CHAT": ("chat", "o"), "CHANNEL_COMMANDER": ("campaign", "o"), "PLAYER_OFF": ("circle", "o", IDLE), "PLAYER_ON": ("circle", "", TALKING), "PLAYER_COMMANDER_OFF": ("campaign", "o"), "PLAYER_COMMANDER_ON": ("campaign", "", TALKING), "AWAY": ("bedtime", "", AWAY), "SERVER_QUERY": ("terminal",), "INPUT_MUTED": ("mic_off", "", MUTED), "INPUT_MUTED_LOCAL": ("mic_off", "", AWAY), "OUTPUT_MUTED": ("volume_off", "", MUTED), "HARDWARE_INPUT_MUTED": ("mic_off", "", IDLE), "HARDWARE_OUTPUT_MUTED": ("headset_off", "", IDLE), "CAPTURE": ("mic",), "PLAYBACK": ("volume_up",), "VOLUME": ("volume_up",), "ACTIVATE_MICROPHONE": ("record_voice_over", "", TALKING), "CONNECT": ("login",), "DISCONNECT": ("logout",), "QUIT": ("power_settings_new",), "SETTINGS": ("settings",), "REFRESH": ("refresh",), "FILE_REFRESH": ("refresh",), "INFO": ("info",), "ABOUT": ("info", "o"), "WARNING": ("warning",), "ERROR": ("error",), "CHANNEL_SWITCH": ("login",), "CHANNEL_CREATE": ("add",), "CHANNEL_CREATE_SUB": ("subdirectory_arrow_right",), "CHANNEL_CREATE_SPACER": ("horizontal_rule",), "CHANNEL_EDIT": ("edit",), "CHANNEL_DELETE": ("delete",), "SUBSCRIBE_TO_CHANNEL": ("visibility",), "UNSUBSCRIBE_FROM_CHANNEL": ("visibility_off",), "SUBSCRIBE_TO_CHANNEL_FAMILY": ("visibility", "o"), "UNSUBSCRIBE_FROM_CHANNEL_FAMILY": ("visibility_off", "o"), "SUBSCRIBE_TO_ALL_CHANNELS": ("visibility",), "UNSUBSCRIBE_FROM_ALL_CHANNELS": ("visibility_off",), "PLAYER_CHAT": ("chat", "o"), "NEW_CHAT": ("add_comment", "o"), "POKE": ("notifications_active",), "CHANGE_NICKNAME": ("drive_file_rename_outline",), "MOVE_CLIENT_TO_OWN_CHANNEL": ("move_down",), "KICK_FROM_CHANNEL": ("remove_circle_outline",), "KICK_FROM_SERVER": ("remove_circle",), "BAN_CLIENT": ("gavel",), "BAN_LIST": ("list",), "PERMISSIONS_SERVER_GROUPS": ("groups",), "PERMISSIONS_CHANNEL_GROUPS": ("group_work", "o"), "ADD_FRIEND": ("person_add",), "REMOVE_FRIEND": ("person_remove",), "ADD_FOE": ("person_off",), "REMOVE_FOE": ("person_remove", "o"), "EDIT_FRIEND_FOE_STATUS": ("contacts",), "CONTACT": ("contacts",), "IDENTITY_MANAGER": ("badge",), "BOOKMARK_MANAGER": ("bookmark",), "BOOKMARK_ADD": ("bookmark_add",), "BOOKMARK_REMOVE": ("bookmark_remove",), "UPLOAD_AVATAR": ("add_a_photo",), "DELETE_AVATAR": ("no_photography",), "ADD": ("add",), "EDIT": ("edit",), "DELETE": ("delete",), "COPY": ("content_copy",), "DEFAULT": ("star",), "CLOSE_BUTTON": ("close",), "TAB_CLOSE_BUTTON": ("close",), "FOLDER": ("folder",), "ADD_FOLDER": ("create_new_folder",), "FILE_UP": ("drive_folder_upload",), "FILETRANSFER": ("swap_vert",), "UPLOAD": ("upload",), "DOWNLOAD": ("download",), # Everything else TeamSpeak's own packs draw, so the pack is as complete as theirs. "CHANNEL_YELLOW": ("chat", "o", YELLOW, UNSUBSCRIBED), "CHANNEL_YELLOW_SUBSCRIBED": ("chat", "o", YELLOW), "CHANNEL_COLLAPSE_ALL": ("unfold_less",), "CHANNEL_COLLAPSE_SUB": ("unfold_less",), "CHANNEL_EXPAND_ALL": ("unfold_more",), "CHANNEL_EXPAND_SUB": ("unfold_more",), "MAKE_CURRENT_CHANNEL_DEFAULT": ("push_pin",), "MODERATED": ("shield", "o"), "SUBSCRIBE_MODE": ("visibility", "o"), "UNSUBSCRIBE_MODE": ("visibility_off", "o"), "CLIENT_SHOW": ("preview",), "CLIENT_HIDE": ("hide_source",), "IS_TALKER": ("record_voice_over", "o"), "TALK_POWER_REQUEST": ("back_hand",), "TALK_POWER_CANCEL_REQUEST": ("do_not_touch",), "TALK_POWER_GRANT": ("record_voice_over", "", GREEN), "TALK_POWER_GRANT_NEXT": ("record_voice_over", "o", GREEN), "TALK_POWER_REVOKE": ("voice_over_off", "", MUTED), "TALK_POWER_REVOKE_ALL_GRANT_NEXT": ("voice_over_off", "o", MUTED), "WHISPER": ("hearing",), "ON_WHISPERLIST": ("hearing", "", TALKING), "PLAYER_WHISPER": ("hearing", "", MUTED), "WHISPERHISTORY": ("history",), "WHISPERLISTS": ("list_alt",), "TOGGLE_BLOCK_INCOMING_WHISPERS": ("hearing_disabled",), "TOGGLE_SERVER_QUERY_CLIENTS": ("terminal", "o"), "3D_SOUND": ("surround_sound",), "3D_SOUND_ME": ("person_pin_circle",), "3D_SOUND_OTHER": ("person_pin",), "MUSIC": ("music_note",), "PLAY": ("play_arrow",), "STOP": ("stop",), "RECORDING_START": ("fiber_manual_record", "", MUTED), "RECORDING_STOP": ("stop_circle",), "PING_CALCULATING": ("network_check",), "PING_DISCONNECTED": ("wifi_off", "", IDLE), "MESSAGES": ("forum",), "MESSAGE_INCOMING": ("call_received",), "MESSAGE_OUTGOING": ("call_made",), "MESSAGE_INFO": ("info", "", TALKING), "OFFLINE_MESSAGES": ("mail", "o"), "NOTIFICATIONS": ("notifications",), "EMOTICON": ("emoji_emotions", "o"), "TEXTFORMAT": ("text_format",), "TEXTFORMAT_BOLD": ("format_bold",), "TEXTFORMAT_ITALIC": ("format_italic",), "TEXTFORMAT_UNDERLINE": ("format_underlined",), "TEXTFORMAT_FOREGROUND": ("format_color_text",), "COPY_URL": ("link",), "URLCATCHER": ("add_link",), "WEBLIST": ("public",), "WARNING_EXTERNAL_LINK": ("open_in_new",), "SERVER_LOG": ("receipt_long",), "COMPLAINT_LIST": ("report", "o"), "SEND_COMPLAINT": ("flag",), "INVITE_BUDDY": ("group_add",), "PHONETICSNICKNAME": ("spellcheck",), "RENAME": ("edit", "o"), "CONNECT_NEW_TAB": ("tab",), "DISCONNECT_ALL": ("link_off",), "HOSTER_BUTTON": ("storage",), "VIRTUALSERVER_EDIT": ("settings_applications",), "TEMP_SERVER_PASSWORD": ("password",), "TEMP_SERVER_PASSWORD_ADD": ("enhanced_encryption",), "TOKEN": ("vpn_key",), "TOKEN_USE": ("key",), "SECURITY": ("security",), "PERMISSION_OVERVIEW": ("policy",), "PERMISSIONS_CLIENTS": ("manage_accounts",), "PERMISSIONS_CHANNELS": ("rule",), "PERMISSIONS_CHANNEL_CLIENT": ("switch_account",), "IDENTITY_ADD": ("person_add_alt",), "IDENTITY_REMOVE": ("person_remove_alt_1",), "IDENTITY_DEFAULT": ("how_to_reg",), "IDENTITY_IMPORT": ("file_download",), "IDENTITY_EXPORT": ("file_upload",), "DELETE_AVATAR_OTHER": ("no_photography",), "BOOKMARK_ADD_FOLDER": ("create_new_folder", "o"), "BOOKMARK_DUPLICATE": ("bookmarks",), "BOOKMARK_SET_FOR_ALL": ("bookmark_added",), "MYTS_ACCOUNT": ("account_circle",), "MYTS_ACCOUNT_ONLINE": ("cloud_done",), "MYTS_ACCOUNT_OFFLINE": ("cloud_off", "", IDLE), "MYTS_ACCOUNT_ERROR": ("sync_problem", "", MUTED), "SYNC": ("sync",), "SYNC_ENABLE": ("cloud_sync",), "SYNC_DISABLE": ("cloud_off",), "REGISTER": ("app_registration",), "ADDON": ("extension",), "ADDON_BROWSER": ("travel_explore",), "ADDON_COLLECTION": ("library_add",), "ADDON_ICONPACK": ("category",), "ADDON_SOUNDPACK": ("library_music",), "ADDON_THEME": ("palette",), "ADDON_TRANSLATION": ("translate",), "PLUGINS": ("electrical_services",), "SOUNDPACK": ("library_music",), "HOTKEYS": ("keyboard",), "GUISETUP": ("tune",), "SWITCH_ADVANCED": ("build",), "SWITCH_STANDARD": ("build", "o"), "CHANGELOG": ("article",), "CHECK_UPDATE": ("update",), "PRESENT": ("redeem",), "APPLY": ("check",), "RELOAD": ("replay",), "FIND": ("search",), "SEARCH": ("search",), "SEARCH_CLEAR": ("close",), "SEARCH_HIDE": ("close",), "FILTER_CLEAR": ("filter_alt_off",), "SORT_BY_NAME": ("sort_by_alpha",), "UP": ("arrow_drop_up",), "UP_ARROW": ("arrow_drop_up",), "DOWN": ("arrow_drop_down",), "DOWN_ARROW": ("arrow_drop_down",), "FILE_HOME": ("home",), "SELECTFOLDER": ("folder_open",), "LISTVIEW": ("view_list",), "ICONSVIEW": ("grid_view",), "ICONVIEWER": ("photo_library",), "BROKEN_IMAGE": ("broken_image",), "LOADING_IMAGE": ("hourglass_empty",), } # TeamSpeak's default server and channel groups, lettered as its own packs letter them. GROUP_BADGES = { "GROUP_100": ("C", "#D9443A"), "GROUP_200": ("O", "#E0782E"), "GROUP_300": ("S", "#B02E6E"), "GROUP_500": ("Q", "#6E4CB0"), "GROUP_600": ("V", "#2E7DB0"), } # Material's own signal icons tell their levels apart by an outline too thin for 16 px. PING_LEVELS = {"PING_1": 1, "PING_2": 2, "PING_3": 3, "PING_4": 4} def ping_bars(level): bars = "".join( f'" for i in range(4)) return ('{bars}') BADGE = ( '' '' '{letter}' ) INI_HEAD = """\ [info] name = Material [options] # Covers every icon TeamSpeak's own packs do. FALLBACK=false [gfxfiles] """ def file_name(icon, style, colour, opacity): parts = [icon] + (["outlined"] if style == "o" else []) if colour: parts.append(colour.lstrip("#").lower()) if opacity is not None: parts.append(f"a{int(opacity * 100)}") return "-".join(parts) + ".svg" def fetch(icon, style): url = SOURCE.format(style="outlined" if style == "o" else "filled", name=icon) with urllib.request.urlopen(url) as response: return response.read().decode() def paint(svg, colour, opacity): attributes = f' fill="{colour or "currentColor"}"' if opacity is not None: attributes += f' fill-opacity="{opacity}"' return svg.replace("