feat: Implement RPC customization settings
- Added RPC preview component for live activity preview. - Added settings for "Listening to" status, show tooltip text, album name and duration in the tooltip. - Refactored settings structure for better organization. - Fixed an issue where the sidebar settings weren't showing. - Updated the minified build.
This commit is contained in:
parent
fc3ff41838
commit
1928b45207
6 changed files with 554 additions and 97 deletions
|
|
@ -3,7 +3,7 @@
|
||||||
"build": "node scripts/build.mjs",
|
"build": "node scripts/build.mjs",
|
||||||
"watch": "node scripts/build.mjs -w",
|
"watch": "node scripts/build.mjs -w",
|
||||||
"lint": "eslint plugins",
|
"lint": "eslint plugins",
|
||||||
"make": "node make.mjs",
|
"make": "node scripts/make.mjs",
|
||||||
"serve": "node scripts/serve.mjs"
|
"serve": "node scripts/serve.mjs"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ const Constants = {
|
||||||
DEFAULT_APP_NAME: "Music",
|
DEFAULT_APP_NAME: "Music",
|
||||||
DEFAULT_TIME_INTERVAL: 5,
|
DEFAULT_TIME_INTERVAL: 5,
|
||||||
// Discord application ID
|
// Discord application ID
|
||||||
APPLICATION_ID: "1054951789318909972",
|
APPLICATION_ID: "1368513179272871956",
|
||||||
// Don't check more than once every 3 seconds to avoid getting rate limited
|
// Don't check more than once every 3 seconds to avoid getting rate limited
|
||||||
MIN_UPDATE_INTERVAL: 3,
|
MIN_UPDATE_INTERVAL: 3,
|
||||||
// How many times to retry failed API calls
|
// How many times to retry failed API calls
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ export const pluginState = {
|
||||||
} as {
|
} as {
|
||||||
pluginStopped: boolean;
|
pluginStopped: boolean;
|
||||||
lastActivity?: any;
|
lastActivity?: any;
|
||||||
updateInterval?: NodeJS.Timer;
|
updateInterval?: NodeJS.Timeout;
|
||||||
lastTrackUrl?: string;
|
lastTrackUrl?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -189,37 +189,53 @@ class PluginManager {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// set up album art and tooltip text
|
// set up album art and tooltip text - SIMPLIFIED VERSION (no fallback image)
|
||||||
if (lastTrack.album || lastTrack.albumArt) {
|
if (lastTrack.album || lastTrack.albumArt) {
|
||||||
const assetUrls = lastTrack.albumArt ? [lastTrack.albumArt] : [];
|
const assetUrls = lastTrack.albumArt ? [lastTrack.albumArt] : [];
|
||||||
const assets = await fetchAsset(assetUrls);
|
const assets = await fetchAsset(assetUrls);
|
||||||
|
|
||||||
if (assets[0]) {
|
// Only use album art if available
|
||||||
|
let largeImageAsset = assets[0];
|
||||||
|
|
||||||
|
if (largeImageAsset) {
|
||||||
|
activity.assets = {
|
||||||
|
large_image: largeImageAsset,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build tooltip text based on settings
|
||||||
if (currentSettings.showLargeText) {
|
if (currentSettings.showLargeText) {
|
||||||
const largeText = lastTrack.album
|
let largeText = "";
|
||||||
? `on ${lastTrack.album}`
|
|
||||||
: `${lastTrack.artist} - ${lastTrack.name}`;
|
|
||||||
|
|
||||||
const durationText =
|
if (currentSettings.showAlbumInTooltip && lastTrack.album) {
|
||||||
currentSettings.showTimestamp && lastTrack.duration
|
largeText += `on ${lastTrack.album}`;
|
||||||
? ` • ${formatDuration(lastTrack.duration)}`
|
}
|
||||||
: "";
|
|
||||||
|
|
||||||
activity.assets = {
|
if (currentSettings.showDurationInTooltip && lastTrack.duration) {
|
||||||
large_image: assets[0],
|
const durationText = formatDuration(lastTrack.duration);
|
||||||
large_text: largeText + durationText,
|
if (largeText) {
|
||||||
};
|
largeText += ` • ${durationText}`;
|
||||||
} else {
|
} else {
|
||||||
activity.assets = {
|
largeText = durationText;
|
||||||
large_image: assets[0],
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
if (largeText) {
|
||||||
|
activity.assets.large_text = largeText;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logVerbose("Album art set:", assets[0]);
|
logVerbose("Album art set:", largeImageAsset);
|
||||||
} else if (lastTrack.album) {
|
if (activity.assets.large_text) {
|
||||||
activity.assets = currentSettings.showLargeText
|
logVerbose("Tooltip text set:", activity.assets.large_text);
|
||||||
? { large_text: `on ${lastTrack.album}` }
|
}
|
||||||
: {};
|
} else if (
|
||||||
|
lastTrack.album &&
|
||||||
|
currentSettings.showLargeText &&
|
||||||
|
currentSettings.showAlbumInTooltip
|
||||||
|
) {
|
||||||
|
activity.assets = {
|
||||||
|
large_text: `on ${lastTrack.album}`,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ const tabsNavigationRef = bunny?.metro?.findByPropsLazy("getRootNavigationRef");
|
||||||
const settingConstants = bunny?.metro?.findByPropsLazy(
|
const settingConstants = bunny?.metro?.findByPropsLazy(
|
||||||
"SETTING_RENDERER_CONFIG",
|
"SETTING_RENDERER_CONFIG",
|
||||||
);
|
);
|
||||||
|
const createListModule = bunny?.metro?.findByPropsLazy("createList");
|
||||||
const SettingsOverviewScreen = bunny?.metro?.findByNameLazy(
|
const SettingsOverviewScreen = bunny?.metro?.findByNameLazy(
|
||||||
"SettingsOverviewScreen",
|
"SettingsOverviewScreen",
|
||||||
false,
|
false,
|
||||||
|
|
@ -96,37 +97,32 @@ function patchPanelUI(tabs, patches) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchTabsUI(tabs, patches) {
|
function patchTabsUI(tabs, patches) {
|
||||||
if (!settingConstants || !SettingsOverviewScreen || !tabsNavigationRef) {
|
if (!settingConstants || !tabsNavigationRef) {
|
||||||
console.warn("[LastFm] Missing required constants for tabs UI patch");
|
console.warn("[LastFm] Missing required constants for tabs UI patch");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const row = {};
|
const row = {
|
||||||
row[tabs.key] = {
|
[tabs.key]: {
|
||||||
type: "pressable",
|
type: "pressable",
|
||||||
title: tabs.title,
|
title: tabs.title,
|
||||||
icon: tabs.icon,
|
icon: tabs.icon,
|
||||||
IconComponent:
|
IconComponent:
|
||||||
tabs.icon &&
|
tabs.icon &&
|
||||||
(() => {
|
(() => React.createElement(TableRowIcon, { source: tabs.icon })),
|
||||||
const actualIconSource =
|
usePredicate: tabs.predicate,
|
||||||
typeof tabs.icon === "object" && tabs.icon.uri !== undefined
|
useTrailing: tabs.trailing,
|
||||||
? tabs.icon.uri
|
onPress: () => {
|
||||||
: tabs.icon;
|
const navigation = tabsNavigationRef.getRootNavigationRef();
|
||||||
return React.createElement(TableRowIcon, { source: actualIconSource });
|
const Component = tabs.page;
|
||||||
}),
|
|
||||||
usePredicate: tabs.predicate,
|
|
||||||
useTrailing: tabs.trailing,
|
|
||||||
onPress: () => {
|
|
||||||
const navigation = tabsNavigationRef.getRootNavigationRef();
|
|
||||||
const Component = tabs.page;
|
|
||||||
|
|
||||||
navigation.navigate("VendettaCustomPage", {
|
navigation.navigate("VendettaCustomPage", {
|
||||||
title: tabs.title(),
|
title: tabs.title(),
|
||||||
render: () => React.createElement(Component),
|
render: () => React.createElement(Component),
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
withArrow: true,
|
||||||
},
|
},
|
||||||
withArrow: true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let rendererConfigValue = settingConstants.SETTING_RENDERER_CONFIG;
|
let rendererConfigValue = settingConstants.SETTING_RENDERER_CONFIG;
|
||||||
|
|
@ -141,30 +137,54 @@ function patchTabsUI(tabs, patches) {
|
||||||
set: (v) => (rendererConfigValue = v),
|
set: (v) => (rendererConfigValue = v),
|
||||||
});
|
});
|
||||||
|
|
||||||
const firstRender = Symbol("LastFm-pinToSettings");
|
const firstRender = Symbol("pinToSettings meow meow");
|
||||||
|
|
||||||
patches.push(
|
try {
|
||||||
after("default", SettingsOverviewScreen, (args, ret) => {
|
if (!createListModule) return;
|
||||||
if (!(args[0] as { [key: symbol]: boolean })[firstRender]) {
|
patches.push(
|
||||||
(args[0] as { [key: symbol]: boolean })[firstRender] = true;
|
after("createList", createListModule, function (args, ret) {
|
||||||
|
if (!args[0][firstRender]) {
|
||||||
|
args[0][firstRender] = true;
|
||||||
|
|
||||||
const { sections } = findInReactTree(
|
const [config] = args;
|
||||||
ret,
|
const sections = config.sections;
|
||||||
(i) => i.props?.sections,
|
|
||||||
).props;
|
|
||||||
|
|
||||||
const section = sections?.find((x) =>
|
const section = sections?.find((x: any) =>
|
||||||
["Bunny", "Revenge", "Kettu", "Vencore", "ShiggyCord"].some(
|
["Bunny", "Revenge", "Kettu", "Vencore", "ShiggyCord"].some(
|
||||||
(mod) => x.label === mod && x.title === mod,
|
(mod) => x.label === mod && x.title === mod,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (section?.settings) {
|
if (section?.settings) {
|
||||||
section.settings = [...section.settings, tabs.key];
|
section.settings = [...section.settings, tabs.key];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}),
|
||||||
}),
|
);
|
||||||
);
|
} catch {
|
||||||
|
if (!SettingsOverviewScreen) return;
|
||||||
|
patches.push(
|
||||||
|
after("default", SettingsOverviewScreen, (args, ret) => {
|
||||||
|
if (!args[0][firstRender]) {
|
||||||
|
args[0][firstRender] = true;
|
||||||
|
|
||||||
|
const { sections } = findInReactTree(
|
||||||
|
ret,
|
||||||
|
(i) => i.props?.sections,
|
||||||
|
).props;
|
||||||
|
const section = sections?.find((x: any) =>
|
||||||
|
["Bunny", "Revenge", "Kettu", "Vencore", "ShiggyCord"].some(
|
||||||
|
(mod) => x.label === mod && x.title === mod,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (section?.settings) {
|
||||||
|
section.settings = [...section.settings, tabs.key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchSettingsPin(tabs) {
|
function patchSettingsPin(tabs) {
|
||||||
|
|
|
||||||
|
|
@ -33,13 +33,397 @@ plugin.storage.librefmUsername ??= "";
|
||||||
plugin.storage.librefmApiKey ??= "";
|
plugin.storage.librefmApiKey ??= "";
|
||||||
plugin.storage.listenbrainzUsername ??= "";
|
plugin.storage.listenbrainzUsername ??= "";
|
||||||
plugin.storage.listenbrainzToken ??= "";
|
plugin.storage.listenbrainzToken ??= "";
|
||||||
plugin.storage.addToSidebar ??= false;
|
plugin.storage.addToSidebar ??= true;
|
||||||
plugin.storage.showLargeText ??= true;
|
plugin.storage.showLargeText ??= true;
|
||||||
plugin.storage.ignoreList ??= [];
|
plugin.storage.ignoreList ??= [];
|
||||||
|
plugin.storage.showAlbumInTooltip ??= true;
|
||||||
|
plugin.storage.showDurationInTooltip ??= true;
|
||||||
|
// Removed: plugin.storage.showFallbackImage ??= false;
|
||||||
|
|
||||||
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);
|
const set = (k: string, v: any) => (plugin.storage[k] = v);
|
||||||
|
|
||||||
|
// RPC Preview Component
|
||||||
|
function RPCPreview() {
|
||||||
|
useProxy(plugin.storage);
|
||||||
|
const [previewTrack, setPreviewTrack] = React.useState<any>(null);
|
||||||
|
const [isLoading, setIsLoading] = React.useState(true);
|
||||||
|
const [currentProgress, setCurrentProgress] = React.useState(0);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const fetchPreviewData = async () => {
|
||||||
|
if (!get("username") || !get("apiKey")) {
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
// Try to get recent tracks from Last.fm API
|
||||||
|
const response = await fetch(
|
||||||
|
`https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=${get("username")}&api_key=${get("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];
|
||||||
|
|
||||||
|
// Try to get track info for duration
|
||||||
|
let duration = 180; // Default 3 minutes
|
||||||
|
try {
|
||||||
|
const trackInfoResponse = await fetch(
|
||||||
|
`https://ws.audioscrobbler.com/2.0/?method=track.getInfo&api_key=${get("apiKey")}&artist=${encodeURIComponent(track.artist["#text"])}&track=${encodeURIComponent(track.name)}&format=json&username=${get("username")}`,
|
||||||
|
);
|
||||||
|
const trackInfo = await trackInfoResponse.json();
|
||||||
|
if (trackInfo.track && trackInfo.track.duration) {
|
||||||
|
duration = Math.floor(trackInfo.track.duration / 1000); // Convert ms to seconds
|
||||||
|
}
|
||||||
|
} 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, // Simulate 1 minute progress for now playing
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch preview data:", error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchPreviewData();
|
||||||
|
}, [get("username"), get("apiKey")]);
|
||||||
|
|
||||||
|
// Update progress for now playing tracks
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!previewTrack?.nowPlaying || !get("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, get("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 (get("showAlbumInTooltip") && previewTrack?.album) {
|
||||||
|
text += `on ${previewTrack.album}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (get("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 {
|
||||||
|
// For non-now playing tracks, show 30% progress as example
|
||||||
|
return {
|
||||||
|
current: previewTrack.duration * 0.3,
|
||||||
|
total: previewTrack.duration,
|
||||||
|
progress: 0.3,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const activityType = get("listeningTo") ? "Listening to" : "Playing";
|
||||||
|
const appName = get("appName", "Music");
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<RN.View
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#2f3136",
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 16,
|
||||||
|
marginHorizontal: 10,
|
||||||
|
marginBottom: 16,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "#40444b",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
minHeight: 120,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RN.Text style={{ color: "#b9bbbe", fontSize: 14 }}>
|
||||||
|
Loading preview from Last.fm...
|
||||||
|
</RN.Text>
|
||||||
|
</RN.View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!get("username") || !get("apiKey")) {
|
||||||
|
return (
|
||||||
|
<RN.View
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#2f3136",
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 16,
|
||||||
|
marginHorizontal: 10,
|
||||||
|
marginBottom: 16,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "#40444b",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
minHeight: 120,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RN.Text
|
||||||
|
style={{ color: "#b9bbbe", fontSize: 14, textAlign: "center" }}
|
||||||
|
>
|
||||||
|
Configure Last.fm credentials to see preview
|
||||||
|
</RN.Text>
|
||||||
|
</RN.View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!previewTrack) {
|
||||||
|
return (
|
||||||
|
<RN.View
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#2f3136",
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 16,
|
||||||
|
marginHorizontal: 10,
|
||||||
|
marginBottom: 16,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "#40444b",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
minHeight: 120,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RN.Text
|
||||||
|
style={{ color: "#b9bbbe", fontSize: 14, textAlign: "center" }}
|
||||||
|
>
|
||||||
|
No recent tracks found or error loading data
|
||||||
|
</RN.Text>
|
||||||
|
</RN.View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const progressData = getCurrentProgressData();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RN.View
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#2f3136",
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 16,
|
||||||
|
marginHorizontal: 10,
|
||||||
|
marginBottom: 16,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "#40444b",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Activity Type Header - replaces "RPC Preview" */}
|
||||||
|
<RN.View
|
||||||
|
style={{
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
marginBottom: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RN.Text
|
||||||
|
style={{
|
||||||
|
color: "#b9bbbe",
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: "600",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{activityType} {appName}
|
||||||
|
</RN.Text>
|
||||||
|
|
||||||
|
{/* Now Playing Indicator - subtle text instead of blue badge */}
|
||||||
|
{previewTrack.nowPlaying && (
|
||||||
|
<RN.Text
|
||||||
|
style={{
|
||||||
|
color: "#72767d",
|
||||||
|
fontSize: 12,
|
||||||
|
fontStyle: "italic",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
RPC Preview
|
||||||
|
</RN.Text>
|
||||||
|
)}
|
||||||
|
</RN.View>
|
||||||
|
|
||||||
|
<RN.View
|
||||||
|
style={{
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Album Art */}
|
||||||
|
<RN.View
|
||||||
|
style={{
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
backgroundColor: "#36393f",
|
||||||
|
borderRadius: 4,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
marginRight: 12,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "#40444b",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{previewTrack.image ? (
|
||||||
|
<RN.Image
|
||||||
|
source={{ uri: previewTrack.image }}
|
||||||
|
style={{ width: 60, height: 60 }}
|
||||||
|
resizeMode="cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<RN.Text
|
||||||
|
style={{
|
||||||
|
color: "#72767d",
|
||||||
|
fontSize: 24,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
🎵
|
||||||
|
</RN.Text>
|
||||||
|
)}
|
||||||
|
</RN.View>
|
||||||
|
|
||||||
|
<RN.View style={{ flex: 1 }}>
|
||||||
|
{/* Track Name */}
|
||||||
|
<RN.Text
|
||||||
|
style={{
|
||||||
|
color: "white",
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "600",
|
||||||
|
marginBottom: 4,
|
||||||
|
}}
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
|
{previewTrack.name}
|
||||||
|
</RN.Text>
|
||||||
|
|
||||||
|
{/* Artist Name */}
|
||||||
|
<RN.Text
|
||||||
|
style={{
|
||||||
|
color: "#b9bbbe",
|
||||||
|
fontSize: 14,
|
||||||
|
marginBottom: 4,
|
||||||
|
}}
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
|
{previewTrack.artist}
|
||||||
|
</RN.Text>
|
||||||
|
|
||||||
|
{/* Tooltip Text */}
|
||||||
|
{get("showLargeText") && getPreviewText() !== "No tooltip text" && (
|
||||||
|
<RN.Text
|
||||||
|
style={{
|
||||||
|
color: "#72767d",
|
||||||
|
fontSize: 12,
|
||||||
|
fontStyle: "italic",
|
||||||
|
marginBottom: 4,
|
||||||
|
}}
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
|
{getPreviewText()}
|
||||||
|
</RN.Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Timestamps */}
|
||||||
|
{get("showTimestamp") && previewTrack.duration && (
|
||||||
|
<RN.View
|
||||||
|
style={{
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RN.View
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
height: 4,
|
||||||
|
backgroundColor: "#36393f",
|
||||||
|
borderRadius: 2,
|
||||||
|
marginRight: 8,
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RN.View
|
||||||
|
style={{
|
||||||
|
width: `${progressData.progress * 100}%`,
|
||||||
|
height: 4,
|
||||||
|
backgroundColor: "#5865f2",
|
||||||
|
borderRadius: 2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</RN.View>
|
||||||
|
<RN.Text
|
||||||
|
style={{
|
||||||
|
color: "#72767d",
|
||||||
|
fontSize: 12,
|
||||||
|
minWidth: 70,
|
||||||
|
textAlign: "right",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formatTime(progressData.current)} /{" "}
|
||||||
|
{formatTime(progressData.total)}
|
||||||
|
</RN.Text>
|
||||||
|
</RN.View>
|
||||||
|
)}
|
||||||
|
</RN.View>
|
||||||
|
</RN.View>
|
||||||
|
</RN.View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Last.fm Settings Page
|
// Last.fm Settings Page
|
||||||
function LastFmSettingsPage() {
|
function LastFmSettingsPage() {
|
||||||
useProxy(plugin.storage);
|
useProxy(plugin.storage);
|
||||||
|
|
@ -323,7 +707,36 @@ function DisplaySettingsPage() {
|
||||||
</Stack>
|
</Stack>
|
||||||
</TableRowGroup>
|
</TableRowGroup>
|
||||||
|
|
||||||
<TableRowGroup title="Activity Options">
|
<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>
|
||||||
|
<RN.View style={{ height: 64 }} />
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// RPC Customization Settings Page
|
||||||
|
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
|
<TableSwitchRow
|
||||||
label="Show as Listening"
|
label="Show as Listening"
|
||||||
subLabel="Display as 'Listening to' instead of 'Playing'"
|
subLabel="Display as 'Listening to' instead of 'Playing'"
|
||||||
|
|
@ -335,7 +748,7 @@ function DisplaySettingsPage() {
|
||||||
/>
|
/>
|
||||||
<TableSwitchRow
|
<TableSwitchRow
|
||||||
label="Show Tooltip Text"
|
label="Show Tooltip Text"
|
||||||
subLabel="Show album name and/or track duration in Discord activity tooltip"
|
subLabel="Show album name and track duration in Discord activity tooltip"
|
||||||
value={get("showLargeText", true)}
|
value={get("showLargeText", true)}
|
||||||
onValueChange={(value: boolean) => {
|
onValueChange={(value: boolean) => {
|
||||||
set("showLargeText", value);
|
set("showLargeText", value);
|
||||||
|
|
@ -354,6 +767,24 @@ function DisplaySettingsPage() {
|
||||||
forceUpdate();
|
forceUpdate();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<TableSwitchRow
|
||||||
|
label="Show Album in Tooltip"
|
||||||
|
subLabel="Include album name in the tooltip text"
|
||||||
|
value={get("showAlbumInTooltip", true)}
|
||||||
|
onValueChange={(value: boolean) => {
|
||||||
|
set("showAlbumInTooltip", value);
|
||||||
|
forceUpdate();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TableSwitchRow
|
||||||
|
label="Show Duration in Tooltip"
|
||||||
|
subLabel="Include track duration in the tooltip text"
|
||||||
|
value={get("showDurationInTooltip", true)}
|
||||||
|
onValueChange={(value: boolean) => {
|
||||||
|
set("showDurationInTooltip", value);
|
||||||
|
forceUpdate();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</TableRowGroup>
|
</TableRowGroup>
|
||||||
</Stack>
|
</Stack>
|
||||||
<RN.View style={{ height: 64 }} />
|
<RN.View style={{ height: 64 }} />
|
||||||
|
|
@ -624,7 +1055,7 @@ export default function Settings() {
|
||||||
<TableRowGroup title="Plugin Configuration">
|
<TableRowGroup title="Plugin Configuration">
|
||||||
<TableRow
|
<TableRow
|
||||||
label="Display Settings"
|
label="Display Settings"
|
||||||
subLabel="Customize how music activity appears"
|
subLabel="Customize app name and update interval"
|
||||||
trailing={<TableRow.Arrow />}
|
trailing={<TableRow.Arrow />}
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
navigation.push("VendettaCustomPage", {
|
navigation.push("VendettaCustomPage", {
|
||||||
|
|
@ -633,6 +1064,17 @@ export default function Settings() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<TableRow
|
||||||
|
label="RPC Customization"
|
||||||
|
subLabel="Customize Discord rich presence display options"
|
||||||
|
trailing={<TableRow.Arrow />}
|
||||||
|
onPress={() =>
|
||||||
|
navigation.push("VendettaCustomPage", {
|
||||||
|
title: "RPC Customization",
|
||||||
|
render: RPCCustomizationSettingsPage,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
<TableRow
|
<TableRow
|
||||||
label="Ignore List"
|
label="Ignore List"
|
||||||
subLabel="Configure apps that should hide your status"
|
subLabel="Configure apps that should hide your status"
|
||||||
|
|
@ -657,32 +1099,11 @@ export default function Settings() {
|
||||||
/>
|
/>
|
||||||
<TableSwitchRow
|
<TableSwitchRow
|
||||||
label="Add to Sidebar"
|
label="Add to Sidebar"
|
||||||
subLabel="Show plugin in Discord settings (may crash Discord)"
|
subLabel="Show plugin in Discord settings"
|
||||||
value={get("addToSidebar", false)}
|
value={get("addToSidebar", false)}
|
||||||
onValueChange={(value: boolean) => {
|
onValueChange={(value: boolean) => {
|
||||||
if (value) {
|
set("addToSidebar", value);
|
||||||
const { showConfirmationAlert } = window;
|
forceUpdate();
|
||||||
showConfirmationAlert({
|
|
||||||
title: "Warning: Potential Discord Crash",
|
|
||||||
content:
|
|
||||||
"Enabling 'Add to Sidebar' may cause Discord to crash or become unstable. This feature is experimental and not recommended.",
|
|
||||||
confirmText: "Enable Anyway",
|
|
||||||
cancelText: "Cancel",
|
|
||||||
confirmColor: "red",
|
|
||||||
onConfirm: () => {
|
|
||||||
set("addToSidebar", true);
|
|
||||||
forceUpdate();
|
|
||||||
showToast(
|
|
||||||
"Sidebar feature enabled - use with caution",
|
|
||||||
getAssetIDByName("Warning"),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onCancel: () => forceUpdate(),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
set("addToSidebar", false);
|
|
||||||
forceUpdate();
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</TableRowGroup>
|
</TableRowGroup>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue