Resolve the client's data directories in one place

Stores read their paths from AppDirs instead of user.home, so a platform
without a home directory can point them elsewhere at startup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 07:52:32 +00:00
parent 1d61f458ae
commit 3c389de36b
13 changed files with 113 additions and 72 deletions

View File

@@ -1,5 +1,6 @@
package com.ts3client.chatlog;
import com.ts3client.config.AppDirs;
import com.ts3client.text.BBCode;
import com.ts3client.text.HtmlText;
import com.ts3client.text.TextQuery;
@@ -121,7 +122,7 @@ public final class ChatLogStore {
}
public static Path defaultRoot() {
return Path.of(System.getProperty("user.home", "."), ".ts3client", "chats");
return AppDirs.chatLogs();
}
public static String decodePathComponent(String value) {

View File

@@ -0,0 +1,53 @@
package com.ts3client.config;
import java.io.File;
import java.nio.file.Path;
/**
* Where the client keeps its own state. On the desktop that is {@code ~/.ts3jclient}, with chat
* logs in the TeamSpeak 3 client's {@code ~/.ts3client/chats} so both clients share them. A
* platform without a meaningful home directory (Android) calls {@link #init} once at startup,
* before any store is loaded.
*/
public final class AppDirs {
private static volatile File profile;
private static volatile Path chatLogs;
private AppDirs() {
}
public static void init(File profileDir, Path chatLogDir) {
profile = profileDir;
chatLogs = chatLogDir;
}
/** The directory holding all persistent client state (settings, identities, caches). */
public static File profile() {
File dir = profile;
if (dir == null) {
dir = new File(System.getProperty("user.home", "."), ".ts3jclient");
profile = dir;
}
return dir;
}
public static File file(String name) {
return new File(profile(), name);
}
/** Creates the profile directory if it does not exist yet; call before writing into it. */
public static void createProfile() {
//noinspection ResultOfMethodCallIgnored
profile().mkdirs();
}
public static Path chatLogs() {
Path dir = chatLogs;
if (dir == null) {
dir = Path.of(System.getProperty("user.home", "."), ".ts3client", "chats");
chatLogs = dir;
}
return dir;
}
}

View File

@@ -13,8 +13,9 @@ import java.util.Properties;
*/
public final class AwayMessages {
private static final File DIR = new File(System.getProperty("user.home"), ".ts3jclient");
private static final File FILE = new File(DIR, "away.properties");
private static File file() {
return AppDirs.file("away.properties");
}
private final List<String> entries = new ArrayList<>();
@@ -36,9 +37,9 @@ public final class AwayMessages {
public static AwayMessages load() {
AwayMessages m = new AwayMessages();
if (!FILE.isFile()) return m.withDefaults();
if (!file().isFile()) return m.withDefaults();
Properties p = new Properties();
try (FileInputStream in = new FileInputStream(FILE)) {
try (FileInputStream in = new FileInputStream(file())) {
p.load(in);
} catch (Exception e) {
return m.withDefaults();
@@ -58,11 +59,8 @@ public final class AwayMessages {
p.setProperty("message." + i, entries.get(i));
}
try {
if (!DIR.isDirectory()) {
//noinspection ResultOfMethodCallIgnored
DIR.mkdirs();
}
try (FileOutputStream out = new FileOutputStream(FILE)) {
AppDirs.createProfile();
try (FileOutputStream out = new FileOutputStream(file())) {
p.store(out, "TS3J client away messages");
}
} catch (Exception ignored) {

View File

@@ -14,8 +14,9 @@ import java.util.Properties;
*/
public final class BanReasonPresets {
private static final File DIR = new File(System.getProperty("user.home"), ".ts3jclient");
private static final File FILE = new File(DIR, "banreasons.properties");
private static File file() {
return AppDirs.file("banreasons.properties");
}
private final List<String> entries = new ArrayList<>();
@@ -36,9 +37,9 @@ public final class BanReasonPresets {
public static BanReasonPresets load() {
BanReasonPresets presets = new BanReasonPresets();
if (!FILE.isFile()) return presets;
if (!file().isFile()) return presets;
Properties p = new Properties();
try (FileInputStream in = new FileInputStream(FILE)) {
try (FileInputStream in = new FileInputStream(file())) {
p.load(in);
} catch (Exception e) {
return presets;
@@ -58,11 +59,8 @@ public final class BanReasonPresets {
p.setProperty("reason." + i, entries.get(i));
}
try {
if (!DIR.isDirectory()) {
//noinspection ResultOfMethodCallIgnored
DIR.mkdirs();
}
try (FileOutputStream out = new FileOutputStream(FILE)) {
AppDirs.createProfile();
try (FileOutputStream out = new FileOutputStream(file())) {
p.store(out, "TS3J client ban reason presets");
}
} catch (Exception ignored) {

View File

@@ -13,8 +13,9 @@ import java.util.Properties;
*/
public final class Bookmarks {
private static final File DIR = new File(System.getProperty("user.home"), ".ts3jclient");
private static final File FILE = new File(DIR, "bookmarks.properties");
private static File file() {
return AppDirs.file("bookmarks.properties");
}
private final List<Bookmark> entries = new ArrayList<>();
@@ -41,9 +42,9 @@ public final class Bookmarks {
public static Bookmarks load() {
Bookmarks b = new Bookmarks();
if (!FILE.isFile()) return b;
if (!file().isFile()) return b;
Properties p = new Properties();
try (FileInputStream in = new FileInputStream(FILE)) {
try (FileInputStream in = new FileInputStream(file())) {
p.load(in);
} catch (Exception e) {
return b;
@@ -83,11 +84,8 @@ public final class Bookmarks {
p.setProperty(prefix + "connectOnStartup", Boolean.toString(bm.connectOnStartup));
}
try {
if (!DIR.isDirectory()) {
//noinspection ResultOfMethodCallIgnored
DIR.mkdirs();
}
try (FileOutputStream out = new FileOutputStream(FILE)) {
AppDirs.createProfile();
try (FileOutputStream out = new FileOutputStream(file())) {
p.store(out, "TS3J client bookmarks");
}
} catch (Exception ignored) {

View File

@@ -32,8 +32,6 @@ public final class IdentityStore {
/** Security level new identities are generated with, matching the TS3 client's default. */
public static final int DEFAULT_SECURITY_LEVEL = 8;
private static final File DIR = new File(System.getProperty("user.home"), ".ts3jclient");
private static final File IDENTITY_DIR = new File(DIR, "identities");
private final List<IdentityEntry> entries = new ArrayList<>();
@@ -51,7 +49,7 @@ public final class IdentityStore {
/** Loads all identities from disk, migrating a legacy single-identity file if needed. */
public static IdentityStore load(Settings settings) {
IdentityStore store = new IdentityStore();
File[] files = IDENTITY_DIR.listFiles((d, n) -> n.toLowerCase(Locale.ROOT).endsWith(".ini"));
File[] files = directory().listFiles((d, n) -> n.toLowerCase(Locale.ROOT).endsWith(".ini"));
if (files != null) {
for (File f : files) {
IdentityEntry e = read(f);
@@ -218,7 +216,7 @@ public final class IdentityStore {
private IdentityEntry create(String name, LocalIdentity identity) throws IOException {
String id = uniqueId(name);
File file = new File(IDENTITY_DIR, id + ".ini");
File file = new File(directory(), id + ".ini");
write(identity, name, file);
IdentityEntry entry = new IdentityEntry(id, name, file, identity);
entries.add(entry);
@@ -283,7 +281,7 @@ public final class IdentityStore {
private boolean taken(String id) {
if (byId(id) != null) return true;
return new File(IDENTITY_DIR, id + ".ini").exists();
return new File(directory(), id + ".ini").exists();
}
private void sort() {
@@ -331,6 +329,6 @@ public final class IdentityStore {
/** Directory the managed identity files live in. */
public static File directory() {
return IDENTITY_DIR;
return AppDirs.file("identities");
}
}

View File

@@ -64,8 +64,9 @@ public final class Settings {
}
}
private static final File DIR = new File(System.getProperty("user.home"), ".ts3jclient");
private static final File FILE = new File(DIR, "settings.properties");
private static File file() {
return AppDirs.file("settings.properties");
}
private final Properties props = new Properties();
@@ -74,7 +75,7 @@ public final class Settings {
public String nickname = System.getProperty("user.name", "TS3J User");
public String serverPassword = "";
/** Legacy single-identity file; only used to migrate into the identity store. */
public String identityFile = new File(DIR, "identity.ini").getAbsolutePath();
public String identityFile = AppDirs.file("identity.ini").getAbsolutePath();
/** Id of the identity used when a server doesn't select one of its own. */
public String defaultIdentityId = "";
@@ -185,8 +186,8 @@ public final class Settings {
public static Settings load() {
Settings s = new Settings();
try {
if (FILE.isFile()) {
try (FileInputStream in = new FileInputStream(FILE)) {
if (file().isFile()) {
try (FileInputStream in = new FileInputStream(file())) {
s.props.load(in);
}
s.applyFromProps();
@@ -199,12 +200,9 @@ public final class Settings {
public void save() {
try {
if (!DIR.isDirectory()) {
//noinspection ResultOfMethodCallIgnored
DIR.mkdirs();
}
AppDirs.createProfile();
writeToProps();
try (FileOutputStream out = new FileOutputStream(FILE)) {
try (FileOutputStream out = new FileOutputStream(file())) {
props.store(out, "TS3J Swing Client settings");
}
} catch (Exception ignored) {
@@ -225,11 +223,6 @@ public final class Settings {
return soundVolume * masterVolume;
}
/** The directory holding all persistent client state (settings, identities, caches). */
public static File configDir() {
return DIR;
}
private void applyFromProps() {
lastAddress = props.getProperty("lastAddress", lastAddress);
nickname = props.getProperty("nickname", nickname);

View File

@@ -1,5 +1,6 @@
package com.ts3client.contacts;
import com.ts3client.config.AppDirs;
import com.ts3client.teamspeak.TeamSpeakSettingsDb;
import java.io.File;
@@ -35,8 +36,9 @@ public final class ContactStore {
void onContactsChanged();
}
private static final File DIR = new File(System.getProperty("user.home"), ".ts3jclient");
private static final File FILE = new File(DIR, "contacts.txt");
private static File file() {
return AppDirs.file("contacts.txt");
}
private final File file;
private final Map<String, Contact> byUid = new LinkedHashMap<>();
@@ -50,8 +52,9 @@ public final class ContactStore {
/** Loads the list, seeding it from the TeamSpeak 3 client's database the first time round. */
public static ContactStore load() {
ContactStore store = new ContactStore(FILE);
if (FILE.isFile()) {
File file = file();
ContactStore store = new ContactStore(file);
if (file.isFile()) {
store.readFile();
} else {
List<Contact> imported = TeamSpeakSettingsDb.readContacts();
@@ -90,10 +93,7 @@ public final class ContactStore {
sb.append(c.toRecord());
}
try {
if (!DIR.isDirectory()) {
//noinspection ResultOfMethodCallIgnored
DIR.mkdirs();
}
AppDirs.createProfile();
Files.writeString(file.toPath(), sb.toString(), StandardCharsets.UTF_8);
} catch (IOException ignored) {
}

View File

@@ -1,5 +1,7 @@
package com.ts3client.hotkey;
import com.ts3client.config.AppDirs;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
@@ -14,8 +16,9 @@ import java.util.Properties;
*/
public final class Hotkeys {
private static final File DIR = new File(System.getProperty("user.home"), ".ts3jclient");
private static final File FILE = new File(DIR, "hotkeys.properties");
private static File file() {
return AppDirs.file("hotkeys.properties");
}
private final List<Hotkey> entries = Collections.synchronizedList(new ArrayList<>());
@@ -48,9 +51,9 @@ public final class Hotkeys {
public static Hotkeys load() {
Hotkeys h = new Hotkeys();
if (!FILE.isFile()) return h;
if (!file().isFile()) return h;
Properties p = new Properties();
try (FileInputStream in = new FileInputStream(FILE)) {
try (FileInputStream in = new FileInputStream(file())) {
p.load(in);
} catch (Exception e) {
return h;
@@ -87,11 +90,8 @@ public final class Hotkeys {
}
}
try {
if (!DIR.isDirectory()) {
//noinspection ResultOfMethodCallIgnored
DIR.mkdirs();
}
try (FileOutputStream out = new FileOutputStream(FILE)) {
AppDirs.createProfile();
try (FileOutputStream out = new FileOutputStream(file())) {
p.store(out, "TS3J client hotkeys");
}
} catch (Exception ignored) {

View File

@@ -1,6 +1,6 @@
package com.ts3client.net;
import com.ts3client.config.Settings;
import com.ts3client.config.AppDirs;
import java.io.File;
import java.io.IOException;
@@ -43,7 +43,7 @@ public final class IconRepository {
private final Fetcher fetcher;
private final Runnable onIconLoaded;
private final File cacheDir = new File(Settings.configDir(), "icons");
private final File cacheDir = new File(AppDirs.profile(), "icons");
private final Map<Long, byte[]> icons = new ConcurrentHashMap<>();
private final Set<Long> pending = ConcurrentHashMap.newKeySet();
private final Set<Long> failed = ConcurrentHashMap.newKeySet();

View File

@@ -1,6 +1,6 @@
package com.ts3client.net.avatar;
import com.ts3client.config.Settings;
import com.ts3client.config.AppDirs;
import java.io.File;
import java.io.IOException;
@@ -47,7 +47,7 @@ public final class AvatarRepository {
* avatar has arrived, so the view can repaint
*/
public AvatarRepository(Fetcher fetcher, Runnable onAvatarLoaded) {
this(fetcher, onAvatarLoaded, new File(Settings.configDir(), "avatars"));
this(fetcher, onAvatarLoaded, new File(AppDirs.profile(), "avatars"));
}
/** @param cacheDir where downloaded avatars are kept between sessions */

View File

@@ -1,5 +1,6 @@
package com.ts3client.sound;
import com.ts3client.config.AppDirs;
import com.ts3client.config.Settings;
import java.io.File;
@@ -48,7 +49,7 @@ public final class SoundNotifier {
/** Re-scans the installed packs and selects the configured one. */
public void reload() {
available = SoundPacks.findAll(new File(Settings.configDir(), "sound"), settings.soundPackDir);
available = SoundPacks.findAll(new File(AppDirs.profile(), "sound"), settings.soundPackDir);
pack = SoundPacks.select(available, settings.soundPack);
}

View File

@@ -5,6 +5,7 @@ import com.github.weisj.jsvg.SVGDocument;
import com.github.weisj.jsvg.parser.LoaderContext;
import com.github.weisj.jsvg.parser.SVGLoader;
import com.github.weisj.jsvg.view.ViewBox;
import com.ts3client.config.AppDirs;
import com.ts3client.config.Settings;
import com.ts3client.gfx.IconPack;
import com.ts3client.gfx.IconPacks;
@@ -70,7 +71,7 @@ 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(Settings.configDir(), "gfx"), settings.iconPackDir);
packs = IconPacks.findAll(new File(AppDirs.profile(), "gfx"), settings.iconPackDir);
fallback = IconPacks.fallback(packs);
setPack(IconPacks.select(packs, settings.iconPack));
}