Share icon packs between desktop and Android

The Android app's Material icons become a TeamSpeak-format pack shipped in
core, which the desktop offers next to the TeamSpeak packs. Android now
draws its icons from packs too and imports TeamSpeak's own zips, falling
back to the Material pack for what they lack.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-26 09:39:42 +00:00
parent a7aa122728
commit cdf81f3258
92 changed files with 786 additions and 139 deletions

View File

@@ -162,6 +162,11 @@ Android app in [`../android`](../android) does.
TeamSpeak 3 client (`$TS3_CLIENT_DIR` or the usual install paths) and a folder of
your choosing, so the official packs (`default_colored_2014.zip`,
`default_mono_2014.zip`, the legacy `default.zip`) work unchanged.
- **Material**, a pack in the same format built from Google's Material icons, ships
with the client (`core/src/main/resources/gfx/material`, generated by
`tools/material-icon-pack.py`). Its monochrome icons paint with `currentColor`, which
follows the theme's text colour. It is the Android app's default, and the Android app
draws TeamSpeak packs too — import one (e.g. `default_colored_2014.zip`) in its settings.
- The pack draws the tree (server, channel, client state), the toolbar, the menus,
the context menus, the file browser and TeamSpeak's default group icons. Vector
art is rasterised at the size it is drawn at; a pack's `FALLBACK` option decides
@@ -240,8 +245,9 @@ core/ com.ts3client
├── contacts friends and blocked clients, in TS3's own record format
├── text BBCode, TeamSpeak links, chat search
├── gfx
│ ├── IconPack an icon pack zip/folder + its settings.ini mapping
│ └── IconPacks discovery of installed packs
│ ├── IconPack an icon pack zip/folder/classpath folder + its settings.ini mapping
│ ├── IconPacks discovery of installed packs, the shipped Material one, lookup order
│ └── IconKeys the icon keys the channel tree asks for
├── hotkey
│ ├── HotkeyAction catalogue of TeamSpeak's hotkey actions (RE'd from its binary)
│ ├── Hotkey/HotkeyCombo one binding: keys, trigger edge, server scope, argument

View File

@@ -0,0 +1,37 @@
package com.ts3client.gfx;
import com.ts3client.net.ClientStatus;
/**
* The TeamSpeak icon keys the channel tree is drawn with, so every frontend asks
* the packs for the same icons.
*/
public final class IconKeys {
private IconKeys() {
}
/**
* @return the keys to try for a channel, most specific first — packs need not ship
* every variant (none of TeamSpeak's own has a subscribed password channel)
*/
public static String[] channel(boolean password, boolean full, boolean subscribed) {
String base = password ? "CHANNEL_PRIVATE" : full ? "CHANNEL_RED" : "CHANNEL_GREEN";
return subscribed ? new String[]{base + "_SUBSCRIBED", base} : new String[]{base};
}
public static String client(ClientStatus status) {
return switch (status) {
case QUERY -> "SERVER_QUERY";
case SPEAKERS_DISABLED -> "HARDWARE_OUTPUT_MUTED";
case SPEAKERS_MUTED -> "OUTPUT_MUTED";
case MIC_DISABLED -> "HARDWARE_INPUT_MUTED";
case MIC_MUTED -> "INPUT_MUTED";
case AWAY -> "AWAY";
case COMMANDER_TALKING -> "PLAYER_COMMANDER_ON";
case COMMANDER -> "PLAYER_COMMANDER_OFF";
case TALKING -> "PLAYER_ON";
case IDLE -> "PLAYER_OFF";
};
}
}

View File

@@ -39,7 +39,8 @@ import java.util.zip.ZipFile;
* resolved by convention, picking the size closest to the one that is asked for.
*
* <p>Icon data is returned as bytes; rasterising it (the packs from 2014 on are
* vector art) is the frontend's job.
* vector art) is the frontend's job. Vector art may paint with {@code currentColor},
* which the frontend sets to its text colour ({@link #withCurrentColor}).
*/
public final class IconPack {
@@ -51,20 +52,26 @@ public final class IconPack {
private record Entry(String path, int size) {
}
/** Where a pack's files are read from: a zip, a directory or the classpath. */
private interface Storage {
/** @return the file's bytes, or {@code null} when there is no such file */
byte[] read(String path) throws IOException;
}
private final String id;
private final File source;
private final String name;
private final boolean zipped;
private final Storage storage;
private final boolean fallback;
/** Icon key to the images that can serve it, smallest first. */
private final Map<String, List<Entry>> entries;
private IconPack(String id, File source, String name, boolean zipped, boolean fallback,
private IconPack(String id, File source, String name, Storage storage, boolean fallback,
Map<String, List<Entry>> entries) {
this.id = id;
this.source = source;
this.name = name;
this.zipped = zipped;
this.storage = storage;
this.fallback = fallback;
this.entries = entries;
}
@@ -78,6 +85,7 @@ public final class IconPack {
return name;
}
/** @return the pack's zip or directory, or {@code null} for one shipped with the client */
public File source() {
return source;
}
@@ -108,7 +116,7 @@ public final class IconPack {
Entry entry = select(entries.get(normalizeKey(key)), preferredSize);
if (entry == null) return null;
try {
return zipped ? readFromZip(entry.path()) : Files.readAllBytes(new File(source, entry.path()).toPath());
return storage.read(entry.path());
} catch (IOException e) {
return null;
}
@@ -134,14 +142,16 @@ public final class IconPack {
return candidates.get(candidates.size() - 1);
}
private byte[] readFromZip(String path) throws IOException {
try (ZipFile zip = new ZipFile(source)) {
ZipEntry entry = zip.getEntry(path);
if (entry == null) return null;
try (InputStream in = zip.getInputStream(entry)) {
return in.readAllBytes();
}
}
/**
* Sets the colour vector art paints with {@code currentColor}, as a stylesheet's
* {@code color} would.
*
* @param rgb the colour as {@code 0xRRGGBB}
*/
public static byte[] withCurrentColor(byte[] svg, int rgb) {
String text = new String(svg, StandardCharsets.UTF_8);
if (!text.contains("currentColor")) return svg;
return text.replace("currentColor", String.format("#%06X", rgb & 0xFFFFFF)).getBytes(StandardCharsets.UTF_8);
}
private static String normalizeKey(String key) {
@@ -162,6 +172,7 @@ public final class IconPack {
Map<String, Long> files = zipped ? listZip(source) : listDirectory(source);
if (files == null || files.isEmpty()) return null;
Storage storage = zipped ? path -> readFromZip(source, path) : path -> readFromDirectory(source, path);
String iniPath = files.keySet().stream()
.filter(p -> p.equalsIgnoreCase(INI))
@@ -170,7 +181,7 @@ public final class IconPack {
Map<String, String> info = new LinkedHashMap<>();
boolean fallback = true;
if (iniPath != null) {
byte[] ini = read(source, zipped, iniPath);
byte[] ini = read(storage, iniPath);
if (ini != null) fallback = parseIni(ini, mapping, info);
}
@@ -181,7 +192,35 @@ public final class IconPack {
String id = source.getName();
String name = info.getOrDefault("name", displayName(id));
return new IconPack(id, source, name, zipped, fallback, entries);
return new IconPack(id, source, name, storage, fallback, entries);
}
/**
* Reads a pack shipped on the classpath. As a classpath folder cannot be listed,
* the pack needs a {@code settings.ini}, whose file names must match exactly.
*
* @param id the pack's identifier in the settings
* @param resource the folder holding it, e.g. {@code /gfx/material}
* @return the pack, or {@code null} when it is not there
*/
public static IconPack loadResource(String id, String resource) {
Storage storage = path -> {
try (InputStream in = IconPack.class.getResourceAsStream(resource + '/' + path)) {
return in == null ? null : in.readAllBytes();
}
};
byte[] ini = read(storage, INI);
if (ini == null) return null;
Map<String, String> mapping = new LinkedHashMap<>();
Map<String, String> info = new LinkedHashMap<>();
boolean fallback = parseIni(ini, mapping, info);
Map<String, List<Entry>> entries = new LinkedHashMap<>();
for (Map.Entry<String, String> e : mapping.entrySet()) {
entries.put(e.getKey(), List.of(new Entry(e.getValue(), 0)));
}
if (entries.isEmpty()) return null;
return new IconPack(id, null, info.getOrDefault("name", displayName(id)), storage, fallback, entries);
}
/** Turns {@code default_colored_2014.zip} into {@code Default Colored 2014}. */
@@ -278,21 +317,29 @@ public final class IconPack {
return null;
}
private static byte[] read(File source, boolean zipped, String path) {
private static byte[] read(Storage storage, String path) {
try {
if (!zipped) return Files.readAllBytes(new File(source, path).toPath());
try (ZipFile zip = new ZipFile(source)) {
ZipEntry entry = zip.getEntry(path);
if (entry == null) return null;
try (InputStream in = zip.getInputStream(entry)) {
return in.readAllBytes();
}
}
return storage.read(path);
} catch (IOException e) {
return null;
}
}
private static byte[] readFromZip(File zipFile, String path) throws IOException {
try (ZipFile zip = new ZipFile(zipFile)) {
ZipEntry entry = zip.getEntry(path);
if (entry == null) return null;
try (InputStream in = zip.getInputStream(entry)) {
return in.readAllBytes();
}
}
}
private static byte[] readFromDirectory(File dir, String path) throws IOException {
File file = new File(dir, path);
return file.isFile() ? Files.readAllBytes(file.toPath()) : null;
}
private static Map<String, Long> listZip(File file) {
if (!file.getName().toLowerCase(Locale.ROOT).endsWith(".zip")) return null;
Map<String, Long> files = new LinkedHashMap<>();

View File

@@ -14,12 +14,16 @@ import java.util.Map;
* Finds the icon packs installed on this machine: the client's own {@code gfx/}
* folder, an extra folder the user configured, and the {@code gfx/} folder of an
* installed TeamSpeak 3 client (whose packs are usable as they are, see
* {@link IconPack}).
* {@link IconPack}) — plus the Material pack the client ships.
*/
public final class IconPacks {
/** The pack every other one falls back to, as TeamSpeak does. */
public static final String DEFAULT_ID = "default.zip";
/** The pack shipped with the client, drawn from Google's Material icons. */
public static final String MATERIAL_ID = "material";
private static volatile IconPack material;
private IconPacks() {
}
@@ -44,11 +48,19 @@ public final class IconPacks {
if (pack != null) byId.put(pack.id(), pack);
}
}
IconPack shipped = material();
if (shipped != null) byId.putIfAbsent(shipped.id(), shipped);
List<IconPack> packs = new ArrayList<>(byId.values());
packs.sort(Comparator.comparing(p -> p.name().toLowerCase(Locale.ROOT)));
return packs;
}
/** @return the Material pack, or {@code null} if the build left it out */
public static IconPack material() {
if (material == null) material = IconPack.loadResource(MATERIAL_ID, "/gfx/material");
return material;
}
/** @return the pack with this id, or {@code null} when it is not installed */
public static IconPack select(List<IconPack> packs, String id) {
for (IconPack p : packs) {
@@ -57,8 +69,19 @@ public final class IconPacks {
return null;
}
/** @return the pack unresolved icons are taken from, or {@code null} */
public static IconPack fallback(List<IconPack> packs) {
return select(packs, DEFAULT_ID);
/**
* The packs to look an icon up in, in turn: the active one, TeamSpeak's
* {@code default.zip} if the active pack asks for it, then {@code lastResort}.
*
* @param active the chosen pack, or {@code null} for none
* @param lastResort the pack for whatever is still missing, or {@code null}
*/
public static List<IconPack> lookupOrder(IconPack active, List<IconPack> packs, IconPack lastResort) {
List<IconPack> order = new ArrayList<>(3);
if (active != null) order.add(active);
IconPack fallback = select(packs, DEFAULT_ID);
if (active != null && active.fallsBack() && fallback != null && !order.contains(fallback)) order.add(fallback);
if (lastResort != null && !order.contains(lastResort)) order.add(lastResort);
return order;
}
}

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>

After

Width:  |  Height:  |  Size: 157 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M3 4V1h2v3h3v2H5v3H3V6H0V4h3zm3 6V7h3V4h7l1.83 2H21c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H5c-1.1 0-2-.9-2-2V10h3zm7 9c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-3.2-5c0 1.77 1.43 3.2 3.2 3.2s3.2-1.43 3.2-3.2-1.43-3.2-3.2-3.2-3.2 1.43-3.2 3.2z"/></svg>

After

Width:  |  Height:  |  Size: 370 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M22 4c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h14l4 4V4zm-2 13.17L18.83 16H4V4h16v13.17zM13 5h-2v4H7v2h4v4h2v-4h4V9h-4z"/></svg>

After

Width:  |  Height:  |  Size: 250 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M20 7h-5V4c0-1.1-.9-2-2-2h-2c-1.1 0-2 .9-2 2v3H4c-1.1 0-2 .9-2 2v11c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V9c0-1.1-.9-2-2-2zM9 12c.83 0 1.5.67 1.5 1.5S9.83 15 9 15s-1.5-.67-1.5-1.5S8.17 12 9 12zm3 6H6v-.75c0-1 2-1.5 3-1.5s3 .5 3 1.5V18zm1-9h-2V4h2v5zm5 7.5h-4V15h4v1.5zm0-3h-4V12h4v1.5z"/></svg>

After

Width:  |  Height:  |  Size: 401 B

View File

@@ -0,0 +1 @@
<svg fill="#9E8A2E" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12.34 2.02C6.59 1.82 2 6.42 2 12c0 5.52 4.48 10 10 10 3.71 0 6.93-2.02 8.66-5.02-7.51-.25-12.09-8.43-8.32-14.96z"/></svg>

After

Width:  |  Height:  |  Size: 230 B

View File

@@ -0,0 +1 @@
<svg fill="#D9443A" fill-opacity="0.5" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zM4 12c0-4.42 3.58-8 8-8 1.85 0 3.55.63 4.9 1.69L5.69 16.9A7.902 7.902 0 0 1 4 12zm8 8c-1.85 0-3.55-.63-4.9-1.69L18.31 7.1A7.902 7.902 0 0 1 20 12c0 4.42-3.58 8-8 8z"/></svg>

After

Width:  |  Height:  |  Size: 366 B

View File

@@ -0,0 +1 @@
<svg fill="#D9443A" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zM4 12c0-4.42 3.58-8 8-8 1.85 0 3.55.63 4.9 1.69L5.69 16.9A7.902 7.902 0 0 1 4 12zm8 8c-1.85 0-3.55-.63-4.9-1.69L18.31 7.1A7.902 7.902 0 0 1 20 12c0 4.42-3.58 8-8 8z"/></svg>

After

Width:  |  Height:  |  Size: 347 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M17 3H7c-1.1 0-1.99.9-1.99 2L5 21l7-3 7 3V5c0-1.1-.9-2-2-2z"/></svg>

After

Width:  |  Height:  |  Size: 181 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M21 7h-2v2h-2V7h-2V5h2V3h2v2h2v2zm-2 14-7-3-7 3V5c0-1.1.9-2 2-2h7a5.002 5.002 0 0 0 5 7.9V21z"/></svg>

After

Width:  |  Height:  |  Size: 215 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M21 7h-6V5h6v2zm-2 3.9A5.002 5.002 0 0 1 14 3H7c-1.1 0-2 .9-2 2v16l7-3 7 3V10.9z"/></svg>

After

Width:  |  Height:  |  Size: 202 B

View File

@@ -0,0 +1 @@
<svg fill="#2E9BF0" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M18 11v2h4v-2h-4zm-2 6.61c.96.71 2.21 1.65 3.2 2.39.4-.53.8-1.07 1.2-1.6-.99-.74-2.24-1.68-3.2-2.4-.4.54-.8 1.08-1.2 1.61zM20.4 5.6c-.4-.53-.8-1.07-1.2-1.6-.99.74-2.24 1.68-3.2 2.4.4.53.8 1.07 1.2 1.6.96-.72 2.21-1.65 3.2-2.4zM4 9c-1.1 0-2 .9-2 2v2c0 1.1.9 2 2 2h1v4h2v-4h1l5 3V6L8 9H4zm11.5 3c0-1.33-.58-2.53-1.5-3.35v6.69c.92-.81 1.5-2.01 1.5-3.34z"/></svg>

After

Width:  |  Height:  |  Size: 467 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M18 11v2h4v-2h-4zm-2 6.61c.96.71 2.21 1.65 3.2 2.39.4-.53.8-1.07 1.2-1.6-.99-.74-2.24-1.68-3.2-2.4-.4.54-.8 1.08-1.2 1.61zM20.4 5.6c-.4-.53-.8-1.07-1.2-1.6-.99.74-2.24 1.68-3.2 2.4.4.53.8 1.07 1.2 1.6.96-.72 2.21-1.65 3.2-2.4zM4 9c-1.1 0-2 .9-2 2v2c0 1.1.9 2 2 2h1v4h2v-4h1l5 3V6L8 9H4zm5.03 1.71L11 9.53v4.94l-1.97-1.18-.48-.29H4v-2h4.55l.48-.29zM15.5 12c0-1.33-.58-2.53-1.5-3.35v6.69c.92-.81 1.5-2.01 1.5-3.34z"/></svg>

After

Width:  |  Height:  |  Size: 534 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" fill-opacity="0.5" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M4 4h16v12H5.17L4 17.17V4m0-2c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2H4zm2 10h8v2H6v-2zm0-3h12v2H6V9zm0-3h12v2H6V6z"/></svg>

After

Width:  |  Height:  |  Size: 281 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M4 4h16v12H5.17L4 17.17V4m0-2c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2H4zm2 10h8v2H6v-2zm0-3h12v2H6V9zm0-3h12v2H6V6z"/></svg>

After

Width:  |  Height:  |  Size: 262 B

View File

@@ -0,0 +1 @@
<svg fill="#2E9BF0" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2z"/></svg>

After

Width:  |  Height:  |  Size: 183 B

View File

@@ -0,0 +1 @@
<svg fill="#888888" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/></svg>

After

Width:  |  Height:  |  Size: 241 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>

After

Width:  |  Height:  |  Size: 223 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M20 0H4v2h16V0zM4 24h16v-2H4v2zM20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-8 2.75c1.24 0 2.25 1.01 2.25 2.25s-1.01 2.25-2.25 2.25S9.75 10.24 9.75 9 10.76 6.75 12 6.75zM17 17H7v-1.5c0-1.67 3.33-2.5 5-2.5s5 .83 5 2.5V17z"/></svg>

After

Width:  |  Height:  |  Size: 374 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>

After

Width:  |  Height:  |  Size: 249 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M20 6h-8l-2-2H4c-1.11 0-1.99.89-1.99 2L2 18c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2zm-1 8h-3v3h-2v-3h-3v-2h3V9h2v3h3v2z"/></svg>

After

Width:  |  Height:  |  Size: 256 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>

After

Width:  |  Height:  |  Size: 199 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M20 13H4c-.55 0-1 .45-1 1v6c0 .55.45 1 1 1h16c.55 0 1-.45 1-1v-6c0-.55-.45-1-1-1zM7 19c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zM20 3H4c-.55 0-1 .45-1 1v6c0 .55.45 1 1 1h16c.55 0 1-.45 1-1V4c0-.55-.45-1-1-1zM7 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"/></svg>

After

Width:  |  Height:  |  Size: 379 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M5 20h14v-2H5v2zM19 9h-4V3H9v6H5l7 7 7-7z"/></svg>

After

Width:  |  Height:  |  Size: 163 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M18.41 5.8 17.2 4.59c-.78-.78-2.05-.78-2.83 0l-2.68 2.68L3 15.96V20h4.04l8.74-8.74 2.63-2.63c.79-.78.79-2.05 0-2.83zM6.21 18H5v-1.21l8.66-8.66 1.21 1.21L6.21 18zM11 20l4-4h6v4H11z"/></svg>

After

Width:  |  Height:  |  Size: 301 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M20 6h-8l-2-2H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm0 12H4V8h16v10zM8 13.01l1.41 1.41L11 12.84V17h2v-4.16l1.59 1.59L16 13.01 12.01 9 8 13.01z"/></svg>

After

Width:  |  Height:  |  Size: 303 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a.996.996 0 0 0 0-1.41l-2.34-2.34a.996.996 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>

After

Width:  |  Height:  |  Size: 267 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>

After

Width:  |  Height:  |  Size: 218 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>

After

Width:  |  Height:  |  Size: 213 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="m5.25 8.069 2.83-2.827 14.134 14.15-2.83 2.827zm4.236-4.242L12.314.998l5.657 5.656-2.828 2.83zM.999 12.315l2.828-2.829 5.657 5.657-2.828 2.828zM1 21h12v2H1z"/></svg>

After

Width:  |  Height:  |  Size: 278 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/><circle cx="8" cy="14" r="2"/><circle cx="12" cy="8" r="2"/><circle cx="16" cy="14" r="2"/></svg>

After

Width:  |  Height:  |  Size: 337 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 12.75c1.63 0 3.07.39 4.24.9 1.08.48 1.76 1.56 1.76 2.73V18H6v-1.61c0-1.18.68-2.26 1.76-2.73 1.17-.52 2.61-.91 4.24-.91zM4 13c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm1.13 1.1c-.37-.06-.74-.1-1.13-.1-.99 0-1.93.21-2.78.58A2.01 2.01 0 0 0 0 16.43V18h4.5v-1.61c0-.83.23-1.61.63-2.29zM20 13c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm4 3.43c0-.81-.48-1.53-1.22-1.85A6.95 6.95 0 0 0 20 14c-.39 0-.76.04-1.13.1.4.68.63 1.46.63 2.29V18H24v-1.57zM12 6c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3z"/></svg>

After

Width:  |  Height:  |  Size: 626 B

View File

@@ -0,0 +1 @@
<svg fill="#888888" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 4c3.87 0 7 3.13 7 7v2h-2.92L21 17.92V11a9 9 0 0 0-9-9c-1.95 0-3.76.62-5.23 1.68l1.44 1.44A6.914 6.914 0 0 1 12 4zM2.27 1.72 1 3l3.33 3.32A8.899 8.899 0 0 0 3 11v7c0 1.66 1.34 3 3 3h3v-8H5v-2c0-1.17.29-2.26.79-3.22L15 17v4h3c.3 0 .59-.06.86-.14L21 23l1.27-1.27-20-20.01z"/></svg>

After

Width:  |  Height:  |  Size: 390 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M4 11h16v2H4z"/></svg>

After

Width:  |  Height:  |  Size: 155 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M11 7h2v2h-2zm0 4h2v6h-2zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/></svg>

After

Width:  |  Height:  |  Size: 270 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>

After

Width:  |  Height:  |  Size: 218 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm0-8h2V7H3v2zm4 4h14v-2H7v2zm0 4h14v-2H7v2zM7 7v2h14V7H7z"/></svg>

After

Width:  |  Height:  |  Size: 208 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" fill-opacity="0.5" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/></svg>

After

Width:  |  Height:  |  Size: 366 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/></svg>

After

Width:  |  Height:  |  Size: 347 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M11 7 9.6 8.4l2.6 2.6H2v2h10.2l-2.6 2.6L11 17l5-5-5-5zm9 12h-8v2h8c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2h-8v2h8v14z"/></svg>

After

Width:  |  Height:  |  Size: 231 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="m17 7-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.58L17 17l5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4V5z"/></svg>

After

Width:  |  Height:  |  Size: 231 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 14c1.66 0 2.99-1.34 2.99-3L15 5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5.3-3c0 3-2.54 5.1-5.3 5.1S6.7 14 6.7 11H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c3.28-.48 6-3.3 6-6.72h-1.7z"/></svg>

After

Width:  |  Height:  |  Size: 313 B

View File

@@ -0,0 +1 @@
<svg fill="#888888" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 11h-1.7c0 .74-.16 1.43-.43 2.05l1.23 1.23c.56-.98.9-2.09.9-3.28zm-4.02.17c0-.06.02-.11.02-.17V5c0-1.66-1.34-3-3-3S9 3.34 9 5v.18l5.98 5.99zM4.27 3 3 4.27l6.01 6.01V11c0 1.66 1.33 3 2.99 3 .22 0 .44-.03.65-.08l1.66 1.66c-.71.33-1.5.52-2.31.52-2.76 0-5.3-2.1-5.3-5.1H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c.91-.13 1.77-.45 2.54-.9L19.73 21 21 19.73 4.27 3z"/></svg>

After

Width:  |  Height:  |  Size: 473 B

View File

@@ -0,0 +1 @@
<svg fill="#9E8A2E" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 11h-1.7c0 .74-.16 1.43-.43 2.05l1.23 1.23c.56-.98.9-2.09.9-3.28zm-4.02.17c0-.06.02-.11.02-.17V5c0-1.66-1.34-3-3-3S9 3.34 9 5v.18l5.98 5.99zM4.27 3 3 4.27l6.01 6.01V11c0 1.66 1.33 3 2.99 3 .22 0 .44-.03.65-.08l1.66 1.66c-.71.33-1.5.52-2.31.52-2.76 0-5.3-2.1-5.3-5.1H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c.91-.13 1.77-.45 2.54-.9L19.73 21 21 19.73 4.27 3z"/></svg>

After

Width:  |  Height:  |  Size: 473 B

View File

@@ -0,0 +1 @@
<svg fill="#D9443A" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 11h-1.7c0 .74-.16 1.43-.43 2.05l1.23 1.23c.56-.98.9-2.09.9-3.28zm-4.02.17c0-.06.02-.11.02-.17V5c0-1.66-1.34-3-3-3S9 3.34 9 5v.18l5.98 5.99zM4.27 3 3 4.27l6.01 6.01V11c0 1.66 1.33 3 2.99 3 .22 0 .44-.03.65-.08l1.66 1.66c-.71.33-1.5.52-2.31.52-2.76 0-5.3-2.1-5.3-5.1H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c.91-.13 1.77-.45 2.54-.9L19.73 21 21 19.73 4.27 3z"/></svg>

After

Width:  |  Height:  |  Size: 473 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M3 11c0 2.45 1.76 4.47 4.08 4.91l-1.49-1.49L7 13l4 4.01L7 21l-1.41-1.41 1.58-1.58v-.06A7.007 7.007 0 0 1 1 11c0-3.87 3.13-7 7-7h3v2H8c-2.76 0-5 2.24-5 5zm19 0V4h-9v7h9zm-2-2h-5V6h5v3zm-7 4h9v7h-9z"/></svg>

After

Width:  |  Height:  |  Size: 318 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M10.94 8.12 7.48 4.66 9 3h6l1.83 2H20c1.1 0 2 .9 2 2v12c0 .05-.01.1-.02.16l-5.1-5.1c.08-.35.12-.7.12-1.06 0-2.76-2.24-5-5-5-.36 0-.71.04-1.06.12zm9.55 15.19L18.17 21H4c-1.1 0-2-.9-2-2V7c0-.59.27-1.12.68-1.49l-2-2L2.1 2.1l19.8 19.8-1.41 1.41zm-6-5.99-1.5-1.5c-.32.1-.64.18-.99.18-1.66 0-3-1.34-3-3 0-.35.08-.67.19-.98l-1.5-1.5A4.74 4.74 0 0 0 7 13c0 2.76 2.24 5 5 5 .91 0 1.76-.25 2.49-.68z"/></svg>

After

Width:  |  Height:  |  Size: 511 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M7.58 4.08 6.15 2.65C3.75 4.48 2.17 7.3 2.03 10.5h2a8.445 8.445 0 0 1 3.55-6.42zm12.39 6.42h2c-.15-3.2-1.73-6.02-4.12-7.85l-1.42 1.43a8.495 8.495 0 0 1 3.54 6.42zM18 11c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2v-5zm-6 11c.14 0 .27-.01.4-.04.65-.14 1.18-.58 1.44-1.18.1-.24.15-.5.15-.78h-4c.01 1.1.9 2 2.01 2z"/></svg>

After

Width:  |  Height:  |  Size: 500 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M15 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm-9-2V7H4v3H1v2h3v3h2v-3h3v-2H6zm9 4c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>

After

Width:  |  Height:  |  Size: 263 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M8.65 5.82a3.999 3.999 0 1 1 5.53 5.53L8.65 5.82zM20 17.17c-.02-1.1-.63-2.11-1.61-2.62-.54-.28-1.13-.54-1.77-.76L20 17.17zm1.19 4.02L2.81 2.81 1.39 4.22l8.89 8.89c-1.81.23-3.39.79-4.67 1.45A2.97 2.97 0 0 0 4 17.22V20h13.17l2.61 2.61 1.41-1.42z"/></svg>

After

Width:  |  Height:  |  Size: 365 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M14 8c0-2.21-1.79-4-4-4S6 5.79 6 8s1.79 4 4 4 4-1.79 4-4zm-2 0c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zM2 18v2h16v-2c0-2.66-5.33-4-8-4s-8 1.34-8 4zm2 0c.2-.71 3.3-2 6-2 2.69 0 5.77 1.28 6 2H4zm13-8h6v2h-6z"/></svg>

After

Width:  |  Height:  |  Size: 330 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M14 8c0-2.21-1.79-4-4-4S6 5.79 6 8s1.79 4 4 4 4-1.79 4-4zm3 2v2h6v-2h-6zM2 18v2h16v-2c0-2.66-5.33-4-8-4s-8 1.34-8 4z"/></svg>

After

Width:  |  Height:  |  Size: 238 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M13 3h-2v10h2V3zm4.83 2.17-1.42 1.42A6.92 6.92 0 0 1 19 12c0 3.87-3.13 7-7 7A6.995 6.995 0 0 1 7.58 6.58L6.17 5.17A8.932 8.932 0 0 0 3 12a9 9 0 0 0 18 0c0-2.74-1.23-5.18-3.17-6.83z"/></svg>

After

Width:  |  Height:  |  Size: 302 B

View File

@@ -0,0 +1 @@
<svg fill="#2E9BF0" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><circle cx="9" cy="9" r="4"/><path d="M9 15c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4zm7.76-9.64-1.68 1.69c.84 1.18.84 2.71 0 3.89l1.68 1.69c2.02-2.02 2.02-5.07 0-7.27zM20.07 2l-1.63 1.63c2.77 3.02 2.77 7.56 0 10.74L20.07 16c3.9-3.89 3.91-9.95 0-14z"/></svg>

After

Width:  |  Height:  |  Size: 360 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M17.65 6.35A7.958 7.958 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>

After

Width:  |  Height:  |  Size: 324 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11H7v-2h10v2z"/></svg>

After

Width:  |  Height:  |  Size: 204 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M7 11v2h10v-2H7zm5-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/></svg>

After

Width:  |  Height:  |  Size: 261 B

View File

@@ -0,0 +1,90 @@
[info]
name = Material
[options]
# Complete for what the client draws; the rest stays with the client's own icons.
FALLBACK=false
[gfxfiles]
SERVER_GREEN = dns.svg
CHANNEL_GREEN = chat-outlined-a50.svg
CHANNEL_GREEN_SUBSCRIBED = chat-outlined.svg
CHANNEL_PRIVATE = lock-a50.svg
CHANNEL_PRIVATE_SUBSCRIBED = lock.svg
CHANNEL_RED = block-d9443a-a50.svg
CHANNEL_RED_SUBSCRIBED = block-d9443a.svg
CHANNEL_CHAT = chat-outlined.svg
CHANNEL_COMMANDER = campaign-outlined.svg
PLAYER_OFF = circle-outlined-888888.svg
PLAYER_ON = circle-2e9bf0.svg
PLAYER_COMMANDER_OFF = campaign-outlined.svg
PLAYER_COMMANDER_ON = campaign-2e9bf0.svg
AWAY = bedtime-9e8a2e.svg
SERVER_QUERY = terminal.svg
INPUT_MUTED = mic_off-d9443a.svg
INPUT_MUTED_LOCAL = mic_off-9e8a2e.svg
OUTPUT_MUTED = volume_off-d9443a.svg
HARDWARE_INPUT_MUTED = mic_off-888888.svg
HARDWARE_OUTPUT_MUTED = headset_off-888888.svg
CAPTURE = mic.svg
PLAYBACK = volume_up.svg
VOLUME = volume_up.svg
ACTIVATE_MICROPHONE = record_voice_over-2e9bf0.svg
CONNECT = login.svg
DISCONNECT = logout.svg
QUIT = power_settings_new.svg
SETTINGS = settings.svg
REFRESH = refresh.svg
FILE_REFRESH = refresh.svg
INFO = info.svg
ABOUT = info-outlined.svg
WARNING = warning.svg
ERROR = error.svg
CHANNEL_SWITCH = login.svg
CHANNEL_CREATE = add.svg
CHANNEL_CREATE_SUB = subdirectory_arrow_right.svg
CHANNEL_CREATE_SPACER = horizontal_rule.svg
CHANNEL_EDIT = edit.svg
CHANNEL_DELETE = delete.svg
SUBSCRIBE_TO_CHANNEL = visibility.svg
UNSUBSCRIBE_FROM_CHANNEL = visibility_off.svg
SUBSCRIBE_TO_CHANNEL_FAMILY = visibility-outlined.svg
UNSUBSCRIBE_FROM_CHANNEL_FAMILY = visibility_off-outlined.svg
SUBSCRIBE_TO_ALL_CHANNELS = visibility.svg
UNSUBSCRIBE_FROM_ALL_CHANNELS = visibility_off.svg
PLAYER_CHAT = chat-outlined.svg
NEW_CHAT = add_comment-outlined.svg
POKE = notifications_active.svg
CHANGE_NICKNAME = drive_file_rename_outline.svg
MOVE_CLIENT_TO_OWN_CHANNEL = move_down.svg
KICK_FROM_CHANNEL = remove_circle_outline.svg
KICK_FROM_SERVER = remove_circle.svg
BAN_CLIENT = gavel.svg
BAN_LIST = list.svg
PERMISSIONS_SERVER_GROUPS = groups.svg
PERMISSIONS_CHANNEL_GROUPS = group_work-outlined.svg
ADD_FRIEND = person_add.svg
REMOVE_FRIEND = person_remove.svg
ADD_FOE = person_off.svg
REMOVE_FOE = person_remove-outlined.svg
EDIT_FRIEND_FOE_STATUS = contacts.svg
CONTACT = contacts.svg
IDENTITY_MANAGER = badge.svg
BOOKMARK_MANAGER = bookmark.svg
BOOKMARK_ADD = bookmark_add.svg
BOOKMARK_REMOVE = bookmark_remove.svg
UPLOAD_AVATAR = add_a_photo.svg
DELETE_AVATAR = no_photography.svg
ADD = add.svg
EDIT = edit.svg
DELETE = delete.svg
COPY = content_copy.svg
DEFAULT = star.svg
CLOSE_BUTTON = close.svg
TAB_CLOSE_BUTTON = close.svg
FOLDER = folder.svg
ADD_FOLDER = create_new_folder.svg
FILE_UP = drive_folder_upload.svg
FILETRANSFER = swap_vert.svg
UPLOAD = upload.svg
DOWNLOAD = download.svg

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.488.488 0 0 0-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 0 0-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>

After

Width:  |  Height:  |  Size: 814 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></svg>

After

Width:  |  Height:  |  Size: 210 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="m19 15-6 6-1.42-1.42L15.17 16H4V4h2v10h9.17l-3.59-3.58L13 9l6 6z"/></svg>

After

Width:  |  Height:  |  Size: 186 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M16 17.01V10h-2v7.01h-3L15 21l4-3.99h-3zM9 3 5 6.99h3V14h2V6.99h3L9 3z"/></svg>

After

Width:  |  Height:  |  Size: 192 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M20 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16c1.1 0 2-.9 2-2V6a2 2 0 0 0-2-2zm0 14H4V8h16v10zm-2-1h-6v-2h6v2zM7.5 17l-1.41-1.41L8.67 13l-2.59-2.59L7.5 9l4 4-4 4z"/></svg>

After

Width:  |  Height:  |  Size: 279 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M5 20h14v-2H5v2zm0-10h4v6h6v-6h4l-7-7-7 7z"/></svg>

After

Width:  |  Height:  |  Size: 164 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 6a9.77 9.77 0 0 1 8.82 5.5C19.17 14.87 15.79 17 12 17s-7.17-2.13-8.82-5.5A9.77 9.77 0 0 1 12 6m0-2C7 4 2.73 7.11 1 11.5 2.73 15.89 7 19 12 19s9.27-3.11 11-7.5C21.27 7.11 17 4 12 4zm0 5a2.5 2.5 0 0 1 0 5 2.5 2.5 0 0 1 0-5m0-2c-2.48 0-4.5 2.02-4.5 4.5S9.52 16 12 16s4.5-2.02 4.5-4.5S14.48 7 12 7z"/></svg>

After

Width:  |  Height:  |  Size: 420 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>

After

Width:  |  Height:  |  Size: 331 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 6a9.77 9.77 0 0 1 8.82 5.5 9.647 9.647 0 0 1-2.41 3.12l1.41 1.41c1.39-1.23 2.49-2.77 3.18-4.53C21.27 7.11 17 4 12 4c-1.27 0-2.49.2-3.64.57l1.65 1.65C10.66 6.09 11.32 6 12 6zm-1.07 1.14L13 9.21c.57.25 1.03.71 1.28 1.28l2.07 2.07c.08-.34.14-.7.14-1.07C16.5 9.01 14.48 7 12 7c-.37 0-.72.05-1.07.14zM2.01 3.87l2.68 2.68A11.738 11.738 0 0 0 1 11.5C2.73 15.89 7 19 12 19c1.52 0 2.98-.29 4.32-.82l3.42 3.42 1.41-1.41L3.42 2.45 2.01 3.87zm7.5 7.5 2.61 2.61c-.04.01-.08.02-.12.02a2.5 2.5 0 0 1-2.5-2.5c0-.05.01-.08.01-.13zm-3.4-3.4 1.75 1.75a4.6 4.6 0 0 0-.36 1.78 4.507 4.507 0 0 0 6.27 4.14l.98.98c-.88.24-1.8.38-2.75.38a9.77 9.77 0 0 1-8.82-5.5c.7-1.43 1.72-2.61 2.93-3.53z"/></svg>

After

Width:  |  Height:  |  Size: 793 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46A11.804 11.804 0 0 0 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78 3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z"/></svg>

After

Width:  |  Height:  |  Size: 627 B

View File

@@ -0,0 +1 @@
<svg fill="#D9443A" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M16.5 12A4.5 4.5 0 0 0 14 7.97v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51A8.796 8.796 0 0 0 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3 3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06a8.99 8.99 0 0 0 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4 9.91 6.09 12 8.18V4z"/></svg>

After

Width:  |  Height:  |  Size: 460 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0 0 14 7.97v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg>

After

Width:  |  Height:  |  Size: 305 B

View File

@@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>

After

Width:  |  Height:  |  Size: 172 B

View File

@@ -1,5 +1,6 @@
package com.ts3client.gfx;
import com.ts3client.net.ClientStatus;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@@ -124,4 +125,36 @@ class IconPackTest {
assertNull(IconPack.load(Files.createDirectory(dir.resolve("empty")).toFile()));
assertNull(IconPack.load(null));
}
@Test
void shipsAMaterialPackForEveryIconTheTreeDraws() {
IconPack pack = IconPacks.material();
assertNotNull(pack);
assertEquals("Material", pack.name());
assertFalse(pack.fallsBack());
assertNull(pack.source());
for (ClientStatus status : ClientStatus.values()) {
String key = IconKeys.client(status);
assertTrue(pack.isVector(key, 16), key);
assertNotNull(pack.icon(key, 16), key);
}
for (boolean password : new boolean[]{false, true}) {
for (boolean full : new boolean[]{false, true}) {
for (boolean subscribed : new boolean[]{false, true}) {
for (String key : IconKeys.channel(password, full, subscribed)) {
assertNotNull(pack.icon(key, 16), key);
}
}
}
}
}
@Test
void setsTheColourVectorArtPaintsWith() {
byte[] svg = "<svg fill=\"currentColor\"><path fill=\"#123456\"/></svg>".getBytes(StandardCharsets.UTF_8);
assertEquals("<svg fill=\"#0A0B0C\"><path fill=\"#123456\"/></svg>",
new String(IconPack.withCurrentColor(svg, 0xFF0A0B0C), StandardCharsets.UTF_8));
byte[] fixed = "<svg/>".getBytes(StandardCharsets.UTF_8);
assertTrue(fixed == IconPack.withCurrentColor(fixed, 0));
}
}

View File

@@ -43,7 +43,7 @@ final class DesignPanel extends JPanel {
private static final int TILE = 44;
/** Stands for "no pack": the icons the client draws itself. */
private static final String BUILT_IN = "Built-in icons";
private static final String BUILT_IN = "Plain (drawn by the client)";
/** The scale factors offered besides following the desktop (0). */
private static final double[] SCALES = {0, 1, 1.25, 1.5, 1.75, 2, 2.5, 3};

View File

@@ -12,6 +12,8 @@ import com.ts3client.gfx.IconPacks;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.UIManager;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
@@ -34,7 +36,8 @@ import java.util.concurrent.CopyOnWriteArrayList;
* instead, so the client always has a complete set.
*
* <p>Rendering is cached per key and size: vector art is rasterised at the size it
* is asked for, bitmaps are scaled from the closest resolution the pack ships.
* is asked for, in the look-and-feel's text colour where it paints with
* {@code currentColor}; bitmaps are scaled from the closest resolution the pack ships.
* {@link #SIZE} and {@link #TOOLBAR_SIZE} are unscaled; sizes passed in are pixels.
*/
public final class IconTheme {
@@ -51,7 +54,8 @@ public final class IconTheme {
private volatile List<IconPack> packs = List.of();
private volatile IconPack active;
private volatile IconPack fallback;
/** Where icons are looked up; what none of these has is drawn by {@link Icons}. */
private volatile List<IconPack> lookupOrder = List.of();
private IconTheme() {
}
@@ -72,14 +76,23 @@ public final class IconTheme {
/** Re-scans the pack folders and activates the one the settings name. */
public void reload(Settings settings) {
packs = IconPacks.findAll(new File(AppDirs.profile(), "gfx"), settings.iconPackDir);
fallback = IconPacks.fallback(packs);
setPack(IconPacks.select(packs, settings.iconPack));
IconPack pack = IconPacks.select(packs, settings.iconPack);
// The shipped pack survives a re-scan as the same object, so setPack alone may not notice.
lookupOrder = IconPacks.lookupOrder(pack, packs, null);
setPack(pack);
}
/** Switches to a pack (which may be {@code null} for the built-in icons). */
public void setPack(IconPack pack) {
if (active == pack) return;
active = pack;
lookupOrder = IconPacks.lookupOrder(pack, packs, null);
cache.clear();
for (Runnable listener : listeners) listener.run();
}
/** Re-renders the icons, which follow the look-and-feel's text colour. */
public void refresh() {
cache.clear();
for (Runnable listener : listeners) listener.run();
}
@@ -105,22 +118,29 @@ public final class IconTheme {
ImageIcon cached = cache.get(cacheKey);
if (cached != null) return cached;
ImageIcon icon = render(active, key, size);
if (icon == null && active != null && active.fallsBack() && fallback != active) {
icon = render(fallback, key, size);
ImageIcon icon = null;
for (IconPack pack : lookupOrder) {
icon = render(pack, key, size);
if (icon != null) break;
}
if (icon != null) cache.put(cacheKey, icon);
return icon;
}
private static ImageIcon render(IconPack pack, String key, int size) {
if (pack == null) return null;
byte[] data = pack.icon(key, size);
if (data == null || data.length == 0) return null;
BufferedImage image = pack.isVector(key, size) ? rasterize(data, size) : decode(data, size);
BufferedImage image = pack.isVector(key, size)
? rasterize(IconPack.withCurrentColor(data, textColor().getRGB()), size)
: decode(data, size);
return image == null ? null : new ImageIcon(image);
}
private static Color textColor() {
Color c = UIManager.getColor("Label.foreground");
return c != null ? c : Color.BLACK;
}
private static BufferedImage rasterize(byte[] data, int size) {
try {
SVGDocument document = new SVGLoader().load(

View File

@@ -1,6 +1,7 @@
package com.ts3client.ui;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.gfx.IconKeys;
import javax.swing.ImageIcon;
import java.awt.BasicStroke;
@@ -73,10 +74,7 @@ public final class Icons {
return icon != null ? icon : make(size, fallback);
}
/**
* Like {@link #themed(String, Painter)}, but tries the keys in turn — packs need not
* ship every variant (none of TeamSpeak's own has a subscribed password channel).
*/
/** Like {@link #themed(String, Painter)}, but tries the keys in turn. */
private static ImageIcon themed(String[] keys, Painter fallback) {
for (String key : keys) {
ImageIcon icon = IconTheme.icon(key, small());
@@ -108,27 +106,22 @@ public final class Icons {
}
public static ImageIcon channel(boolean subscribed) {
return themed(keys("CHANNEL_GREEN", subscribed),
return themed(IconKeys.channel(false, false, subscribed),
g -> paintChannel(g, new Color(0x3E7CB1), false, subscribed));
}
/** A channel nobody can join without its password. */
public static ImageIcon channelLocked(boolean subscribed) {
return themed(keys("CHANNEL_PRIVATE", subscribed),
return themed(IconKeys.channel(true, false, subscribed),
g -> paintChannel(g, new Color(0x8A6D3B), true, subscribed));
}
/** A channel that has reached its client limit. */
public static ImageIcon channelFull(boolean subscribed) {
return themed(keys("CHANNEL_RED", subscribed),
return themed(IconKeys.channel(false, true, subscribed),
g -> paintChannel(g, new Color(0xA53F3F), false, subscribed));
}
/** The pack keys to try for a channel icon, most specific first. */
private static String[] keys(String base, boolean subscribed) {
return subscribed ? new String[]{base + "_SUBSCRIBED", base} : new String[]{base};
}
/** The chat tab for the current channel. */
public static ImageIcon channelChat() {
ImageIcon icon = of("CHANNEL_CHAT");

View File

@@ -43,6 +43,7 @@ public final class LookAndFeelManager {
*/
public static void switchTo(Settings.Appearance appearance) {
install(appearance);
IconTheme.get().refresh();
FlatLaf.updateUI();
for (Window w : Window.getWindows()) {
w.repaint();

View File

@@ -0,0 +1,165 @@
#!/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("<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}")
(OUT / "settings.ini").write_text(INI_HEAD + "\n".join(lines) + "\n")
if __name__ == "__main__":
main()