Remove changelog and changelog UI, require explicit service selection

- Delete changelog.ts and ChangelogView.tsx - Make service selection
explicit; default to undefined instead of "lastfm" - Update settings and
service logic to handle undefined service - Refactor credential checks
to use only service-specific fields - Improve error handling for missing
or unknown service types
This commit is contained in:
kmmiio99o 2025-10-04 19:57:23 +02:00
parent a5d2b77f40
commit 511d9f8e93
9 changed files with 331 additions and 431 deletions

2
plugins/defs.d.ts vendored
View file

@ -38,7 +38,7 @@ export type LFMSettings = {
listeningTo: boolean;
ignoreSpotify: boolean;
verboseLogging: boolean;
service: ServiceType;
service: ServiceType | undefined;
// Service-specific settings
librefmUsername?: string;
librefmApiKey?: string;

View file

@ -1,61 +0,0 @@
interface ChangelogEntry {
version: string;
date: string;
changes: string[];
}
export const changelog: ChangelogEntry[] = [
{
version: "3.0.0",
date: "2025-01-21",
changes: [
"Complete plugin rewrite with multi-service support",
"Added support for Last.fm, Libre.fm, and ListenBrainz",
"Replaced toast notifications with comprehensive console logging",
"Enhanced timestamp handling with proper start/end times",
"Improved error handling and retry logic",
"Added service validation and connection testing",
"New settings UI with per-service configuration",
"Better credential management for multiple services",
"Enhanced verbose logging for debugging",
"Improved activity updates with service detection",
"Added dynamic app name with track variables",
"Better album art handling and fallbacks",
"Comprehensive status monitoring and reporting",
],
},
{
version: "2.3.0",
date: "2025-09-19",
changes: [
"1. I TRIED SO HARD 2. remove backend error 3. factor settings tab 4. factor V2 5. factor V3 6. factor V4 7. factor V5 8. factor V6 9. Full settings refactor :3",
],
},
{
version: "2.2.0",
date: "2025-09-13",
changes: ["Remember kids! Do not sniff crack!"],
},
{
version: "2.0.0",
date: "2025-09-13",
changes: ["Fix settings scrolling"],
},
{
version: "1.3.0",
date: "2025-09-13",
changes: ["fix reanimated"],
},
{
version: "1.2.0",
date: "2025-09-13",
changes: ["test"],
},
{
version: "1.1.0",
date: "2025-09-12",
changes: ["fixed reanimated issue??"],
},
];
export const currentVersion = "3.0.0";

View file

@ -50,7 +50,7 @@ const Constants = {
listeningTo: true,
ignoreSpotify: true,
verboseLogging: false,
service: "lastfm" as ServiceType,
service: undefined as ServiceType | undefined,
librefmUsername: "",
librefmApiKey: "",
listenbrainzUsername: "",

View file

@ -74,10 +74,16 @@ async function tryInitialize() {
}
async function validateAndInitialize() {
// Check if user has selected a service first
if (!currentSettings.service) {
log("No service selected. Please configure a service in plugin settings.");
return;
}
const serviceName = serviceFactory.getCurrentService().getServiceName();
// Make sure we have credentials for whatever service is selected
const service = currentSettings.service || "lastfm";
const service = currentSettings.service;
let hasCredentials = false;
switch (service) {
@ -86,14 +92,11 @@ async function validateAndInitialize() {
break;
case "librefm":
hasCredentials = !!(
(currentSettings.librefmUsername || currentSettings.username) &&
(currentSettings.librefmApiKey || currentSettings.apiKey)
currentSettings.librefmUsername && currentSettings.librefmApiKey
);
break;
case "listenbrainz":
hasCredentials = !!(
currentSettings.listenbrainzUsername || currentSettings.username
);
hasCredentials = !!currentSettings.listenbrainzUsername;
break;
}
@ -128,11 +131,16 @@ export default {
log("Plugin loading...");
pluginState.pluginStopped = false;
// Show what we're starting with
const serviceName = serviceFactory.getCurrentService().getServiceName();
log(
`Configuration: Service=${serviceName}, Update Interval=${currentSettings.timeInterval}s, Verbose=${currentSettings.verboseLogging}`,
);
// Check if service is configured
if (!currentSettings.service) {
log("No service configured. Please select a service in plugin settings.");
} else {
// Show what we're starting with
const serviceName = serviceFactory.getCurrentService().getServiceName();
log(
`Configuration: Service=${serviceName}, Update Interval=${currentSettings.timeInterval}s, Verbose=${currentSettings.verboseLogging}`,
);
}
// Add to sidebar if user wants it
try {
@ -200,12 +208,16 @@ export default {
// Switch services if needed
if (oldService !== newService && newService) {
log(`Service changed from ${oldService} to ${newService}`);
log(`Service changed from ${oldService || "none"} to ${newService}`);
await switchService(newService);
} else if (!pluginState.pluginStopped) {
// Just restart with the updated settings
} else if (!pluginState.pluginStopped && currentSettings.service) {
// Just restart with the updated settings (only if service is selected)
log("Restarting with updated settings...");
await tryInitialize();
} else if (!currentSettings.service) {
// Service was unselected, stop the plugin
log("Service unselected, stopping plugin...");
stop();
}
},

View file

@ -38,131 +38,137 @@ interface LibreFMTrack {
}
export class LibreFmService extends BaseService {
getServiceName(): string {
return "Libre.fm";
}
getServiceName(): string {
return "Libre.fm";
}
protected logVerbose(...args: any[]): void {
if (currentSettings.verboseLogging) {
console.log(`[${this.getServiceName()}] Verbose:`, ...args);
}
protected logVerbose(...args: any[]): void {
if (currentSettings.verboseLogging) {
console.log(`[${this.getServiceName()}] Verbose:`, ...args);
}
}
async validateCredentials(): Promise<boolean> {
async validateCredentials(): Promise<boolean> {
try {
const username = currentSettings.librefmUsername;
const apiKey = currentSettings.librefmApiKey;
if (!username || !apiKey) {
throw new Error("Username or API key not set for Libre.fm");
}
const params = new URLSearchParams({
method: "user.getinfo",
user: username,
api_key: apiKey,
format: "json",
});
const url = `${Constants.SERVICES.librefm.baseUrl}?${params}`;
await this.makeRequest(url);
this.log("Credentials validation successful");
return true;
} catch (error) {
this.logError("Credentials validation failed:", error);
return false;
}
}
async fetchLatestScrobble(): Promise<Track> {
try {
const username = currentSettings.librefmUsername;
const apiKey = currentSettings.librefmApiKey;
if (!username || !apiKey) {
throw new Error("Username or API key not set for Libre.fm");
}
this.logVerbose("Fetching latest scrobble for user:", username);
const params = new URLSearchParams({
method: "user.getrecenttracks",
user: username,
api_key: apiKey,
limit: "1",
extended: "1",
format: "json",
});
const url = `${Constants.SERVICES.librefm.baseUrl}?${params}`;
const data: LibreFMResponse = await this.makeRequest(url);
const lastTrack = data?.recenttracks?.track?.[0];
if (!lastTrack) {
throw new Error("No tracks found");
}
this.logVerbose("Raw track data:", lastTrack);
const isNowPlaying = Boolean(lastTrack["@attr"]?.nowplaying);
const trackTimestamp = lastTrack.date?.uts
? parseInt(lastTrack.date.uts)
: Math.floor(Date.now() / 1000);
let duration: number | null = null;
let endTime: number | null = null;
// Fetch track duration if it's currently playing
if (isNowPlaying) {
try {
const username = currentSettings.librefmUsername || currentSettings.username;
const apiKey = currentSettings.librefmApiKey || currentSettings.apiKey;
const trackInfoParams = new URLSearchParams({
method: "track.getInfo",
track: lastTrack.name,
artist: lastTrack.artist.name,
api_key: apiKey,
format: "json",
});
if (!username || !apiKey) {
throw new Error("Username or API key not set for Libre.fm");
const trackInfoUrl = `${Constants.SERVICES.librefm.baseUrl}?${trackInfoParams}`;
const trackInfo: LibreFMResponse =
await this.makeRequest(trackInfoUrl);
if (trackInfo?.track?.duration) {
duration = parseInt(trackInfo.track.duration);
if (duration > 0) {
// Duration is in milliseconds, convert to seconds
duration = Math.floor(duration / 1000);
endTime = trackTimestamp + duration;
}
const params = new URLSearchParams({
method: "user.getinfo",
user: username,
api_key: apiKey,
format: "json",
});
const url = `${Constants.SERVICES.librefm.baseUrl}?${params}`;
await this.makeRequest(url);
this.log("Credentials validation successful");
return true;
}
} catch (error) {
this.logError("Credentials validation failed:", error);
return false;
}
}
async fetchLatestScrobble(): Promise<Track> {
try {
const username = currentSettings.librefmUsername || currentSettings.username;
const apiKey = currentSettings.librefmApiKey || currentSettings.apiKey;
if (!username || !apiKey) {
throw new Error("Username or API key not set for Libre.fm");
}
this.logVerbose("Fetching latest scrobble for user:", username);
const params = new URLSearchParams({
method: "user.getrecenttracks",
user: username,
api_key: apiKey,
limit: "1",
extended: "1",
format: "json",
});
const url = `${Constants.SERVICES.librefm.baseUrl}?${params}`;
const data: LibreFMResponse = await this.makeRequest(url);
const lastTrack = data?.recenttracks?.track?.[0];
if (!lastTrack) {
throw new Error("No tracks found");
}
this.logVerbose("Raw track data:", lastTrack);
const isNowPlaying = Boolean(lastTrack["@attr"]?.nowplaying);
const trackTimestamp = lastTrack.date?.uts ? parseInt(lastTrack.date.uts) : Math.floor(Date.now() / 1000);
let duration: number | null = null;
let endTime: number | null = null;
// Fetch track duration if it's currently playing
if (isNowPlaying) {
try {
const trackInfoParams = new URLSearchParams({
method: "track.getInfo",
track: lastTrack.name,
artist: lastTrack.artist.name,
api_key: apiKey,
format: "json",
});
const trackInfoUrl = `${Constants.SERVICES.librefm.baseUrl}?${trackInfoParams}`;
const trackInfo: LibreFMResponse = await this.makeRequest(trackInfoUrl);
if (trackInfo?.track?.duration) {
duration = parseInt(trackInfo.track.duration);
if (duration > 0) {
// Duration is in milliseconds, convert to seconds
duration = Math.floor(duration / 1000);
endTime = trackTimestamp + duration;
}
}
} catch (error) {
this.logVerbose("Failed to fetch track duration:", error);
}
}
const albumArt = this.processAlbumArt(
lastTrack.image?.find(img => img.size === "large")?.["#text"]
);
const track: Track = {
name: lastTrack.name,
artist: lastTrack.artist.name,
album: lastTrack.album["#text"],
albumArt,
url: lastTrack.url,
date: lastTrack.date?.["#text"] ?? "now",
nowPlaying: isNowPlaying,
loved: lastTrack.loved === "1",
from: trackTimestamp,
to: endTime,
duration,
};
this.logVerbose("Processed track:", track);
this.log(`${isNowPlaying ? "Now playing" : "Last played"}:`, `${track.artist} - ${track.name}`);
return track;
} catch (error) {
this.logError("Failed to fetch latest scrobble:", error);
throw error;
this.logVerbose("Failed to fetch track duration:", error);
}
}
const albumArt = this.processAlbumArt(
lastTrack.image?.find((img) => img.size === "large")?.["#text"],
);
const track: Track = {
name: lastTrack.name,
artist: lastTrack.artist.name,
album: lastTrack.album["#text"],
albumArt,
url: lastTrack.url,
date: lastTrack.date?.["#text"] ?? "now",
nowPlaying: isNowPlaying,
loved: lastTrack.loved === "1",
from: trackTimestamp,
to: endTime,
duration,
};
this.logVerbose("Processed track:", track);
this.log(
`${isNowPlaying ? "Now playing" : "Last played"}:`,
`${track.artist} - ${track.name}`,
);
return track;
} catch (error) {
this.logError("Failed to fetch latest scrobble:", error);
throw error;
}
}
}

View file

@ -37,156 +37,171 @@ interface ListenBrainzPlayingNowResponse {
}
export class ListenBrainzService extends BaseService {
getServiceName(): string {
return "ListenBrainz";
}
getServiceName(): string {
return "ListenBrainz";
}
protected logVerbose(...args: any[]): void {
if (currentSettings.verboseLogging) {
console.log(`[${this.getServiceName()}] Verbose:`, ...args);
protected logVerbose(...args: any[]): void {
if (currentSettings.verboseLogging) {
console.log(`[${this.getServiceName()}] Verbose:`, ...args);
}
}
async validateCredentials(): Promise<boolean> {
try {
const username = currentSettings.listenbrainzUsername;
const token = currentSettings.listenbrainzToken;
if (!username) {
throw new Error("Username not set for ListenBrainz");
}
// For ListenBrainz, we don't need a token to read public data
// But we can test the connection by fetching user listens
const url = `${Constants.SERVICES.listenbrainz.baseUrl}/user/${encodeURIComponent(username)}/listens?count=1`;
const headers: Record<string, string> = {};
if (token) {
headers["Authorization"] = `Token ${token}`;
}
await this.makeRequest(url, { headers });
this.log("Credentials validation successful");
return true;
} catch (error) {
this.logError("Credentials validation failed:", error);
return false;
}
}
async fetchLatestScrobble(): Promise<Track> {
try {
const username = currentSettings.listenbrainzUsername;
const token = currentSettings.listenbrainzToken;
if (!username) {
throw new Error("Username not set for ListenBrainz");
}
this.logVerbose("Fetching latest scrobble for user:", username);
// First check for currently playing track
let currentlyPlaying: ListenBrainzListen | null = null;
try {
const playingNowUrl = `${Constants.SERVICES.listenbrainz.baseUrl}/user/${encodeURIComponent(username)}/playing-now`;
const headers: Record<string, string> = {};
if (token) {
headers["Authorization"] = `Token ${token}`;
}
}
async validateCredentials(): Promise<boolean> {
const playingNowData: ListenBrainzPlayingNowResponse =
await this.makeRequest(playingNowUrl, { headers });
if (playingNowData.listens && playingNowData.listens.length > 0) {
currentlyPlaying = playingNowData.listens[0];
currentlyPlaying.playing_now = true;
}
} catch (error) {
this.logVerbose(
"No currently playing track or failed to fetch:",
error,
);
}
let latestListen: ListenBrainzListen;
if (currentlyPlaying) {
latestListen = currentlyPlaying;
this.logVerbose("Using currently playing track");
} else {
// Fetch recent listens
const url = `${Constants.SERVICES.listenbrainz.baseUrl}/user/${encodeURIComponent(username)}/listens?count=1`;
const headers: Record<string, string> = {};
if (token) {
headers["Authorization"] = `Token ${token}`;
}
const data: ListenBrainzResponse = await this.makeRequest(url, {
headers,
});
if (!data.listens || data.listens.length === 0) {
throw new Error("No listens found");
}
latestListen = data.listens[0];
this.logVerbose("Using latest completed listen");
}
this.logVerbose("Raw listen data:", latestListen);
const isNowPlaying = Boolean(latestListen.playing_now);
const trackTimestamp =
latestListen.listened_at || Math.floor(Date.now() / 1000);
let duration: number | null = null;
let endTime: number | null = null;
// Extract duration from additional_info if available
if (latestListen.track_metadata.additional_info?.duration_ms) {
duration = Math.floor(
latestListen.track_metadata.additional_info.duration_ms / 1000,
);
if (isNowPlaying && duration > 0) {
endTime = trackTimestamp + duration;
}
}
// Try to get album art from MusicBrainz or other sources
const albumArt: string | null = null;
if (latestListen.track_metadata.additional_info?.release_mbid) {
try {
const username = currentSettings.listenbrainzUsername || currentSettings.username;
const token = currentSettings.listenbrainzToken;
if (!username) {
throw new Error("Username not set for ListenBrainz");
}
// For ListenBrainz, we don't need a token to read public data
// But we can test the connection by fetching user listens
const url = `${Constants.SERVICES.listenbrainz.baseUrl}/user/${encodeURIComponent(username)}/listens?count=1`;
const headers: Record<string, string> = {};
if (token) {
headers["Authorization"] = `Token ${token}`;
}
await this.makeRequest(url, { headers });
this.log("Credentials validation successful");
return true;
// We could fetch cover art from Cover Art Archive, but for now we'll leave it null
// albumArt = `https://coverartarchive.org/release/${latestListen.track_metadata.additional_info.release_mbid}/front`;
} catch (error) {
this.logError("Credentials validation failed:", error);
return false;
this.logVerbose("Failed to fetch album art:", error);
}
}
const track: Track = {
name: latestListen.track_metadata.track_name,
artist: latestListen.track_metadata.artist_name,
album: latestListen.track_metadata.release_name || "",
albumArt,
url:
latestListen.track_metadata.additional_info?.origin_url ||
`https://listenbrainz.org/user/${username}`,
date: isNowPlaying
? "now"
: new Date(trackTimestamp * 1000).toISOString(),
nowPlaying: isNowPlaying,
loved: false, // ListenBrainz doesn't have a direct "loved" concept
from: trackTimestamp,
to: endTime,
duration,
};
this.logVerbose("Processed track:", track);
this.log(
`${isNowPlaying ? "Now playing" : "Last played"}:`,
`${track.artist} - ${track.name}`,
);
return track;
} catch (error) {
this.logError("Failed to fetch latest listen:", error);
throw error;
}
}
async fetchLatestScrobble(): Promise<Track> {
try {
const username = currentSettings.listenbrainzUsername || currentSettings.username;
const token = currentSettings.listenbrainzToken;
if (!username) {
throw new Error("Username not set for ListenBrainz");
}
this.logVerbose("Fetching latest scrobble for user:", username);
// First check for currently playing track
let currentlyPlaying: ListenBrainzListen | null = null;
try {
const playingNowUrl = `${Constants.SERVICES.listenbrainz.baseUrl}/user/${encodeURIComponent(username)}/playing-now`;
const headers: Record<string, string> = {};
if (token) {
headers["Authorization"] = `Token ${token}`;
}
const playingNowData: ListenBrainzPlayingNowResponse = await this.makeRequest(playingNowUrl, { headers });
if (playingNowData.listens && playingNowData.listens.length > 0) {
currentlyPlaying = playingNowData.listens[0];
currentlyPlaying.playing_now = true;
}
} catch (error) {
this.logVerbose("No currently playing track or failed to fetch:", error);
}
let latestListen: ListenBrainzListen;
if (currentlyPlaying) {
latestListen = currentlyPlaying;
this.logVerbose("Using currently playing track");
} else {
// Fetch recent listens
const url = `${Constants.SERVICES.listenbrainz.baseUrl}/user/${encodeURIComponent(username)}/listens?count=1`;
const headers: Record<string, string> = {};
if (token) {
headers["Authorization"] = `Token ${token}`;
}
const data: ListenBrainzResponse = await this.makeRequest(url, { headers });
if (!data.listens || data.listens.length === 0) {
throw new Error("No listens found");
}
latestListen = data.listens[0];
this.logVerbose("Using latest completed listen");
}
this.logVerbose("Raw listen data:", latestListen);
const isNowPlaying = Boolean(latestListen.playing_now);
const trackTimestamp = latestListen.listened_at || Math.floor(Date.now() / 1000);
let duration: number | null = null;
let endTime: number | null = null;
// Extract duration from additional_info if available
if (latestListen.track_metadata.additional_info?.duration_ms) {
duration = Math.floor(latestListen.track_metadata.additional_info.duration_ms / 1000);
if (isNowPlaying && duration > 0) {
endTime = trackTimestamp + duration;
}
}
// Try to get album art from MusicBrainz or other sources
const albumArt: string | null = null;
if (latestListen.track_metadata.additional_info?.release_mbid) {
try {
// We could fetch cover art from Cover Art Archive, but for now we'll leave it null
// albumArt = `https://coverartarchive.org/release/${latestListen.track_metadata.additional_info.release_mbid}/front`;
} catch (error) {
this.logVerbose("Failed to fetch album art:", error);
}
}
const track: Track = {
name: latestListen.track_metadata.track_name,
artist: latestListen.track_metadata.artist_name,
album: latestListen.track_metadata.release_name || "",
albumArt,
url: latestListen.track_metadata.additional_info?.origin_url ||
`https://listenbrainz.org/user/${username}`,
date: isNowPlaying ? "now" : new Date(trackTimestamp * 1000).toISOString(),
nowPlaying: isNowPlaying,
loved: false, // ListenBrainz doesn't have a direct "loved" concept
from: trackTimestamp,
to: endTime,
duration,
};
this.logVerbose("Processed track:", track);
this.log(`${isNowPlaying ? "Now playing" : "Last played"}:`, `${track.artist} - ${track.name}`);
return track;
} catch (error) {
this.logError("Failed to fetch latest listen:", error);
throw error;
}
}
protected getErrorMessage(error: any): string {
protected getErrorMessage(error: any): string {
// ListenBrainz uses different error format
if (error.error) {
return error.error;
}
if (error.message) {
return error.message;
}
return error.toString() || "Unknown error";
if (error.error) {
return error.error;
}
if (error.message) {
return error.message;
}
return error.toString() || "Unknown error";
}
}

View file

@ -26,7 +26,13 @@ export class ServiceFactory {
this.serviceInstances = new Map();
}
const type = serviceType || currentSettings.service || "lastfm";
const type = serviceType || currentSettings.service;
if (!type) {
throw new Error(
"[ServiceFactory] No service type specified and no default service configured",
);
}
if (!this.serviceInstances.has(type)) {
this.serviceInstances.set(type, this.createService(type));
@ -48,10 +54,9 @@ export class ServiceFactory {
case "listenbrainz":
return new ListenBrainzService();
default:
console.warn(
`[ServiceFactory] Unknown service type: ${serviceType}, falling back to Last.fm`,
throw new Error(
`[ServiceFactory] Unknown service type: ${serviceType}`,
);
return new LastFmService();
}
}

View file

@ -1,80 +0,0 @@
import { clipboard } from "@vendetta/metro/common";
import { getAssetIDByName } from "@vendetta/ui/assets";
import { Forms, General } from "@vendetta/ui/components";
import { showToast } from "@vendetta/ui/toasts";
import { changelog, currentVersion } from "../changelog";
import { React } from "@vendetta/metro/common";
import { findByProps } from "@vendetta/metro";
const { TableRowGroup, TableRow, Stack } = findByProps(
"TableSwitchRow",
"TableRowGroup",
"Stack",
"TableRow",
);
const { ScrollView } = General;
export default function ChangelogView() {
return (
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 10 }}>
<Stack spacing={8}>
<TableRowGroup title="CURRENT VERSION">
<TableRow
label={currentVersion}
trailing={
<General.TouchableOpacity
onPress={() => {
clipboard.setString(currentVersion);
showToast(
"Version copied to clipboard",
getAssetIDByName("toast_copy_link"),
);
}}
>
<General.Text style={{ color: "#72767d", fontSize: 16 }}>
Copy
</General.Text>
</General.TouchableOpacity>
}
/>
</TableRowGroup>
<TableRowGroup title="CHANGELOG">
{changelog.map((entry, index) => (
<Stack
key={index}
spacing={4}
style={{
padding: 12,
borderBottomWidth: index === changelog.length - 1 ? 0 : 0.5,
borderBottomColor: "#292929",
}}
>
<General.View
style={{
flexDirection: "row",
justifyContent: "space-between",
}}
>
<General.Text style={{ color: "#fff", fontWeight: "bold" }}>
Version {entry.version}
</General.Text>
<General.Text style={{ color: "#72767d" }}>
{entry.date}
</General.Text>
</General.View>
{entry.changes.map((change, changeIndex) => (
<General.Text
key={changeIndex}
style={{ color: "#dcddde", marginLeft: 8, marginTop: 2 }}
>
• {change}
</General.Text>
))}
</Stack>
))}
</TableRowGroup>
</Stack>
</ScrollView>
);
}

View file

@ -35,7 +35,7 @@ plugin.storage.listenbrainzUsername ??= "";
plugin.storage.listenbrainzToken ??= "";
plugin.storage.addToSidebar ??= true;
const get = (k: string, fallback: any = "") => plugin.storage[k] ?? fallback;
const get = (k: string, fallback?: any) => plugin.storage[k] ?? fallback;
const set = (k: string, v: any) => (plugin.storage[k] = v);
// Last.fm Settings Page
@ -145,7 +145,7 @@ function LibreFmSettingsPage() {
<TableRowGroup title="Credentials">
<Stack spacing={4}>
<TextInput
placeholder="Libre.fm Username (optional, uses Last.fm if empty)"
placeholder="Libre.fm Username"
value={get("librefmUsername")}
onChange={(v: string) => {
set("librefmUsername", v);
@ -154,7 +154,7 @@ function LibreFmSettingsPage() {
isClearable
/>
<TextInput
placeholder="Libre.fm API Key (optional, uses Last.fm if empty)"
placeholder="Libre.fm API Key"
value={get("librefmApiKey")}
onChange={(v: string) => {
set("librefmApiKey", v);
@ -422,7 +422,7 @@ export default function Settings() {
const navigation = NavigationNative.useNavigation();
const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
const currentService = get("service", "lastfm") as ServiceType;
const currentService = get("service") as ServiceType;
const getCredentialStatus = (service: ServiceType) => {
switch (service) {
@ -432,15 +432,14 @@ export default function Settings() {
: "❌ Missing credentials";
}
case "librefm": {
const librefmUser = get("librefmUsername") || get("username");
const librefmKey = get("librefmApiKey") || get("apiKey");
return librefmUser && librefmKey
return get("librefmUsername") && get("librefmApiKey")
? "✅ Configured"
: "❌ Missing credentials";
}
case "listenbrainz": {
const lbUser = get("listenbrainzUsername") || get("username");
return lbUser ? "✅ Configured" : "❌ Missing username";
return get("listenbrainzUsername")
? "✅ Configured"
: "❌ Missing username";
}
default:
return "❓ Unknown";
@ -454,7 +453,11 @@ export default function Settings() {
<TableRowGroup title="Active Service">
<TableRow
label="Current Service"
subLabel={`Using: ${serviceFactory.getServiceDisplayName(currentService)}`}
subLabel={
currentService
? `Using: ${serviceFactory.getServiceDisplayName(currentService)}`
: "No service selected"
}
/>
{(["lastfm", "librefm", "listenbrainz"] as ServiceType[]).map(
(service) => (