The rest of default_colored_2014's keys get Material counterparts: talk power, whisper, recording, text formatting, identities, add-ons, permissions and the like. Ping levels are drawn as signal bars, since Material's own tell their levels apart by a hairline, and the default group icons as lettered badges. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
337 lines
12 KiB
Python
337 lines
12 KiB
Python
#!/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'<rect x="{3 + i * 5}" y="{17 - i * 4}" width="3.5" height="{4 + i * 4}" rx="1"'
|
|
+ ("" if i < level else ' fill-opacity="0.3"') + "/>"
|
|
for i in range(4))
|
|
return ('<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" '
|
|
f'viewBox="0 0 24 24">{bars}</svg>')
|
|
|
|
|
|
BADGE = (
|
|
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">'
|
|
'<rect x="2" y="2" width="20" height="20" rx="5" fill="{colour}"/>'
|
|
'<text x="12" y="17.5" text-anchor="middle" font-family="sans-serif" font-weight="bold" '
|
|
'font-size="15" fill="#FFFFFF">{letter}</text></svg>'
|
|
)
|
|
|
|
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("<svg ", "<svg" + attributes + " ", 1)
|
|
|
|
|
|
def main():
|
|
OUT.mkdir(parents=True, exist_ok=True)
|
|
for old in OUT.glob("*.svg"):
|
|
old.unlink()
|
|
lines = []
|
|
width = max(map(len, ICONS))
|
|
for key, spec in ICONS.items():
|
|
icon, style, colour, opacity = spec + ("", None, None)[len(spec) - 1:]
|
|
name = file_name(icon, style, colour, opacity)
|
|
path = OUT / name
|
|
if not path.exists():
|
|
path.write_text(paint(fetch(icon, style), colour, opacity) + "\n")
|
|
lines.append(f"{key.ljust(width)} = {name}")
|
|
for key, level in PING_LEVELS.items():
|
|
name = f"ping-{level}.svg"
|
|
(OUT / name).write_text(ping_bars(level) + "\n")
|
|
lines.append(f"{key.ljust(width)} = {name}")
|
|
for key, (letter, colour) in GROUP_BADGES.items():
|
|
name = f"group-{letter.lower()}.svg"
|
|
(OUT / name).write_text(BADGE.format(letter=letter, colour=colour) + "\n")
|
|
lines.append(f"{key.ljust(width)} = {name}")
|
|
(OUT / "settings.ini").write_text(INI_HEAD + "\n".join(lines) + "\n")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|