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; package com.ts3client.chatlog;
import com.ts3client.config.AppDirs;
import com.ts3client.text.BBCode; import com.ts3client.text.BBCode;
import com.ts3client.text.HtmlText; import com.ts3client.text.HtmlText;
import com.ts3client.text.TextQuery; import com.ts3client.text.TextQuery;
@@ -121,7 +122,7 @@ public final class ChatLogStore {
} }
public static Path defaultRoot() { public static Path defaultRoot() {
return Path.of(System.getProperty("user.home", "."), ".ts3client", "chats"); return AppDirs.chatLogs();
} }
public static String decodePathComponent(String value) { 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 { public final class AwayMessages {
private static final File DIR = new File(System.getProperty("user.home"), ".ts3jclient"); private static File file() {
private static final File FILE = new File(DIR, "away.properties"); return AppDirs.file("away.properties");
}
private final List<String> entries = new ArrayList<>(); private final List<String> entries = new ArrayList<>();
@@ -36,9 +37,9 @@ public final class AwayMessages {
public static AwayMessages load() { public static AwayMessages load() {
AwayMessages m = new AwayMessages(); AwayMessages m = new AwayMessages();
if (!FILE.isFile()) return m.withDefaults(); if (!file().isFile()) return m.withDefaults();
Properties p = new Properties(); Properties p = new Properties();
try (FileInputStream in = new FileInputStream(FILE)) { try (FileInputStream in = new FileInputStream(file())) {
p.load(in); p.load(in);
} catch (Exception e) { } catch (Exception e) {
return m.withDefaults(); return m.withDefaults();
@@ -58,11 +59,8 @@ public final class AwayMessages {
p.setProperty("message." + i, entries.get(i)); p.setProperty("message." + i, entries.get(i));
} }
try { try {
if (!DIR.isDirectory()) { AppDirs.createProfile();
//noinspection ResultOfMethodCallIgnored try (FileOutputStream out = new FileOutputStream(file())) {
DIR.mkdirs();
}
try (FileOutputStream out = new FileOutputStream(FILE)) {
p.store(out, "TS3J client away messages"); p.store(out, "TS3J client away messages");
} }
} catch (Exception ignored) { } catch (Exception ignored) {

View File

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

View File

@@ -13,8 +13,9 @@ import java.util.Properties;
*/ */
public final class Bookmarks { public final class Bookmarks {
private static final File DIR = new File(System.getProperty("user.home"), ".ts3jclient"); private static File file() {
private static final File FILE = new File(DIR, "bookmarks.properties"); return AppDirs.file("bookmarks.properties");
}
private final List<Bookmark> entries = new ArrayList<>(); private final List<Bookmark> entries = new ArrayList<>();
@@ -41,9 +42,9 @@ public final class Bookmarks {
public static Bookmarks load() { public static Bookmarks load() {
Bookmarks b = new Bookmarks(); Bookmarks b = new Bookmarks();
if (!FILE.isFile()) return b; if (!file().isFile()) return b;
Properties p = new Properties(); Properties p = new Properties();
try (FileInputStream in = new FileInputStream(FILE)) { try (FileInputStream in = new FileInputStream(file())) {
p.load(in); p.load(in);
} catch (Exception e) { } catch (Exception e) {
return b; return b;
@@ -83,11 +84,8 @@ public final class Bookmarks {
p.setProperty(prefix + "connectOnStartup", Boolean.toString(bm.connectOnStartup)); p.setProperty(prefix + "connectOnStartup", Boolean.toString(bm.connectOnStartup));
} }
try { try {
if (!DIR.isDirectory()) { AppDirs.createProfile();
//noinspection ResultOfMethodCallIgnored try (FileOutputStream out = new FileOutputStream(file())) {
DIR.mkdirs();
}
try (FileOutputStream out = new FileOutputStream(FILE)) {
p.store(out, "TS3J client bookmarks"); p.store(out, "TS3J client bookmarks");
} }
} catch (Exception ignored) { } 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. */ /** Security level new identities are generated with, matching the TS3 client's default. */
public static final int DEFAULT_SECURITY_LEVEL = 8; 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<>(); 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. */ /** Loads all identities from disk, migrating a legacy single-identity file if needed. */
public static IdentityStore load(Settings settings) { public static IdentityStore load(Settings settings) {
IdentityStore store = new IdentityStore(); 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) { if (files != null) {
for (File f : files) { for (File f : files) {
IdentityEntry e = read(f); IdentityEntry e = read(f);
@@ -218,7 +216,7 @@ public final class IdentityStore {
private IdentityEntry create(String name, LocalIdentity identity) throws IOException { private IdentityEntry create(String name, LocalIdentity identity) throws IOException {
String id = uniqueId(name); String id = uniqueId(name);
File file = new File(IDENTITY_DIR, id + ".ini"); File file = new File(directory(), id + ".ini");
write(identity, name, file); write(identity, name, file);
IdentityEntry entry = new IdentityEntry(id, name, file, identity); IdentityEntry entry = new IdentityEntry(id, name, file, identity);
entries.add(entry); entries.add(entry);
@@ -283,7 +281,7 @@ public final class IdentityStore {
private boolean taken(String id) { private boolean taken(String id) {
if (byId(id) != null) return true; if (byId(id) != null) return true;
return new File(IDENTITY_DIR, id + ".ini").exists(); return new File(directory(), id + ".ini").exists();
} }
private void sort() { private void sort() {
@@ -331,6 +329,6 @@ public final class IdentityStore {
/** Directory the managed identity files live in. */ /** Directory the managed identity files live in. */
public static File directory() { 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 File file() {
private static final File FILE = new File(DIR, "settings.properties"); return AppDirs.file("settings.properties");
}
private final Properties props = new 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 nickname = System.getProperty("user.name", "TS3J User");
public String serverPassword = ""; public String serverPassword = "";
/** Legacy single-identity file; only used to migrate into the identity store. */ /** 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. */ /** Id of the identity used when a server doesn't select one of its own. */
public String defaultIdentityId = ""; public String defaultIdentityId = "";
@@ -185,8 +186,8 @@ public final class Settings {
public static Settings load() { public static Settings load() {
Settings s = new Settings(); Settings s = new Settings();
try { try {
if (FILE.isFile()) { if (file().isFile()) {
try (FileInputStream in = new FileInputStream(FILE)) { try (FileInputStream in = new FileInputStream(file())) {
s.props.load(in); s.props.load(in);
} }
s.applyFromProps(); s.applyFromProps();
@@ -199,12 +200,9 @@ public final class Settings {
public void save() { public void save() {
try { try {
if (!DIR.isDirectory()) { AppDirs.createProfile();
//noinspection ResultOfMethodCallIgnored
DIR.mkdirs();
}
writeToProps(); writeToProps();
try (FileOutputStream out = new FileOutputStream(FILE)) { try (FileOutputStream out = new FileOutputStream(file())) {
props.store(out, "TS3J Swing Client settings"); props.store(out, "TS3J Swing Client settings");
} }
} catch (Exception ignored) { } catch (Exception ignored) {
@@ -225,11 +223,6 @@ public final class Settings {
return soundVolume * masterVolume; return soundVolume * masterVolume;
} }
/** The directory holding all persistent client state (settings, identities, caches). */
public static File configDir() {
return DIR;
}
private void applyFromProps() { private void applyFromProps() {
lastAddress = props.getProperty("lastAddress", lastAddress); lastAddress = props.getProperty("lastAddress", lastAddress);
nickname = props.getProperty("nickname", nickname); nickname = props.getProperty("nickname", nickname);

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
package com.ts3client.sound; package com.ts3client.sound;
import com.ts3client.config.AppDirs;
import com.ts3client.config.Settings; import com.ts3client.config.Settings;
import java.io.File; import java.io.File;
@@ -48,7 +49,7 @@ public final class SoundNotifier {
/** Re-scans the installed packs and selects the configured one. */ /** Re-scans the installed packs and selects the configured one. */
public void reload() { 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); 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.LoaderContext;
import com.github.weisj.jsvg.parser.SVGLoader; import com.github.weisj.jsvg.parser.SVGLoader;
import com.github.weisj.jsvg.view.ViewBox; import com.github.weisj.jsvg.view.ViewBox;
import com.ts3client.config.AppDirs;
import com.ts3client.config.Settings; import com.ts3client.config.Settings;
import com.ts3client.gfx.IconPack; import com.ts3client.gfx.IconPack;
import com.ts3client.gfx.IconPacks; 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. */ /** Re-scans the pack folders and activates the one the settings name. */
public void reload(Settings settings) { 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); fallback = IconPacks.fallback(packs);
setPack(IconPacks.select(packs, settings.iconPack)); setPack(IconPacks.select(packs, settings.iconPack));
} }