Ship TeamSpeak's icon packs with both clients

Default, Default Colored 2014 and Default Mono 2014 now come inside the
desktop JAR and the APK, so the default pack is there without a TeamSpeak 3
install; before, a desktop without one had no icons at all. As with the
sound packs, the builds take them from the TeamSpeak 3 client next to the
repository (ts3.client.dir to point elsewhere) and ship none without it.

Core reads them from the classpath in memory; a pack of the same name found
on disk, in an install or imported, still comes first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 23:16:48 +00:00
parent af0564702e
commit 4ca5534ef4
4 changed files with 84 additions and 2 deletions

View File

@@ -2,9 +2,23 @@ plugins {
`java-library`
}
val ts3ClientDir: String = providers.gradleProperty("ts3.client.dir")
.orElse(providers.environmentVariable("TS3_CLIENT_DIR"))
.getOrElse(rootDir.resolve("../TeamSpeak3-Client-linux_amd64").path)
/** TeamSpeak's own icon packs, proprietary and so taken from an install, as the desktop's build does. */
val teamspeakIconPacks = tasks.register<Sync>("teamspeakIconPacks") {
into(layout.buildDirectory.dir("generated/iconPacks"))
from(File(ts3ClientDir, "gfx")) {
include("default.zip", "default_colored_2014.zip", "default_mono_2014.zip")
into("gfx/teamspeak")
}
}
java {
sourceSets["main"].java.setSrcDirs(listOf(rootDir.resolve("../ts3-client/core/src/main/java")))
sourceSets["main"].resources.setSrcDirs(listOf(rootDir.resolve("../ts3-client/core/src/main/resources")))
sourceSets["main"].resources.srcDir(teamspeakIconPacks)
}
tasks.withType<JavaCompile>().configureEach {

View File

@@ -17,6 +17,8 @@
<properties>
<!-- Android runs core too, and it only knows Java 17. -->
<maven.compiler.release>17</maven.compiler.release>
<!-- The TeamSpeak 3 client whose icon packs ship (-Dts3.client.dir=…); without one, none do. -->
<ts3.client.dir>${project.basedir}/../../TeamSpeak3-Client-linux_amd64</ts3.client.dir>
</properties>
<dependencies>
@@ -31,4 +33,22 @@
<scope>test</scope>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
<!-- TeamSpeak's own icon packs, proprietary and so taken from an install, not the repository. -->
<resource>
<directory>${ts3.client.dir}/gfx</directory>
<targetPath>gfx/teamspeak</targetPath>
<includes>
<include>default.zip</include>
<include>default_colored_2014.zip</include>
<include>default_mono_2014.zip</include>
</includes>
</resource>
</resources>
</build>
</project>

View File

@@ -17,6 +17,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
/**
* A TeamSpeak icon pack: a zip (or unpacked directory) of images plus an optional
@@ -173,7 +174,34 @@ 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);
return create(source.getName(), source, files, storage);
}
/**
* Reads a pack's {@code .zip} shipped on the classpath, held in memory as it cannot be
* opened as a file there.
*
* @param resource the zip, e.g. {@code /gfx/teamspeak/default.zip}
* @return the pack, or {@code null} when it is not there or holds no icons
*/
public static IconPack loadZipResource(String resource) {
Map<String, byte[]> contents = new LinkedHashMap<>();
try (InputStream in = IconPack.class.getResourceAsStream(resource)) {
if (in == null) return null;
try (ZipInputStream zip = new ZipInputStream(in)) {
for (ZipEntry e; (e = zip.getNextEntry()) != null; ) {
if (!e.isDirectory()) contents.put(e.getName(), zip.readAllBytes());
}
}
} catch (IOException e) {
return null;
}
Map<String, Long> files = new LinkedHashMap<>();
contents.forEach((path, data) -> files.put(path, (long) data.length));
return create(resource.substring(resource.lastIndexOf('/') + 1), null, files, contents::get);
}
private static IconPack create(String id, File source, Map<String, Long> files, Storage storage) {
String iniPath = files.keySet().stream()
.filter(p -> p.equalsIgnoreCase(INI))
.findFirst().orElse(null);
@@ -190,7 +218,6 @@ public final class IconPack {
: byConvention(files);
if (entries.isEmpty()) return null;
String id = source.getName();
String name = info.getOrDefault("name", displayName(id));
return new IconPack(id, source, name, storage, fallback, entries);
}

View File

@@ -23,14 +23,21 @@ public final class IconPacks {
/** The pack shipped with the client, drawn from Google's Material icons. */
public static final String MATERIAL_ID = "material";
/** TeamSpeak's own packs, which the build ships when it finds a TeamSpeak 3 client to take them from. */
private static final List<String> TEAMSPEAK_IDS =
List.of(DEFAULT_ID, "default_colored_2014.zip", "default_mono_2014.zip");
private static final String TEAMSPEAK_RESOURCE = "/gfx/teamspeak/";
private static volatile IconPack material;
private static volatile List<IconPack> teamspeak;
private IconPacks() {
}
/**
* Loads every pack found under {@code extraDirectory} and the well-known
* locations, the first search root winning on a name clash.
* locations, the first search root winning on a name clash, then the shipped
* ones the folders did not have.
*
* @param userDirectory the client's own pack folder (may not exist yet)
* @param extraDirectory an additional folder chosen by the user, or {@code null}
@@ -48,6 +55,7 @@ public final class IconPacks {
if (pack != null) byId.put(pack.id(), pack);
}
}
for (IconPack shipped : teamspeak()) byId.putIfAbsent(shipped.id(), shipped);
IconPack shipped = material();
if (shipped != null) byId.putIfAbsent(shipped.id(), shipped);
List<IconPack> packs = new ArrayList<>(byId.values());
@@ -61,6 +69,19 @@ public final class IconPacks {
return material;
}
/** @return TeamSpeak's packs the build shipped, which a pack of the same name found on disk replaces */
public static List<IconPack> teamspeak() {
if (teamspeak == null) {
List<IconPack> packs = new ArrayList<>();
for (String id : TEAMSPEAK_IDS) {
IconPack pack = IconPack.loadZipResource(TEAMSPEAK_RESOURCE + id);
if (pack != null) packs.add(pack);
}
teamspeak = List.copyOf(packs);
}
return teamspeak;
}
/** @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) {