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:
@@ -2,9 +2,23 @@ plugins {
|
|||||||
`java-library`
|
`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 {
|
java {
|
||||||
sourceSets["main"].java.setSrcDirs(listOf(rootDir.resolve("../ts3-client/core/src/main/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.setSrcDirs(listOf(rootDir.resolve("../ts3-client/core/src/main/resources")))
|
||||||
|
sourceSets["main"].resources.srcDir(teamspeakIconPacks)
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<JavaCompile>().configureEach {
|
tasks.withType<JavaCompile>().configureEach {
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
<properties>
|
<properties>
|
||||||
<!-- Android runs core too, and it only knows Java 17. -->
|
<!-- Android runs core too, and it only knows Java 17. -->
|
||||||
<maven.compiler.release>17</maven.compiler.release>
|
<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>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
@@ -31,4 +33,22 @@
|
|||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</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>
|
</project>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import java.util.regex.Matcher;
|
|||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipFile;
|
import java.util.zip.ZipFile;
|
||||||
|
import java.util.zip.ZipInputStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A TeamSpeak icon pack: a zip (or unpacked directory) of images plus an optional
|
* 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);
|
Map<String, Long> files = zipped ? listZip(source) : listDirectory(source);
|
||||||
if (files == null || files.isEmpty()) return null;
|
if (files == null || files.isEmpty()) return null;
|
||||||
Storage storage = zipped ? path -> readFromZip(source, path) : path -> readFromDirectory(source, path);
|
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()
|
String iniPath = files.keySet().stream()
|
||||||
.filter(p -> p.equalsIgnoreCase(INI))
|
.filter(p -> p.equalsIgnoreCase(INI))
|
||||||
.findFirst().orElse(null);
|
.findFirst().orElse(null);
|
||||||
@@ -190,7 +218,6 @@ public final class IconPack {
|
|||||||
: byConvention(files);
|
: byConvention(files);
|
||||||
if (entries.isEmpty()) return null;
|
if (entries.isEmpty()) return null;
|
||||||
|
|
||||||
String id = source.getName();
|
|
||||||
String name = info.getOrDefault("name", displayName(id));
|
String name = info.getOrDefault("name", displayName(id));
|
||||||
return new IconPack(id, source, name, storage, fallback, entries);
|
return new IconPack(id, source, name, storage, fallback, entries);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,14 +23,21 @@ public final class IconPacks {
|
|||||||
/** The pack shipped with the client, drawn from Google's Material icons. */
|
/** The pack shipped with the client, drawn from Google's Material icons. */
|
||||||
public static final String MATERIAL_ID = "material";
|
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 IconPack material;
|
||||||
|
private static volatile List<IconPack> teamspeak;
|
||||||
|
|
||||||
private IconPacks() {
|
private IconPacks() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads every pack found under {@code extraDirectory} and the well-known
|
* 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 userDirectory the client's own pack folder (may not exist yet)
|
||||||
* @param extraDirectory an additional folder chosen by the user, or {@code null}
|
* @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);
|
if (pack != null) byId.put(pack.id(), pack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (IconPack shipped : teamspeak()) byId.putIfAbsent(shipped.id(), shipped);
|
||||||
IconPack shipped = material();
|
IconPack shipped = material();
|
||||||
if (shipped != null) byId.putIfAbsent(shipped.id(), shipped);
|
if (shipped != null) byId.putIfAbsent(shipped.id(), shipped);
|
||||||
List<IconPack> packs = new ArrayList<>(byId.values());
|
List<IconPack> packs = new ArrayList<>(byId.values());
|
||||||
@@ -61,6 +69,19 @@ public final class IconPacks {
|
|||||||
return material;
|
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 */
|
/** @return the pack with this id, or {@code null} when it is not installed */
|
||||||
public static IconPack select(List<IconPack> packs, String id) {
|
public static IconPack select(List<IconPack> packs, String id) {
|
||||||
for (IconPack p : packs) {
|
for (IconPack p : packs) {
|
||||||
|
|||||||
Reference in New Issue
Block a user