#!/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" 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",), } INI_HEAD = """\ [info] name = Material [options] # Complete for what the client draws; the rest stays with the client's own icons. 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("