#!/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 ('')
BADGE = (
''
)
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("