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:
kmmiio99o 2025-11-16 16:54:30 +01:00
parent 1928b45207
commit fdf24a1aeb
10 changed files with 1125 additions and 919 deletions

File diff suppressed because it is too large Load diff

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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;

View file

@ -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");