feat: Implement Settings page navigation
This commit introduces navigation between the main settings page and the settings pages for the individual components. It also adds the individual pages.
This commit is contained in:
parent
1928b45207
commit
fdf24a1aeb
10 changed files with 1125 additions and 919 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,71 @@
|
|||
// pages/DisplaySettingsPage.tsx
|
||||
import { React } from "@vendetta/metro/common";
|
||||
import { useProxy } from "@vendetta/storage";
|
||||
import {
|
||||
ScrollView,
|
||||
Stack,
|
||||
TableRowGroup,
|
||||
TableRow,
|
||||
TextInput,
|
||||
} from "./components/TableComponents";
|
||||
import { plugin } from "@vendetta";
|
||||
import Constants from "../../../constants";
|
||||
import { getStorage, setStorage } from "../Settings";
|
||||
|
||||
export default 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={getStorage("appName", Constants.DEFAULT_SETTINGS.appName)}
|
||||
onChange={(v: string) => {
|
||||
setStorage("appName", v);
|
||||
forceUpdate();
|
||||
}}
|
||||
isClearable
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={`Update Interval (Default: ${Constants.DEFAULT_SETTINGS.timeInterval}s)`}
|
||||
value={String(
|
||||
getStorage(
|
||||
"timeInterval",
|
||||
Constants.DEFAULT_SETTINGS.timeInterval,
|
||||
),
|
||||
)}
|
||||
onChange={(v: string) => {
|
||||
const interval = Number(v);
|
||||
if (interval >= Constants.MIN_UPDATE_INTERVAL) {
|
||||
setStorage("timeInterval", interval);
|
||||
forceUpdate();
|
||||
}
|
||||
}}
|
||||
keyboardType="numeric"
|
||||
isClearable
|
||||
/>
|
||||
</Stack>
|
||||
</TableRowGroup>
|
||||
|
||||
<TableRowGroup title="About Display Settings">
|
||||
<TableRow
|
||||
label="App Name"
|
||||
subLabel="The name shown in Discord for your activity"
|
||||
/>
|
||||
<TableRow
|
||||
label="Update Interval"
|
||||
subLabel="How often the plugin checks for new tracks (in seconds)"
|
||||
/>
|
||||
<TableRow
|
||||
label="Minimum Interval"
|
||||
subLabel={`The plugin will never check more frequently than ${Constants.MIN_UPDATE_INTERVAL} seconds`}
|
||||
/>
|
||||
</TableRowGroup>
|
||||
</Stack>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
// pages/IgnoreListSettingsPage.tsx
|
||||
import { React, ReactNative as RN } from "@vendetta/metro/common";
|
||||
import { useProxy } from "@vendetta/storage";
|
||||
import {
|
||||
ScrollView,
|
||||
Stack,
|
||||
TableRowGroup,
|
||||
TableRow,
|
||||
TextInput,
|
||||
} from "./components/TableComponents";
|
||||
import { plugin } from "@vendetta";
|
||||
import { showToast } from "@vendetta/ui/toasts";
|
||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||
import { getStorage, setStorage } from "../Settings";
|
||||
|
||||
export default function IgnoreListSettingsPage() {
|
||||
useProxy(plugin.storage);
|
||||
const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
|
||||
const [newAppName, setNewAppName] = React.useState("");
|
||||
|
||||
const addAppToIgnoreList = () => {
|
||||
if (!newAppName.trim()) {
|
||||
showToast("Please enter an app name", getAssetIDByName("Small"));
|
||||
return;
|
||||
}
|
||||
|
||||
const ignoreList = getStorage("ignoreList", []);
|
||||
if (!ignoreList.includes(newAppName.trim())) {
|
||||
setStorage("ignoreList", [...ignoreList, newAppName.trim()]);
|
||||
setNewAppName("");
|
||||
forceUpdate();
|
||||
showToast("App added to ignore list", getAssetIDByName("Check"));
|
||||
} else {
|
||||
showToast("App already in ignore list", getAssetIDByName("Warning"));
|
||||
}
|
||||
};
|
||||
|
||||
const removeAppFromIgnoreList = (appName: string) => {
|
||||
const ignoreList = getStorage("ignoreList", []);
|
||||
setStorage(
|
||||
"ignoreList",
|
||||
ignoreList.filter((app: string) => app !== appName),
|
||||
);
|
||||
forceUpdate();
|
||||
showToast("App removed from ignore list", getAssetIDByName("Check"));
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 10 }}>
|
||||
<Stack spacing={8}>
|
||||
<TableRowGroup title="Add App to Ignore">
|
||||
<Stack spacing={4}>
|
||||
<TextInput
|
||||
placeholder="Enter app name"
|
||||
value={newAppName}
|
||||
onChange={setNewAppName}
|
||||
isClearable
|
||||
onSubmitEditing={addAppToIgnoreList}
|
||||
returnKeyType="done"
|
||||
/>
|
||||
</Stack>
|
||||
</TableRowGroup>
|
||||
|
||||
<TableRowGroup>
|
||||
<TableRow
|
||||
label="Add to Ignore List"
|
||||
subLabel="Add the current app name to your ignore list"
|
||||
trailing={<TableRow.Arrow />}
|
||||
onPress={addAppToIgnoreList}
|
||||
/>
|
||||
</TableRowGroup>
|
||||
|
||||
{getStorage("ignoreList", []).length > 0 && (
|
||||
<TableRowGroup title="Ignored Apps">
|
||||
{getStorage("ignoreList", []).map(
|
||||
(appName: string, index: number) => (
|
||||
<TableRow
|
||||
key={index}
|
||||
label={appName}
|
||||
trailing={
|
||||
<RN.TouchableOpacity
|
||||
onPress={() => removeAppFromIgnoreList(appName)}
|
||||
style={{
|
||||
padding: 8,
|
||||
backgroundColor: "#ff4d4d",
|
||||
borderRadius: 12,
|
||||
width: 24,
|
||||
height: 24,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<RN.Image
|
||||
source={getAssetIDByName("TrashIcon")}
|
||||
style={{ width: 14, height: 14, tintColor: "#ffffff" }}
|
||||
/>
|
||||
</RN.TouchableOpacity>
|
||||
}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</TableRowGroup>
|
||||
)}
|
||||
|
||||
<TableRowGroup title="About Ignore List">
|
||||
<TableRow
|
||||
label="How it Works"
|
||||
subLabel="When any app in your ignore list is active, your music status will be hidden"
|
||||
/>
|
||||
<TableRow
|
||||
label="Detection"
|
||||
subLabel="Apps are detected by their Discord activity name"
|
||||
/>
|
||||
<TableRow
|
||||
label="Examples"
|
||||
subLabel="Spotify, YouTube Music, Kizzy, Metrolist, echo"
|
||||
/>
|
||||
</TableRowGroup>
|
||||
</Stack>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
// pages/LastFmSettingsPage.tsx
|
||||
import { React } from "@vendetta/metro/common";
|
||||
import { useProxy } from "@vendetta/storage";
|
||||
import { Linking } from "react-native";
|
||||
import { showToast } from "@vendetta/ui/toasts";
|
||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||
import {
|
||||
ScrollView,
|
||||
Stack,
|
||||
TableRowGroup,
|
||||
TableRow,
|
||||
TextInput,
|
||||
} from "./components/TableComponents";
|
||||
import { plugin } from "@vendetta";
|
||||
import { serviceFactory, getStorage, setStorage } from "../Settings";
|
||||
|
||||
export default 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={getStorage("username")}
|
||||
onChange={(v: string) => {
|
||||
setStorage("username", v);
|
||||
forceUpdate();
|
||||
}}
|
||||
isClearable
|
||||
/>
|
||||
<TextInput
|
||||
placeholder="Last.fm API Key"
|
||||
value={getStorage("apiKey")}
|
||||
onChange={(v: string) => {
|
||||
setStorage("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>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
// pages/LibreFmSettingsPage.tsx
|
||||
import { React } from "@vendetta/metro/common";
|
||||
import { useProxy } from "@vendetta/storage";
|
||||
import { Linking } from "react-native";
|
||||
import { showToast } from "@vendetta/ui/toasts";
|
||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||
import {
|
||||
ScrollView,
|
||||
Stack,
|
||||
TableRowGroup,
|
||||
TableRow,
|
||||
TextInput,
|
||||
} from "./components/TableComponents";
|
||||
import { plugin } from "@vendetta";
|
||||
import { serviceFactory, getStorage, setStorage } from "../Settings";
|
||||
|
||||
export default 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={getStorage("librefmUsername")}
|
||||
onChange={(v: string) => {
|
||||
setStorage("librefmUsername", v);
|
||||
forceUpdate();
|
||||
}}
|
||||
isClearable
|
||||
/>
|
||||
<TextInput
|
||||
placeholder="Libre.fm API Key"
|
||||
value={getStorage("librefmApiKey")}
|
||||
onChange={(v: string) => {
|
||||
setStorage("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>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
// pages/ListenBrainzSettingsPage.tsx
|
||||
import { React } from "@vendetta/metro/common";
|
||||
import { useProxy } from "@vendetta/storage";
|
||||
import { Linking } from "react-native";
|
||||
import { showToast } from "@vendetta/ui/toasts";
|
||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||
import {
|
||||
ScrollView,
|
||||
Stack,
|
||||
TableRowGroup,
|
||||
TableRow,
|
||||
TextInput,
|
||||
} from "./components/TableComponents";
|
||||
import { plugin } from "@vendetta";
|
||||
import { serviceFactory, getStorage, setStorage } from "../Settings";
|
||||
|
||||
export default 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"
|
||||
value={getStorage("listenbrainzUsername")}
|
||||
onChange={(v: string) => {
|
||||
setStorage("listenbrainzUsername", v);
|
||||
forceUpdate();
|
||||
}}
|
||||
isClearable
|
||||
/>
|
||||
<TextInput
|
||||
placeholder="ListenBrainz Token (for private profiles)"
|
||||
value={getStorage("listenbrainzToken")}
|
||||
onChange={(v: string) => {
|
||||
setStorage("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>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
// pages/LoggingSettingsPage.tsx
|
||||
import { React } from "@vendetta/metro/common";
|
||||
import { useProxy } from "@vendetta/storage";
|
||||
import {
|
||||
ScrollView,
|
||||
Stack,
|
||||
TableRowGroup,
|
||||
TableRow,
|
||||
TableSwitchRow,
|
||||
} from "./components/TableComponents";
|
||||
import { plugin } from "@vendetta";
|
||||
import Constants from "../../../constants";
|
||||
import { getStorage, setStorage } from "../Settings";
|
||||
|
||||
export default 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={getStorage(
|
||||
"verboseLogging",
|
||||
Constants.DEFAULT_SETTINGS.verboseLogging,
|
||||
)}
|
||||
onValueChange={(value: boolean) => {
|
||||
setStorage("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>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
// pages/RPCCustomizationSettingsPage.tsx
|
||||
import { React } from "@vendetta/metro/common";
|
||||
import { useProxy } from "@vendetta/storage";
|
||||
import {
|
||||
ScrollView,
|
||||
Stack,
|
||||
TableRowGroup,
|
||||
TableSwitchRow,
|
||||
} from "./components/TableComponents";
|
||||
import { plugin } from "@vendetta";
|
||||
import Constants from "../../../constants";
|
||||
import { getStorage, setStorage } from "../Settings";
|
||||
import RPCPreview from "./components/RPCPreview";
|
||||
|
||||
export default function RPCCustomizationSettingsPage() {
|
||||
useProxy(plugin.storage);
|
||||
const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
|
||||
|
||||
return (
|
||||
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 10 }}>
|
||||
<RPCPreview />
|
||||
<Stack spacing={8}>
|
||||
<TableRowGroup title="RPC Display Options">
|
||||
<TableSwitchRow
|
||||
label="Show as Listening"
|
||||
subLabel="Display as 'Listening to' instead of 'Playing'"
|
||||
value={getStorage(
|
||||
"listeningTo",
|
||||
Constants.DEFAULT_SETTINGS.listeningTo,
|
||||
)}
|
||||
onValueChange={(value: boolean) => {
|
||||
setStorage("listeningTo", value);
|
||||
forceUpdate();
|
||||
}}
|
||||
/>
|
||||
<TableSwitchRow
|
||||
label="Show Tooltip Text"
|
||||
subLabel="Show album name and track duration in Discord activity tooltip"
|
||||
value={getStorage("showLargeText", true)}
|
||||
onValueChange={(value: boolean) => {
|
||||
setStorage("showLargeText", value);
|
||||
forceUpdate();
|
||||
}}
|
||||
/>
|
||||
<TableSwitchRow
|
||||
label="Show Timestamp"
|
||||
subLabel="Display track progress and duration"
|
||||
value={getStorage(
|
||||
"showTimestamp",
|
||||
Constants.DEFAULT_SETTINGS.showTimestamp,
|
||||
)}
|
||||
onValueChange={(value: boolean) => {
|
||||
setStorage("showTimestamp", value);
|
||||
forceUpdate();
|
||||
}}
|
||||
/>
|
||||
<TableSwitchRow
|
||||
label="Show Album in Tooltip"
|
||||
subLabel="Include album name in the tooltip text"
|
||||
value={getStorage("showAlbumInTooltip", true)}
|
||||
onValueChange={(value: boolean) => {
|
||||
setStorage("showAlbumInTooltip", value);
|
||||
forceUpdate();
|
||||
}}
|
||||
/>
|
||||
<TableSwitchRow
|
||||
label="Show Duration in Tooltip"
|
||||
subLabel="Include track duration in the tooltip text"
|
||||
value={getStorage("showDurationInTooltip", true)}
|
||||
onValueChange={(value: boolean) => {
|
||||
setStorage("showDurationInTooltip", value);
|
||||
forceUpdate();
|
||||
}}
|
||||
/>
|
||||
</TableRowGroup>
|
||||
</Stack>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,381 @@
|
|||
// pages/components/RPCPreview.tsx
|
||||
import { React, ReactNative as RN } from "@vendetta/metro/common";
|
||||
import { useProxy } from "@vendetta/storage";
|
||||
import { plugin } from "@vendetta";
|
||||
import { getStorage } from "../../Settings";
|
||||
|
||||
export default function RPCPreview() {
|
||||
useProxy(plugin.storage);
|
||||
const [previewTrack, setPreviewTrack] = React.useState<any>(null);
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
const [currentProgress, setCurrentProgress] = React.useState(0);
|
||||
const [usingFallback, setUsingFallback] = React.useState(false);
|
||||
|
||||
// Fallback preview data
|
||||
const fallbackTrack = {
|
||||
name: "Bohemian Rhapsody",
|
||||
artist: "Queen",
|
||||
album: "A Night at the Opera",
|
||||
image: null,
|
||||
nowPlaying: true,
|
||||
duration: 354,
|
||||
startTime: Math.floor(Date.now() / 1000) - 120,
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const fetchPreviewData = async () => {
|
||||
if (!getStorage("username") || !getStorage("apiKey")) {
|
||||
// Use fallback if no credentials
|
||||
setPreviewTrack(fallbackTrack);
|
||||
setUsingFallback(true);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await fetch(
|
||||
`https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=${getStorage("username")}&api_key=${getStorage("apiKey")}&format=json&limit=1`,
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
if (
|
||||
data.recenttracks &&
|
||||
data.recenttracks.track &&
|
||||
data.recenttracks.track.length > 0
|
||||
) {
|
||||
const track = data.recenttracks.track[0];
|
||||
|
||||
let duration = 180;
|
||||
try {
|
||||
const trackInfoResponse = await fetch(
|
||||
`https://ws.audioscrobbler.com/2.0/?method=track.getInfo&api_key=${getStorage("apiKey")}&artist=${encodeURIComponent(track.artist["#text"])}&track=${encodeURIComponent(track.name)}&format=json&username=${getStorage("username")}`,
|
||||
);
|
||||
const trackInfo = await trackInfoResponse.json();
|
||||
if (trackInfo.track && trackInfo.track.duration) {
|
||||
duration = Math.floor(trackInfo.track.duration / 1000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Could not fetch track duration, using default");
|
||||
}
|
||||
|
||||
setPreviewTrack({
|
||||
name: track.name || "Unknown Track",
|
||||
artist: track.artist?.["#text"] || "Unknown Artist",
|
||||
album: track.album?.["#text"] || "Unknown Album",
|
||||
image:
|
||||
track.image?.[2]?.["#text"] ||
|
||||
track.image?.[1]?.["#text"] ||
|
||||
null,
|
||||
nowPlaying: track["@attr"]?.nowplaying === "true",
|
||||
duration: duration,
|
||||
startTime:
|
||||
track["@attr"]?.nowplaying === "true"
|
||||
? Math.floor(Date.now() / 1000) - 60
|
||||
: null,
|
||||
});
|
||||
setUsingFallback(false);
|
||||
} else {
|
||||
// No tracks found, use fallback
|
||||
setPreviewTrack(fallbackTrack);
|
||||
setUsingFallback(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch preview data, using fallback:", error);
|
||||
// Use fallback on error
|
||||
setPreviewTrack(fallbackTrack);
|
||||
setUsingFallback(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPreviewData();
|
||||
}, [getStorage("username"), getStorage("apiKey")]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!previewTrack?.nowPlaying || !getStorage("showTimestamp")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (previewTrack.startTime && previewTrack.duration) {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const elapsed = now - previewTrack.startTime;
|
||||
const progress = Math.min(elapsed / previewTrack.duration, 1);
|
||||
setCurrentProgress(progress);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [previewTrack, getStorage("showTimestamp")]);
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return `${mins}:${secs.toString().padStart(2, "0")}`;
|
||||
};
|
||||
|
||||
const getPreviewText = () => {
|
||||
let text = "";
|
||||
|
||||
if (getStorage("showAlbumInTooltip") && previewTrack?.album) {
|
||||
text += `on ${previewTrack.album}`;
|
||||
}
|
||||
|
||||
if (getStorage("showDurationInTooltip") && previewTrack?.duration) {
|
||||
const durationText = ` • ${formatTime(previewTrack.duration)}`;
|
||||
if (text) {
|
||||
text += durationText;
|
||||
} else {
|
||||
text = formatTime(previewTrack.duration);
|
||||
}
|
||||
}
|
||||
|
||||
return text || "No tooltip text";
|
||||
};
|
||||
|
||||
const getCurrentProgressData = () => {
|
||||
if (!previewTrack?.duration) return { current: 0, total: 0, progress: 0 };
|
||||
|
||||
if (previewTrack.nowPlaying) {
|
||||
const current = currentProgress * previewTrack.duration;
|
||||
return {
|
||||
current: current,
|
||||
total: previewTrack.duration,
|
||||
progress: currentProgress,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
current: previewTrack.duration * 0.3,
|
||||
total: previewTrack.duration,
|
||||
progress: 0.3,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const activityType = getStorage("listeningTo") ? "Listening to" : "Playing";
|
||||
const appName = getStorage("appName", "Music");
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<RN.View style={styles.container}>
|
||||
<RN.View style={styles.loadingContent}>
|
||||
<RN.View style={styles.loadingSpinner} />
|
||||
<RN.Text style={styles.loadingText}>Loading preview...</RN.Text>
|
||||
</RN.View>
|
||||
</RN.View>
|
||||
);
|
||||
}
|
||||
|
||||
if (!previewTrack) {
|
||||
return (
|
||||
<RN.View style={styles.container}>
|
||||
<RN.Text style={styles.centeredText}>Unable to load preview</RN.Text>
|
||||
</RN.View>
|
||||
);
|
||||
}
|
||||
|
||||
const progressData = getCurrentProgressData();
|
||||
|
||||
return (
|
||||
<RN.View style={styles.previewContainer}>
|
||||
<RN.View style={styles.header}>
|
||||
<RN.Text style={styles.activityType}>
|
||||
{activityType} {appName}
|
||||
</RN.Text>
|
||||
<RN.Text style={styles.rpcPreviewText} numberOfLines={1}>
|
||||
RPC Preview
|
||||
</RN.Text>
|
||||
</RN.View>
|
||||
|
||||
<RN.View style={styles.content}>
|
||||
<RN.View style={styles.albumArt}>
|
||||
{previewTrack.image ? (
|
||||
<RN.Image
|
||||
source={{ uri: previewTrack.image }}
|
||||
style={styles.albumImage}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
) : (
|
||||
<RN.Text style={styles.musicIcon}>🎵</RN.Text>
|
||||
)}
|
||||
</RN.View>
|
||||
|
||||
<RN.View style={styles.trackInfo}>
|
||||
<RN.Text style={styles.trackName} numberOfLines={1}>
|
||||
{previewTrack.name}
|
||||
</RN.Text>
|
||||
<RN.Text style={styles.artistName} numberOfLines={1}>
|
||||
{previewTrack.artist}
|
||||
</RN.Text>
|
||||
{getStorage("showLargeText") &&
|
||||
getPreviewText() !== "No tooltip text" && (
|
||||
<RN.Text style={styles.tooltipText} numberOfLines={1}>
|
||||
{getPreviewText()}
|
||||
</RN.Text>
|
||||
)}
|
||||
|
||||
{getStorage("showTimestamp") && previewTrack.duration && (
|
||||
<RN.View style={styles.progressContainer}>
|
||||
<RN.Text style={styles.timeText}>
|
||||
{formatTime(progressData.current)}
|
||||
</RN.Text>
|
||||
<RN.View style={styles.progressBar}>
|
||||
<RN.View
|
||||
style={[
|
||||
styles.progressFill,
|
||||
{ width: `${progressData.progress * 100}%` },
|
||||
]}
|
||||
/>
|
||||
</RN.View>
|
||||
<RN.Text style={styles.timeText}>
|
||||
{formatTime(progressData.total)}
|
||||
</RN.Text>
|
||||
</RN.View>
|
||||
)}
|
||||
</RN.View>
|
||||
</RN.View>
|
||||
</RN.View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
backgroundColor: "#1e1f22",
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginHorizontal: 10,
|
||||
marginBottom: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: "#3a3c41",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minHeight: 120,
|
||||
},
|
||||
loadingContent: {
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
loadingSpinner: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
borderWidth: 2,
|
||||
borderColor: "#5865f2",
|
||||
borderTopColor: "transparent",
|
||||
marginBottom: 8,
|
||||
},
|
||||
previewContainer: {
|
||||
backgroundColor: "#1e1f22",
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginHorizontal: 10,
|
||||
marginBottom: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: "#3a3c41",
|
||||
},
|
||||
header: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: 12,
|
||||
gap: 8,
|
||||
},
|
||||
content: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
},
|
||||
albumArt: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
backgroundColor: "#2b2d31",
|
||||
borderRadius: 12,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginRight: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: "#40444b",
|
||||
overflow: "hidden",
|
||||
flexShrink: 0,
|
||||
},
|
||||
trackInfo: {
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
},
|
||||
progressContainer: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
marginTop: 6,
|
||||
},
|
||||
progressBar: {
|
||||
flex: 1,
|
||||
height: 2,
|
||||
backgroundColor: "#2b2d31",
|
||||
borderRadius: 3,
|
||||
overflow: "hidden",
|
||||
},
|
||||
progressFill: {
|
||||
height: 2,
|
||||
backgroundColor: "#5865f2",
|
||||
borderRadius: 3,
|
||||
},
|
||||
loadingText: {
|
||||
color: "#949ba4",
|
||||
fontSize: 14,
|
||||
fontWeight: "500",
|
||||
},
|
||||
centeredText: {
|
||||
color: "#949ba4",
|
||||
fontSize: 14,
|
||||
fontWeight: "500",
|
||||
textAlign: "center",
|
||||
},
|
||||
activityType: {
|
||||
color: "#dbdee1",
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
flex: 1,
|
||||
},
|
||||
rpcPreviewText: {
|
||||
color: "#80848e",
|
||||
fontSize: 12,
|
||||
fontStyle: "italic",
|
||||
flexShrink: 0,
|
||||
},
|
||||
albumImage: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 12,
|
||||
},
|
||||
musicIcon: {
|
||||
color: "#80848e",
|
||||
fontSize: 32,
|
||||
},
|
||||
trackName: {
|
||||
color: "#ffffff",
|
||||
fontSize: 16,
|
||||
fontWeight: "700",
|
||||
marginBottom: 4,
|
||||
},
|
||||
artistName: {
|
||||
color: "#b5bac1",
|
||||
fontSize: 14,
|
||||
fontWeight: "500",
|
||||
marginBottom: 4,
|
||||
},
|
||||
tooltipText: {
|
||||
color: "#80848e",
|
||||
fontSize: 12,
|
||||
fontStyle: "italic",
|
||||
marginBottom: 6,
|
||||
},
|
||||
timeText: {
|
||||
color: "#80848e",
|
||||
fontSize: 11,
|
||||
fontWeight: "500",
|
||||
minWidth: 35,
|
||||
textAlign: "center",
|
||||
},
|
||||
} as const;
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { findByProps } from "@vendetta/metro";
|
||||
|
||||
export const { ScrollView } = findByProps("ScrollView");
|
||||
export const { TableRowGroup, TableSwitchRow, Stack, TableRow } = findByProps(
|
||||
"TableSwitchRow",
|
||||
"TableRowGroup",
|
||||
"Stack",
|
||||
"TableRow",
|
||||
);
|
||||
export const { TextInput } = findByProps("TextInput");
|
||||
Loading…
Add table
Reference in a new issue