565 lines
23 KiB
TypeScript
565 lines
23 KiB
TypeScript
import { plugin } from "@vendetta";
|
|
import { useProxy } from "@vendetta/storage";
|
|
import { React, ReactNative as RN } from "@vendetta/metro/common";
|
|
import { Linking } from "react-native";
|
|
import { NavigationNative } from "@vendetta/metro/common";
|
|
import { findByProps } from "@vendetta/metro";
|
|
import { showToast } from "@vendetta/ui/toasts";
|
|
import { getAssetIDByName } from "@vendetta/ui/assets";
|
|
|
|
const { ScrollView } = findByProps("ScrollView");
|
|
const { TableRowGroup, TableSwitchRow, Stack, TableRow } = findByProps(
|
|
"TableSwitchRow",
|
|
"TableRowGroup",
|
|
"Stack",
|
|
"TableRow",
|
|
);
|
|
const { TextInput } = findByProps("TextInput");
|
|
|
|
import Constants from "../../constants";
|
|
import { serviceFactory } from "../../services/ServiceFactory";
|
|
import { ServiceType } from "../../../../defs";
|
|
|
|
plugin.storage.username ??= "";
|
|
plugin.storage.apiKey ??= "";
|
|
plugin.storage.appName ??= "Music";
|
|
plugin.storage.timeInterval ??= 5;
|
|
plugin.storage.showTimestamp ??= true;
|
|
plugin.storage.listeningTo ??= true;
|
|
plugin.storage.ignoreSpotify ??= true;
|
|
plugin.storage.verboseLogging ??= false;
|
|
plugin.storage.service ??= "lastfm";
|
|
plugin.storage.librefmUsername ??= "";
|
|
plugin.storage.librefmApiKey ??= "";
|
|
plugin.storage.listenbrainzUsername ??= "";
|
|
plugin.storage.listenbrainzToken ??= "";
|
|
plugin.storage.addToSidebar ??= true;
|
|
|
|
const get = (k: string, fallback?: any) => plugin.storage[k] ?? fallback;
|
|
const set = (k: string, v: any) => (plugin.storage[k] = v);
|
|
|
|
// Last.fm Settings Page
|
|
function LastFmSettingsPage() {
|
|
useProxy(plugin.storage);
|
|
const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
|
|
|
|
const testConnection = async () => {
|
|
showToast("Testing Last.fm connection...", getAssetIDByName("ClockIcon"));
|
|
try {
|
|
const isValid = await serviceFactory.testService("lastfm");
|
|
if (isValid) {
|
|
showToast(
|
|
"✅ Last.fm connection successful!",
|
|
getAssetIDByName("CheckIcon"),
|
|
);
|
|
} else {
|
|
showToast("❌ Last.fm connection failed", getAssetIDByName("XIcon"));
|
|
}
|
|
} catch (error) {
|
|
showToast("❌ Connection error", getAssetIDByName("XIcon"));
|
|
}
|
|
};
|
|
|
|
return (
|
|
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 10 }}>
|
|
<Stack spacing={8}>
|
|
<TableRowGroup title="Credentials">
|
|
<Stack spacing={4}>
|
|
<TextInput
|
|
placeholder="Last.fm Username"
|
|
value={get("username")}
|
|
onChange={(v: string) => {
|
|
set("username", v);
|
|
forceUpdate();
|
|
}}
|
|
isClearable
|
|
/>
|
|
<TextInput
|
|
placeholder="Last.fm API Key"
|
|
value={get("apiKey")}
|
|
onChange={(v: string) => {
|
|
set("apiKey", v);
|
|
forceUpdate();
|
|
}}
|
|
secureTextEntry={true}
|
|
isClearable
|
|
/>
|
|
</Stack>
|
|
</TableRowGroup>
|
|
|
|
<TableRowGroup title="Actions">
|
|
<TableRow
|
|
label="Test Connection"
|
|
subLabel="Verify your Last.fm credentials"
|
|
trailing={<TableRow.Arrow />}
|
|
onPress={testConnection}
|
|
/>
|
|
<TableRow
|
|
label="Get API Key"
|
|
subLabel="Create a Last.fm API key at last.fm/api/account/create"
|
|
trailing={<TableRow.Arrow />}
|
|
onPress={async () => {
|
|
try {
|
|
await Linking.openURL("https://www.last.fm/api/account/create");
|
|
} catch (error) {
|
|
console.error("Failed to open Last.fm API URL:", error);
|
|
showToast(
|
|
"Failed to open web browser. Please visit: https://www.last.fm/api/account/create",
|
|
getAssetIDByName("XIcon"),
|
|
);
|
|
}
|
|
}}
|
|
/>
|
|
</TableRowGroup>
|
|
</Stack>
|
|
<RN.View style={{ height: 64 }} />
|
|
</ScrollView>
|
|
);
|
|
}
|
|
|
|
// Libre.fm Settings Page
|
|
function LibreFmSettingsPage() {
|
|
useProxy(plugin.storage);
|
|
const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
|
|
|
|
const testConnection = async () => {
|
|
showToast("Testing Libre.fm connection...", getAssetIDByName("ClockIcon"));
|
|
try {
|
|
const isValid = await serviceFactory.testService("librefm");
|
|
if (isValid) {
|
|
showToast(
|
|
"✅ Libre.fm connection successful!",
|
|
getAssetIDByName("CheckIcon"),
|
|
);
|
|
} else {
|
|
showToast("❌ Libre.fm connection failed", getAssetIDByName("XIcon"));
|
|
}
|
|
} catch (error) {
|
|
showToast("❌ Connection error", getAssetIDByName("XIcon"));
|
|
}
|
|
};
|
|
|
|
return (
|
|
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 10 }}>
|
|
<Stack spacing={8}>
|
|
<TableRowGroup title="Credentials">
|
|
<Stack spacing={4}>
|
|
<TextInput
|
|
placeholder="Libre.fm Username"
|
|
value={get("librefmUsername")}
|
|
onChange={(v: string) => {
|
|
set("librefmUsername", v);
|
|
forceUpdate();
|
|
}}
|
|
isClearable
|
|
/>
|
|
<TextInput
|
|
placeholder="Libre.fm API Key"
|
|
value={get("librefmApiKey")}
|
|
onChange={(v: string) => {
|
|
set("librefmApiKey", v);
|
|
forceUpdate();
|
|
}}
|
|
secureTextEntry={true}
|
|
isClearable
|
|
/>
|
|
</Stack>
|
|
</TableRowGroup>
|
|
|
|
<TableRowGroup title="Actions">
|
|
<TableRow
|
|
label="Test Connection"
|
|
subLabel="Verify your Libre.fm credentials"
|
|
trailing={<TableRow.Arrow />}
|
|
onPress={testConnection}
|
|
/>
|
|
<TableRow
|
|
label="Get API Key"
|
|
subLabel="Create a Last.fm API key (compatible with Libre.fm)"
|
|
trailing={<TableRow.Arrow />}
|
|
onPress={async () => {
|
|
try {
|
|
await Linking.openURL("https://www.last.fm/api/account/create");
|
|
} catch (error) {
|
|
console.error("Failed to open Last.fm API URL:", error);
|
|
showToast(
|
|
"Failed to open web browser. Please visit: https://www.last.fm/api/account/create",
|
|
getAssetIDByName("XIcon"),
|
|
);
|
|
}
|
|
}}
|
|
/>
|
|
</TableRowGroup>
|
|
</Stack>
|
|
<RN.View style={{ height: 64 }} />
|
|
</ScrollView>
|
|
);
|
|
}
|
|
|
|
// ListenBrainz Settings Page
|
|
function ListenBrainzSettingsPage() {
|
|
useProxy(plugin.storage);
|
|
const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
|
|
|
|
const testConnection = async () => {
|
|
showToast(
|
|
"Testing ListenBrainz connection...",
|
|
getAssetIDByName("ClockIcon"),
|
|
);
|
|
try {
|
|
const isValid = await serviceFactory.testService("listenbrainz");
|
|
if (isValid) {
|
|
showToast(
|
|
"✅ ListenBrainz connection successful!",
|
|
getAssetIDByName("CheckIcon"),
|
|
);
|
|
} else {
|
|
showToast(
|
|
"❌ ListenBrainz connection failed",
|
|
getAssetIDByName("XIcon"),
|
|
);
|
|
}
|
|
} catch (error) {
|
|
showToast("❌ Connection error", getAssetIDByName("XIcon"));
|
|
}
|
|
};
|
|
|
|
return (
|
|
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 10 }}>
|
|
<Stack spacing={8}>
|
|
<TableRowGroup title="Credentials">
|
|
<Stack spacing={4}>
|
|
<TextInput
|
|
placeholder="ListenBrainz Username (optional, uses Last.fm if empty)"
|
|
value={get("listenbrainzUsername")}
|
|
onChange={(v: string) => {
|
|
set("listenbrainzUsername", v);
|
|
forceUpdate();
|
|
}}
|
|
isClearable
|
|
/>
|
|
<TextInput
|
|
placeholder="ListenBrainz Token (optional, for private profiles)"
|
|
value={get("listenbrainzToken")}
|
|
onChange={(v: string) => {
|
|
set("listenbrainzToken", v);
|
|
forceUpdate();
|
|
}}
|
|
secureTextEntry={true}
|
|
isClearable
|
|
/>
|
|
</Stack>
|
|
</TableRowGroup>
|
|
|
|
<TableRowGroup title="Actions">
|
|
<TableRow
|
|
label="Test Connection"
|
|
subLabel="Verify your ListenBrainz credentials"
|
|
trailing={<TableRow.Arrow />}
|
|
onPress={testConnection}
|
|
/>
|
|
<TableRow
|
|
label="Get User Token"
|
|
subLabel="Get your ListenBrainz user token at listenbrainz.org/settings/"
|
|
trailing={<TableRow.Arrow />}
|
|
onPress={async () => {
|
|
try {
|
|
await Linking.openURL("https://listenbrainz.org/settings/");
|
|
} catch (error) {
|
|
console.error(
|
|
"Failed to open ListenBrainz settings URL:",
|
|
error,
|
|
);
|
|
showToast(
|
|
"Failed to open web browser. Please visit: https://listenbrainz.org/settings/",
|
|
getAssetIDByName("XIcon"),
|
|
);
|
|
}
|
|
}}
|
|
/>
|
|
</TableRowGroup>
|
|
</Stack>
|
|
<RN.View style={{ height: 64 }} />
|
|
</ScrollView>
|
|
);
|
|
}
|
|
|
|
// Display Settings Page
|
|
function DisplaySettingsPage() {
|
|
useProxy(plugin.storage);
|
|
const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
|
|
|
|
return (
|
|
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 10 }}>
|
|
<Stack spacing={8}>
|
|
<TableRowGroup title="Activity Display">
|
|
<Stack spacing={4}>
|
|
<TextInput
|
|
placeholder={`App Name (Default: ${Constants.DEFAULT_SETTINGS.appName})`}
|
|
value={get("appName", Constants.DEFAULT_SETTINGS.appName)}
|
|
onChange={(v: string) => {
|
|
set("appName", v);
|
|
forceUpdate();
|
|
}}
|
|
isClearable
|
|
/>
|
|
<TextInput
|
|
placeholder={`Update Interval (Default: ${Constants.DEFAULT_SETTINGS.timeInterval}s)`}
|
|
value={String(
|
|
get("timeInterval", Constants.DEFAULT_SETTINGS.timeInterval),
|
|
)}
|
|
onChange={(v: string) => {
|
|
const interval = Number(v);
|
|
if (interval >= Constants.MIN_UPDATE_INTERVAL) {
|
|
set("timeInterval", interval);
|
|
forceUpdate();
|
|
}
|
|
}}
|
|
keyboardType="numeric"
|
|
isClearable
|
|
/>
|
|
</Stack>
|
|
</TableRowGroup>
|
|
|
|
<TableRowGroup title="Activity Options">
|
|
<TableSwitchRow
|
|
label="Show as Listening"
|
|
subLabel="Display as 'Listening to' instead of 'Playing'"
|
|
value={get("listeningTo", Constants.DEFAULT_SETTINGS.listeningTo)}
|
|
onValueChange={(value: boolean) => {
|
|
set("listeningTo", value);
|
|
forceUpdate();
|
|
}}
|
|
/>
|
|
<TableSwitchRow
|
|
label="Show Timestamp"
|
|
subLabel="Display track progress and duration"
|
|
value={get(
|
|
"showTimestamp",
|
|
Constants.DEFAULT_SETTINGS.showTimestamp,
|
|
)}
|
|
onValueChange={(value: boolean) => {
|
|
set("showTimestamp", value);
|
|
forceUpdate();
|
|
}}
|
|
/>
|
|
<TableSwitchRow
|
|
label="Ignore Spotify"
|
|
subLabel="Don't show activity when Spotify is playing"
|
|
value={get(
|
|
"ignoreSpotify",
|
|
Constants.DEFAULT_SETTINGS.ignoreSpotify,
|
|
)}
|
|
onValueChange={(value: boolean) => {
|
|
set("ignoreSpotify", value);
|
|
forceUpdate();
|
|
}}
|
|
/>
|
|
</TableRowGroup>
|
|
</Stack>
|
|
<RN.View style={{ height: 64 }} />
|
|
</ScrollView>
|
|
);
|
|
}
|
|
|
|
// Logging Settings Page
|
|
function LoggingSettingsPage() {
|
|
useProxy(plugin.storage);
|
|
const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
|
|
|
|
return (
|
|
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 10 }}>
|
|
<Stack spacing={8}>
|
|
<TableRowGroup title="Logging Options">
|
|
<TableSwitchRow
|
|
label="Verbose Logging"
|
|
subLabel="Enable detailed console logging for debugging"
|
|
value={get(
|
|
"verboseLogging",
|
|
Constants.DEFAULT_SETTINGS.verboseLogging,
|
|
)}
|
|
onValueChange={(value: boolean) => {
|
|
set("verboseLogging", value);
|
|
forceUpdate();
|
|
}}
|
|
/>
|
|
</TableRowGroup>
|
|
|
|
<TableRowGroup title="Debug Information">
|
|
<TableRow
|
|
label="Console Logging"
|
|
subLabel="Logs are written to the browser/app console when verbose is enabled"
|
|
/>
|
|
<TableRow
|
|
label="Error Tracking"
|
|
subLabel="Connection errors and API failures are automatically logged"
|
|
/>
|
|
</TableRowGroup>
|
|
|
|
<TableRowGroup title="Log Information">
|
|
<TableRow
|
|
label="API Calls"
|
|
subLabel="All API requests are logged when verbose is enabled"
|
|
/>
|
|
<TableRow
|
|
label="Track Updates"
|
|
subLabel="Song changes and RPC updates are logged"
|
|
/>
|
|
<TableRow
|
|
label="Error Details"
|
|
subLabel="Connection errors and retries are logged"
|
|
/>
|
|
</TableRowGroup>
|
|
</Stack>
|
|
<RN.View style={{ height: 64 }} />
|
|
</ScrollView>
|
|
);
|
|
}
|
|
|
|
// Main Settings Page
|
|
export default function Settings() {
|
|
useProxy(plugin.storage);
|
|
const navigation = NavigationNative.useNavigation();
|
|
const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
|
|
|
|
const currentService = get("service") as ServiceType;
|
|
|
|
const getCredentialStatus = (service: ServiceType) => {
|
|
switch (service) {
|
|
case "lastfm": {
|
|
return get("username") && get("apiKey")
|
|
? "✅ Configured"
|
|
: "❌ Missing credentials";
|
|
}
|
|
case "librefm": {
|
|
return get("librefmUsername") && get("librefmApiKey")
|
|
? "✅ Configured"
|
|
: "❌ Missing credentials";
|
|
}
|
|
case "listenbrainz": {
|
|
return get("listenbrainzUsername")
|
|
? "✅ Configured"
|
|
: "❌ Missing username";
|
|
}
|
|
default:
|
|
return "❓ Unknown";
|
|
}
|
|
};
|
|
|
|
return (
|
|
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 10 }}>
|
|
<Stack spacing={8}>
|
|
{/* Service Selection */}
|
|
<TableRowGroup title="Active Service">
|
|
<TableRow
|
|
label="Current Service"
|
|
subLabel={
|
|
currentService
|
|
? `Using: ${serviceFactory.getServiceDisplayName(currentService)}`
|
|
: "No service selected"
|
|
}
|
|
/>
|
|
{(["lastfm", "librefm", "listenbrainz"] as ServiceType[]).map(
|
|
(service) => (
|
|
<TableSwitchRow
|
|
key={service}
|
|
label={serviceFactory.getServiceDisplayName(service)}
|
|
subLabel={getCredentialStatus(service)}
|
|
value={currentService === service}
|
|
onValueChange={(value: boolean) => {
|
|
if (value && service !== currentService) {
|
|
set("service", service);
|
|
forceUpdate();
|
|
}
|
|
}}
|
|
/>
|
|
),
|
|
)}
|
|
</TableRowGroup>
|
|
|
|
{/* Service Configuration */}
|
|
<TableRowGroup title="Service Configuration">
|
|
<TableRow
|
|
label="Last.fm Settings"
|
|
subLabel="Configure Last.fm credentials and options"
|
|
trailing={<TableRow.Arrow />}
|
|
onPress={() =>
|
|
navigation.push("VendettaCustomPage", {
|
|
title: "Last.fm Settings",
|
|
render: LastFmSettingsPage,
|
|
})
|
|
}
|
|
/>
|
|
<TableRow
|
|
label="Libre.fm Settings"
|
|
subLabel="Configure Libre.fm credentials and options"
|
|
trailing={<TableRow.Arrow />}
|
|
onPress={() =>
|
|
navigation.push("VendettaCustomPage", {
|
|
title: "Libre.fm Settings",
|
|
render: LibreFmSettingsPage,
|
|
})
|
|
}
|
|
/>
|
|
<TableRow
|
|
label="ListenBrainz Settings"
|
|
subLabel="Configure ListenBrainz credentials and options"
|
|
trailing={<TableRow.Arrow />}
|
|
onPress={() =>
|
|
navigation.push("VendettaCustomPage", {
|
|
title: "ListenBrainz Settings",
|
|
render: ListenBrainzSettingsPage,
|
|
})
|
|
}
|
|
/>
|
|
</TableRowGroup>
|
|
|
|
{/* Plugin Configuration */}
|
|
<TableRowGroup title="Plugin Configuration">
|
|
<TableRow
|
|
label="Display Settings"
|
|
subLabel="Customize how music activity appears"
|
|
trailing={<TableRow.Arrow />}
|
|
onPress={() =>
|
|
navigation.push("VendettaCustomPage", {
|
|
title: "Display Settings",
|
|
render: DisplaySettingsPage,
|
|
})
|
|
}
|
|
/>
|
|
<TableRow
|
|
label="Logging Settings"
|
|
subLabel="Configure logging and debugging options"
|
|
trailing={<TableRow.Arrow />}
|
|
onPress={() =>
|
|
navigation.push("VendettaCustomPage", {
|
|
title: "Logging Settings",
|
|
render: LoggingSettingsPage,
|
|
})
|
|
}
|
|
/>
|
|
<TableSwitchRow
|
|
label="Add to Sidebar"
|
|
subLabel="Show plugin in Discord settings"
|
|
value={get("addToSidebar", false)}
|
|
onValueChange={(value: boolean) => {
|
|
set("addToSidebar", value);
|
|
forceUpdate();
|
|
}}
|
|
/>
|
|
</TableRowGroup>
|
|
|
|
{/* About */}
|
|
<TableRowGroup title="About">
|
|
<TableRow
|
|
label="Multi Scrobbler"
|
|
subLabel="Show off your music status from multiple services"
|
|
/>
|
|
<TableRow label="Author" subLabel="kmmiio99o" />
|
|
<TableRow label="Version" subLabel="1.0.1" />
|
|
</TableRowGroup>
|
|
</Stack>
|
|
<RN.View style={{ height: 64 }} />
|
|
</ScrollView>
|
|
);
|
|
}
|