From 735f8aba4c6c65908a327774e77cf2ae06b99330 Mon Sep 17 00:00:00 2001
From: kmmiio99o <164557248+kmmiio99o@users.noreply.github.com>
Date: Sat, 4 Oct 2025 20:14:14 +0200
Subject: [PATCH] fix lint and most of errors in terminal
---
.eslintrc.json | 72 +-
package.json | 27 +-
plugins/Commands/settings.tsx | 2506 ++++++++---------
plugins/Commands/src/commands/lovefemboys.ts | 284 +-
plugins/Commands/src/utils/api.ts | 97 +-
plugins/multi-scrobbler/src/constants.ts | 140 +-
plugins/multi-scrobbler/src/index.tsx | 326 +--
plugins/multi-scrobbler/src/manager.ts | 660 ++---
.../src/services/BaseService.ts | 132 +-
.../src/services/LastFmService.ts | 236 +-
.../src/services/LibreFmService.ts | 242 +-
.../src/services/ListenBrainzService.ts | 284 +-
.../src/services/ServiceFactory.ts | 138 +-
plugins/multi-scrobbler/src/sidebar.tsx | 304 +-
.../multi-scrobbler/src/ui/pages/Settings.tsx | 968 +++----
plugins/multi-scrobbler/src/utils/activity.ts | 90 +-
plugins/multi-scrobbler/src/utils/debug.ts | 246 +-
plugins/multi-scrobbler/src/utils/time.ts | 128 +-
18 files changed, 3441 insertions(+), 3439 deletions(-)
diff --git a/.eslintrc.json b/.eslintrc.json
index a128ed3..e0ceb91 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -1,44 +1,36 @@
{
- "env": {
- "es2021": true,
- "node": true
+ "env": {
+ "es2021": true,
+ "node": true
+ },
+ "extends": [
+ "eslint:recommended",
+ "plugin:react/recommended",
+ "plugin:@typescript-eslint/recommended"
+ ],
+ "parser": "@typescript-eslint/parser",
+ "parserOptions": {
+ "ecmaFeatures": {
+ "jsx": true
},
- "extends": [
- "eslint:recommended",
- "plugin:react/recommended",
- "plugin:@typescript-eslint/recommended"
- ],
- "parser": "@typescript-eslint/parser",
- "parserOptions": {
- "ecmaFeatures": {
- "jsx": true
- },
- "ecmaVersion": "latest",
- "sourceType": "module"
- },
- "plugins": [
- "react",
- "@typescript-eslint"
- ],
- "rules": {
- "indent": [
- "error",
- 4,
- { "SwitchCase": 1 }
- ],
- "quotes": [
- "error",
- "double"
- ],
- "semi": [
- "error",
- "always"
- ],
- "@typescript-eslint/no-explicit-any": "off",
- "@typescript-eslint/ban-ts-comment": "off",
- "@typescript-eslint/triple-slash-reference": "off",
- "@typescript-eslint/no-non-null-assertion": "off",
- "react/react-in-jsx-scope": "off",
- "eol-last": "error"
+ "ecmaVersion": "latest",
+ "sourceType": "module"
+ },
+ "plugins": ["react", "@typescript-eslint"],
+ "settings": {
+ "react": {
+ "version": "detect"
}
+ },
+ "rules": {
+ "indent": ["error", 4, { "SwitchCase": 1 }],
+ "quotes": ["error", "double"],
+ "semi": ["error", "always"],
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/ban-ts-comment": "off",
+ "@typescript-eslint/triple-slash-reference": "off",
+ "@typescript-eslint/no-non-null-assertion": "off",
+ "react/react-in-jsx-scope": "off",
+ "eol-last": "error"
+ }
}
diff --git a/package.json b/package.json
index 750821e..9343568 100644
--- a/package.json
+++ b/package.json
@@ -7,23 +7,26 @@
"serve": "node serve.mjs"
},
"devDependencies": {
- "@rollup/plugin-commonjs": "^24.1.0",
- "@rollup/plugin-node-resolve": "^15.0.2",
- "@swc/core": "^1.3.51",
+ "@rollup/plugin-commonjs": "^28.0.6",
+ "@rollup/plugin-node-resolve": "^16.0.2",
+ "@swc/core": "^1.13.5",
"@types/react": "^18.0.37",
"@types/react-native": "^0.70.13",
- "@typescript-eslint/eslint-plugin": "^5.59.0",
- "@typescript-eslint/parser": "^5.59.0",
- "esbuild": "^0.17.17",
- "eslint": "^8.38.0",
+ "@typescript-eslint/eslint-plugin": "^8.45.0",
+ "@typescript-eslint/parser": "^8.45.0",
+ "esbuild": "^0.25.10",
+ "eslint": "^9.37.0",
"eslint-plugin-react": "^7.32.2",
- "rollup": "^3.20.6",
- "rollup-plugin-esbuild": "^5.0.0",
- "typescript": "^4.9.5",
- "vendetta-types": "^2.4.18"
+ "rollup": "^4.24.4",
+ "rollup-plugin-esbuild": "^6.1.1",
+ "typescript": "^5.7.2",
+ "vendetta-types": "^2.4.21"
},
"dependencies": {
- "@swc/helpers": "^0.5.0",
+ "@swc/helpers": "^0.5.17",
"serve": "^14.2.5"
+ },
+ "overrides": {
+ "@types/react": "^18.0.37"
}
}
diff --git a/plugins/Commands/settings.tsx b/plugins/Commands/settings.tsx
index 6fc6ca9..64617be 100644
--- a/plugins/Commands/settings.tsx
+++ b/plugins/Commands/settings.tsx
@@ -13,1401 +13,1401 @@ const { FormRow, FormSwitchRow } = Forms;
// Initialize storage with default values
storage.factSettings ??= {
- sendAsReply: true,
- includeCitation: false,
+ sendAsReply: true,
+ includeCitation: false,
};
storage.listSettings ??= {
- pluginListAlwaysDetailed: false,
- themeListAlwaysDetailed: false,
+ pluginListAlwaysDetailed: false,
+ themeListAlwaysDetailed: false,
};
storage.garySettings ??= {
- imageSource: "gary",
+ imageSource: "gary",
};
storage.enabledCommands ??= {
- catfact: true,
- dogfact: true,
- useless: true,
- petpet: true,
- pluginList: true,
- themeList: true,
- konoself: true,
- konosend: true,
- firstmessage: true,
- sysinfo: true,
- spotifyTrack: true,
- spotifyAlbum: true,
- spotifyArtists: true,
- spotifyCover: true,
- gary: true,
- ip: true, // IP command - enabled by default
- lovefemboys: false, // Hidden command - disabled by default
- nekoslife: false, // Hidden command - disabled by default
+ catfact: true,
+ dogfact: true,
+ useless: true,
+ petpet: true,
+ pluginList: true,
+ themeList: true,
+ konoself: true,
+ konosend: true,
+ firstmessage: true,
+ sysinfo: true,
+ spotifyTrack: true,
+ spotifyAlbum: true,
+ spotifyArtists: true,
+ spotifyCover: true,
+ gary: true,
+ ip: true, // IP command - enabled by default
+ lovefemboys: false, // Hidden command - disabled by default
+ nekoslife: false, // Hidden command - disabled by default
};
storage.pendingRestart ??= false;
// Hidden settings storage with NSFW bypass option
storage.hiddenSettings ??= {
- enabled: false,
- visible: false,
- konochanBypassNsfw: false, // New bypass option - disabled by default
+ enabled: false,
+ visible: false,
+ konochanBypassNsfw: false, // New bypass option - disabled by default
};
// Better Table Row Group Component
function BetterTableRowGroup({
- title,
- icon,
- children,
- padding = false,
+ title,
+ icon,
+ children,
+ padding = false,
}: React.PropsWithChildren<{
title?: string;
icon?: number;
padding?: boolean;
}>) {
- const styles = stylesheet.createThemedStyleSheet({
- main: {
- backgroundColor: semanticColors.CARD_PRIMARY_BG,
- borderColor: semanticColors.BORDER_FAINT,
- borderWidth: 1,
- borderRadius: 16,
- overflow: "hidden",
- flex: 1,
- },
- titleContainer: {
- marginBottom: 8,
- marginHorizontal: 0,
- marginTop: 8,
- gap: 4,
- flexDirection: "row",
- alignItems: "center",
- },
- icon: {
- width: 16,
- height: 16,
- marginTop: 1.5,
- tintColor: semanticColors.TEXT_MUTED,
- },
- titleText: {
- fontSize: 14,
- fontWeight: "600",
- color: semanticColors.TEXT_MUTED,
- },
- });
+ const styles = stylesheet.createThemedStyleSheet({
+ main: {
+ backgroundColor: semanticColors.CARD_PRIMARY_BG,
+ borderColor: semanticColors.BORDER_FAINT,
+ borderWidth: 1,
+ borderRadius: 16,
+ overflow: "hidden",
+ flex: 1,
+ },
+ titleContainer: {
+ marginBottom: 8,
+ marginHorizontal: 0,
+ marginTop: 8,
+ gap: 4,
+ flexDirection: "row",
+ alignItems: "center",
+ },
+ icon: {
+ width: 16,
+ height: 16,
+ marginTop: 1.5,
+ tintColor: semanticColors.TEXT_MUTED,
+ },
+ titleText: {
+ fontSize: 14,
+ fontWeight: "600",
+ color: semanticColors.TEXT_MUTED,
+ },
+ });
- return (
-
- {title && (
-
- {icon && (
-
- )}
- {title.toUpperCase()}
+ return (
+
+ {title && (
+
+ {icon && (
+
+ )}
+ {title.toUpperCase()}
+
+ )}
+
+ {padding ? (
+
+ {children}
+
+ ) : (
+ children
+ )}
+
- )}
-
- {padding ? (
-
- {children}
-
- ) : (
- children
- )}
-
-
- );
+ );
}
// Header Component with secret unlock
function Header({ onHiddenUnlock }: { onHiddenUnlock?: () => void }) {
- const [clickCounter, setClickCounter] = React.useState(0);
- const [clickTimeout, setClickTimeout] = React.useState(
- null,
- );
-
- const styles = stylesheet.createThemedStyleSheet({
- container: {
- flexDirection: "column",
- alignItems: "center",
- paddingVertical: 24,
- paddingHorizontal: 16,
- },
- title: {
- fontSize: 28,
- fontWeight: "700",
- color: semanticColors.TEXT_NORMAL,
- marginBottom: 4,
- },
- subtitle: {
- fontSize: 16,
- fontWeight: "500",
- color: semanticColors.TEXT_MUTED,
- },
- iconContainer: {
- backgroundColor: semanticColors.BACKGROUND_SECONDARY,
- borderRadius: 16,
- padding: 12,
- marginBottom: 16,
- },
- icon: {
- width: 32,
- height: 32,
- tintColor: semanticColors.TEXT_NORMAL,
- },
- });
-
- const handleIconPress = () => {
- if (storage.hiddenSettings.enabled) {
- storage.hiddenSettings.visible = !storage.hiddenSettings.visible;
- showToast(
- `Hidden settings ${storage.hiddenSettings.visible ? "visible" : "hidden"}`,
- getAssetIDByName("SettingsIcon"),
- );
- onHiddenUnlock?.();
- return;
- }
-
- if (clickTimeout) {
- clearTimeout(clickTimeout);
- }
-
- const newTimeout = setTimeout(() => {
- setClickCounter(0);
- }, 1000);
- setClickTimeout(newTimeout);
-
- const newCounter = clickCounter + 1;
- setClickCounter(newCounter);
-
- if (newCounter < 10) {
- return;
- }
-
- showToast(
- "🔓 Hidden settings unlocked!",
- getAssetIDByName("CheckmarkIcon"),
+ const [clickCounter, setClickCounter] = React.useState(0);
+ const [clickTimeout, setClickTimeout] = React.useState(
+ null,
);
- storage.hiddenSettings.enabled = true;
- storage.hiddenSettings.visible = true;
- setClickCounter(0);
- onHiddenUnlock?.();
- };
+ const styles = stylesheet.createThemedStyleSheet({
+ container: {
+ flexDirection: "column",
+ alignItems: "center",
+ paddingVertical: 24,
+ paddingHorizontal: 16,
+ },
+ title: {
+ fontSize: 28,
+ fontWeight: "700",
+ color: semanticColors.TEXT_NORMAL,
+ marginBottom: 4,
+ },
+ subtitle: {
+ fontSize: 16,
+ fontWeight: "500",
+ color: semanticColors.TEXT_MUTED,
+ },
+ iconContainer: {
+ backgroundColor: semanticColors.BACKGROUND_SECONDARY,
+ borderRadius: 16,
+ padding: 12,
+ marginBottom: 16,
+ },
+ icon: {
+ width: 32,
+ height: 32,
+ tintColor: semanticColors.TEXT_NORMAL,
+ },
+ });
- return (
-
-
-
-
- Commands
- A collection of useful commands
-
- );
+ const handleIconPress = () => {
+ if (storage.hiddenSettings.enabled) {
+ storage.hiddenSettings.visible = !storage.hiddenSettings.visible;
+ showToast(
+ `Hidden settings ${storage.hiddenSettings.visible ? "visible" : "hidden"}`,
+ getAssetIDByName("SettingsIcon"),
+ );
+ onHiddenUnlock?.();
+ return;
+ }
+
+ if (clickTimeout) {
+ clearTimeout(clickTimeout);
+ }
+
+ const newTimeout = setTimeout(() => {
+ setClickCounter(0);
+ }, 1000);
+ setClickTimeout(newTimeout);
+
+ const newCounter = clickCounter + 1;
+ setClickCounter(newCounter);
+
+ if (newCounter < 10) {
+ return;
+ }
+
+ showToast(
+ "🔓 Hidden settings unlocked!",
+ getAssetIDByName("CheckmarkIcon"),
+ );
+ storage.hiddenSettings.enabled = true;
+ storage.hiddenSettings.visible = true;
+ setClickCounter(0);
+
+ onHiddenUnlock?.();
+ };
+
+ return (
+
+
+
+
+ Commands
+ A collection of useful commands
+
+ );
}
// NekosLife Categories Reference Page - SFW ONLY
function NekosLifeCategoriesPage() {
- const styles = stylesheet.createThemedStyleSheet({
- container: {
- flex: 1,
- backgroundColor: semanticColors.BACKGROUND_PRIMARY,
- },
- tableHeader: {
- flexDirection: "row",
- alignItems: "center",
- paddingVertical: 12,
- paddingHorizontal: 16,
- backgroundColor: semanticColors.BACKGROUND_MODIFIER_ACCENT,
- borderBottomWidth: 2,
- borderBottomColor: semanticColors.BORDER_STRONG,
- },
- headerText: {
- fontSize: 14,
- fontWeight: "700",
- color: semanticColors.TEXT_NORMAL,
- textTransform: "uppercase",
- },
- nameHeader: {
- flex: 2,
- },
- codeHeader: {
- flex: 2,
- textAlign: "center",
- },
- badgeHeader: {
- flex: 1,
- textAlign: "center",
- },
- categoryItem: {
- flexDirection: "row",
- alignItems: "center",
- paddingVertical: 12,
- paddingHorizontal: 16,
- borderBottomWidth: 1,
- borderBottomColor: semanticColors.BORDER_FAINT,
- },
- categoryName: {
- fontSize: 16,
- fontWeight: "500",
- color: semanticColors.TEXT_NORMAL,
- flex: 2,
- },
- categoryValue: {
- fontSize: 14,
- color: semanticColors.TEXT_MUTED,
- fontFamily: "monospace",
- flex: 2,
- textAlign: "center",
- },
- sfwBadgeContainer: {
- flex: 1,
- alignItems: "center",
- },
- sfwBadge: {
- backgroundColor: semanticColors.STATUS_POSITIVE,
- paddingHorizontal: 8,
- paddingVertical: 4,
- borderRadius: 12,
- },
- badgeText: {
- fontSize: 12,
- fontWeight: "600",
- color: semanticColors.WHITE,
- },
- infoText: {
- fontSize: 14,
- color: semanticColors.TEXT_MUTED,
- textAlign: "center",
- lineHeight: 20,
- },
- });
+ const styles = stylesheet.createThemedStyleSheet({
+ container: {
+ flex: 1,
+ backgroundColor: semanticColors.BACKGROUND_PRIMARY,
+ },
+ tableHeader: {
+ flexDirection: "row",
+ alignItems: "center",
+ paddingVertical: 12,
+ paddingHorizontal: 16,
+ backgroundColor: semanticColors.BACKGROUND_MODIFIER_ACCENT,
+ borderBottomWidth: 2,
+ borderBottomColor: semanticColors.BORDER_STRONG,
+ },
+ headerText: {
+ fontSize: 14,
+ fontWeight: "700",
+ color: semanticColors.TEXT_NORMAL,
+ textTransform: "uppercase",
+ },
+ nameHeader: {
+ flex: 2,
+ },
+ codeHeader: {
+ flex: 2,
+ textAlign: "center",
+ },
+ badgeHeader: {
+ flex: 1,
+ textAlign: "center",
+ },
+ categoryItem: {
+ flexDirection: "row",
+ alignItems: "center",
+ paddingVertical: 12,
+ paddingHorizontal: 16,
+ borderBottomWidth: 1,
+ borderBottomColor: semanticColors.BORDER_FAINT,
+ },
+ categoryName: {
+ fontSize: 16,
+ fontWeight: "500",
+ color: semanticColors.TEXT_NORMAL,
+ flex: 2,
+ },
+ categoryValue: {
+ fontSize: 14,
+ color: semanticColors.TEXT_MUTED,
+ fontFamily: "monospace",
+ flex: 2,
+ textAlign: "center",
+ },
+ sfwBadgeContainer: {
+ flex: 1,
+ alignItems: "center",
+ },
+ sfwBadge: {
+ backgroundColor: semanticColors.STATUS_POSITIVE,
+ paddingHorizontal: 8,
+ paddingVertical: 4,
+ borderRadius: 12,
+ },
+ badgeText: {
+ fontSize: 12,
+ fontWeight: "600",
+ color: semanticColors.WHITE,
+ },
+ infoText: {
+ fontSize: 14,
+ color: semanticColors.TEXT_MUTED,
+ textAlign: "center",
+ lineHeight: 20,
+ },
+ });
- // Only SFW nekos.life categories
- const sfwCategories = [
- { name: "Avatar", value: "avatar" },
- { name: "Classic", value: "classic" },
- { name: "Cuddle", value: "cuddle" },
- { name: "Fox Girl", value: "fox_girl" },
- { name: "Gecg", value: "gecg" },
- { name: "Holo", value: "holo" },
- { name: "Kemonomimi", value: "kemonomimi" },
- { name: "Kiss", value: "kiss" },
- { name: "Neko", value: "neko" },
- { name: "Neko GIF", value: "ngif" },
- { name: "Smug", value: "smug" },
- { name: "Spank", value: "spank" },
- { name: "Tickle", value: "tickle" },
- { name: "Waifu", value: "waifu" },
- { name: "Wallpaper", value: "wallpaper" },
- { name: "Woof", value: "woof" },
- ];
+ // Only SFW nekos.life categories
+ const sfwCategories = [
+ { name: "Avatar", value: "avatar" },
+ { name: "Classic", value: "classic" },
+ { name: "Cuddle", value: "cuddle" },
+ { name: "Fox Girl", value: "fox_girl" },
+ { name: "Gecg", value: "gecg" },
+ { name: "Holo", value: "holo" },
+ { name: "Kemonomimi", value: "kemonomimi" },
+ { name: "Kiss", value: "kiss" },
+ { name: "Neko", value: "neko" },
+ { name: "Neko GIF", value: "ngif" },
+ { name: "Smug", value: "smug" },
+ { name: "Spank", value: "spank" },
+ { name: "Tickle", value: "tickle" },
+ { name: "Waifu", value: "waifu" },
+ { name: "Wallpaper", value: "wallpaper" },
+ { name: "Woof", value: "woof" },
+ ];
- // Sort categories alphabetically
- const sortedCategories = sfwCategories.sort((a, b) =>
- a.name.localeCompare(b.name),
- );
+ // Sort categories alphabetically
+ const sortedCategories = sfwCategories.sort((a, b) =>
+ a.name.localeCompare(b.name),
+ );
- return (
-
-
-
-
+ return (
+
+
+
+
Total SFW Categories: {sfwCategories.length}
- {"\n\n"}
+ {"\n\n"}
Use these category names with the /nekoslife command.{"\n"}
All categories are Safe For Work (SFW).
-
-
+
+
-
- {/* Table Header */}
-
-
+
+ {/* Table Header */}
+
+
Category Name
-
-
+
+
Command Code
-
-
+
+
Type
-
-
+
+
- {/* Table Rows */}
- {sortedCategories.map((category, index) => (
-
- {category.name}
- {category.value}
-
-
- SFW
-
-
+ {/* Table Rows */}
+ {sortedCategories.map((category, index) => (
+
+ {category.name}
+ {category.value}
+
+
+ SFW
+
+
+
+ ))}
+
- ))}
-
-
-
- );
+
+ );
}
// Hidden Commands Settings Page
function HiddenSettingsPage() {
- useProxy(storage);
- const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
- const navigation = NavigationNative.useNavigation();
+ useProxy(storage);
+ const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
+ const navigation = NavigationNative.useNavigation();
- const styles = stylesheet.createThemedStyleSheet({
- container: {
- flex: 1,
- backgroundColor: semanticColors.BACKGROUND_PRIMARY,
- },
- warningText: {
- fontSize: 14,
- color: semanticColors.TEXT_DANGER,
- textAlign: "center",
- lineHeight: 20,
- fontWeight: "600",
- },
- infoText: {
- fontSize: 14,
- color: semanticColors.TEXT_MUTED,
- textAlign: "center",
- lineHeight: 20,
- marginTop: 8,
- },
- });
+ const styles = stylesheet.createThemedStyleSheet({
+ container: {
+ flex: 1,
+ backgroundColor: semanticColors.BACKGROUND_PRIMARY,
+ },
+ warningText: {
+ fontSize: 14,
+ color: semanticColors.TEXT_DANGER,
+ textAlign: "center",
+ lineHeight: 20,
+ fontWeight: "600",
+ },
+ infoText: {
+ fontSize: 14,
+ color: semanticColors.TEXT_MUTED,
+ textAlign: "center",
+ lineHeight: 20,
+ marginTop: 8,
+ },
+ });
- return (
-
-
-
-
+ return (
+
+
+
+
These are hidden commands that may contain mature content or
experimental features.
-
-
+
+
Use at your own discretion. Commands in this section are disabled by
default.
-
-
+
+
-
- }
- value={storage.enabledCommands.lovefemboys}
- onValueChange={(v) => {
- storage.enabledCommands.lovefemboys = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
+
+ }
+ value={storage.enabledCommands.lovefemboys}
+ onValueChange={(v) => {
+ storage.enabledCommands.lovefemboys = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+
-
- }
- value={storage.hiddenSettings.konochanBypassNsfw}
- onValueChange={(v) => {
- if (v) {
- alerts.showConfirmationAlert({
- title: "⚠️ NSFW Bypass Warning",
- content:
+
+ }
+ value={storage.hiddenSettings.konochanBypassNsfw}
+ onValueChange={(v) => {
+ if (v) {
+ alerts.showConfirmationAlert({
+ title: "⚠️ NSFW Bypass Warning",
+ content:
"Enabling this allows NSFW content from KonoChan to be sent in any channel, including non-NSFW channels. This could violate server rules or Discord ToS. Use responsibly!",
- confirmText: "I Understand",
- cancelText: "Cancel",
- onConfirm: () => {
- storage.hiddenSettings.konochanBypassNsfw = true;
- forceUpdate();
- },
- onCancel: () => {
- // Do nothing - keep it disabled
- },
- });
- } else {
- storage.hiddenSettings.konochanBypassNsfw = false;
- forceUpdate();
- }
- }}
- />
-
+ confirmText: "I Understand",
+ cancelText: "Cancel",
+ onConfirm: () => {
+ storage.hiddenSettings.konochanBypassNsfw = true;
+ forceUpdate();
+ },
+ onCancel: () => {
+ // Do nothing - keep it disabled
+ },
+ });
+ } else {
+ storage.hiddenSettings.konochanBypassNsfw = false;
+ forceUpdate();
+ }
+ }}
+ />
+
-
- }
- value={storage.hiddenSettings.visible}
- onValueChange={(v) => {
- storage.hiddenSettings.visible = v;
- }}
- />
- }
- onPress={() => {
- alerts.showConfirmationAlert({
- title: "Reset Hidden Settings",
- content:
+
+ }
+ value={storage.hiddenSettings.visible}
+ onValueChange={(v) => {
+ storage.hiddenSettings.visible = v;
+ }}
+ />
+ }
+ onPress={() => {
+ alerts.showConfirmationAlert({
+ title: "Reset Hidden Settings",
+ content:
"This will hide the hidden settings section and disable all hidden commands. Are you sure?",
- confirmText: "Reset",
- onConfirm: () => {
- storage.hiddenSettings.enabled = false;
- storage.hiddenSettings.visible = false;
- storage.hiddenSettings.konochanBypassNsfw = false;
- storage.enabledCommands.lovefemboys = false;
+ confirmText: "Reset",
+ onConfirm: () => {
+ storage.hiddenSettings.enabled = false;
+ storage.hiddenSettings.visible = false;
+ storage.hiddenSettings.konochanBypassNsfw = false;
+ storage.enabledCommands.lovefemboys = false;
- storage.pendingRestart = true;
- showToast(
- "Hidden settings reset",
- getAssetIDByName("CheckmarkIcon"),
- );
- },
- cancelText: "Cancel",
- });
- }}
- />
-
-
-
- );
+ storage.pendingRestart = true;
+ showToast(
+ "Hidden settings reset",
+ getAssetIDByName("CheckmarkIcon"),
+ );
+ },
+ cancelText: "Cancel",
+ });
+ }}
+ />
+
+
+
+ );
}
// Facts Commands Settings Page
function FactsSettingsPage() {
- useProxy(storage);
- const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
+ useProxy(storage);
+ const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
- return (
-
-
-
-
- }
- value={storage.factSettings.sendAsReply}
- onValueChange={(v) => {
- storage.factSettings.sendAsReply = v;
- }}
- />
- }
- value={storage.factSettings.includeCitation}
- onValueChange={(v) => {
- storage.factSettings.includeCitation = v;
- }}
- />
-
+
+
+
+ }
+ value={storage.factSettings.sendAsReply}
+ onValueChange={(v) => {
+ storage.factSettings.sendAsReply = v;
+ }}
+ />
+ }
+ value={storage.factSettings.includeCitation}
+ onValueChange={(v) => {
+ storage.factSettings.includeCitation = v;
+ }}
+ />
+
-
-
- }
- value={storage.enabledCommands.catfact}
- onValueChange={(v) => {
- storage.enabledCommands.catfact = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
- }
- value={storage.enabledCommands.dogfact}
- onValueChange={(v) => {
- storage.enabledCommands.dogfact = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
- }
- value={storage.enabledCommands.useless}
- onValueChange={(v) => {
- storage.enabledCommands.useless = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
-
-
- );
+
+
+ }
+ value={storage.enabledCommands.catfact}
+ onValueChange={(v) => {
+ storage.enabledCommands.catfact = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+
+ }
+ value={storage.enabledCommands.dogfact}
+ onValueChange={(v) => {
+ storage.enabledCommands.dogfact = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+
+ }
+ value={storage.enabledCommands.useless}
+ onValueChange={(v) => {
+ storage.enabledCommands.useless = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+
+
+
+ );
}
// Gary API Settings Page
function GaryAPIPage() {
- useProxy(storage);
- const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
+ useProxy(storage);
+ const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
- const styles = stylesheet.createThemedStyleSheet({
- container: {
- flex: 1,
- backgroundColor: semanticColors.BACKGROUND_PRIMARY,
- },
- infoText: {
- fontSize: 14,
- color: semanticColors.TEXT_MUTED,
- marginBottom: 16,
- textAlign: "center",
- },
- currentText: {
- fontSize: 14,
- color: semanticColors.TEXT_NORMAL,
- textAlign: "center",
- fontWeight: "600",
- },
- });
+ const styles = stylesheet.createThemedStyleSheet({
+ container: {
+ flex: 1,
+ backgroundColor: semanticColors.BACKGROUND_PRIMARY,
+ },
+ infoText: {
+ fontSize: 14,
+ color: semanticColors.TEXT_MUTED,
+ marginBottom: 16,
+ textAlign: "center",
+ },
+ currentText: {
+ fontSize: 14,
+ color: semanticColors.TEXT_NORMAL,
+ textAlign: "center",
+ fontWeight: "600",
+ },
+ });
- const currentSource = storage.garySettings.imageSource;
+ const currentSource = storage.garySettings.imageSource;
- return (
-
-
-
-
- }
- value={storage.enabledCommands.gary}
- onValueChange={(v) => {
- storage.enabledCommands.gary = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
+ return (
+
+
+
+
+ }
+ value={storage.enabledCommands.gary}
+ onValueChange={(v) => {
+ storage.enabledCommands.gary = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+
-
-
+
+
Choose which API the /gary command should use to fetch images.
-
-
+
+
-
- }
- value={currentSource === "gary"}
- onValueChange={(v) => {
- if (v) {
- storage.garySettings.imageSource = "gary";
- forceUpdate();
- }
- }}
- />
- }
- value={currentSource === "catapi"}
- onValueChange={(v) => {
- if (v) {
- storage.garySettings.imageSource = "catapi";
- forceUpdate();
- }
- }}
- />
- }
- value={currentSource === "minker"}
- onValueChange={(v) => {
- if (v) {
- storage.garySettings.imageSource = "minker";
- forceUpdate();
- }
- }}
- />
- }
- value={currentSource === "goober"}
- onValueChange={(v) => {
- if (v) {
- storage.garySettings.imageSource = "goober";
- forceUpdate();
- }
- }}
- />
-
+
+ }
+ value={currentSource === "gary"}
+ onValueChange={(v) => {
+ if (v) {
+ storage.garySettings.imageSource = "gary";
+ forceUpdate();
+ }
+ }}
+ />
+ }
+ value={currentSource === "catapi"}
+ onValueChange={(v) => {
+ if (v) {
+ storage.garySettings.imageSource = "catapi";
+ forceUpdate();
+ }
+ }}
+ />
+ }
+ value={currentSource === "minker"}
+ onValueChange={(v) => {
+ if (v) {
+ storage.garySettings.imageSource = "minker";
+ forceUpdate();
+ }
+ }}
+ />
+ }
+ value={currentSource === "goober"}
+ onValueChange={(v) => {
+ if (v) {
+ storage.garySettings.imageSource = "goober";
+ forceUpdate();
+ }
+ }}
+ />
+
-
-
+
+
Currently using:{" "}
- {currentSource === "gary"
- ? "Gary API"
- : currentSource === "catapi"
- ? "Cat API"
- : currentSource === "minker"
- ? "Minker API"
- : currentSource === "goober"
- ? "Goober API"
- : "Gary API"}
-
-
-
-
- );
+ {currentSource === "gary"
+ ? "Gary API"
+ : currentSource === "catapi"
+ ? "Cat API"
+ : currentSource === "minker"
+ ? "Minker API"
+ : currentSource === "goober"
+ ? "Goober API"
+ : "Gary API"}
+
+
+
+
+ );
}
// List Settings Page
function ListSettingsPage() {
- useProxy(storage);
- const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
+ useProxy(storage);
+ const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
- return (
-
-
-
-
- }
- value={storage.listSettings.pluginListAlwaysDetailed}
- onValueChange={(v) => {
- storage.listSettings.pluginListAlwaysDetailed = v;
- }}
- />
-
- }
- value={storage.listSettings.themeListAlwaysDetailed}
- onValueChange={(v) => {
- storage.listSettings.themeListAlwaysDetailed = v;
- }}
- />
-
+
+
+
+ }
+ value={storage.listSettings.pluginListAlwaysDetailed}
+ onValueChange={(v) => {
+ storage.listSettings.pluginListAlwaysDetailed = v;
+ }}
+ />
+
+ }
+ value={storage.listSettings.themeListAlwaysDetailed}
+ onValueChange={(v) => {
+ storage.listSettings.themeListAlwaysDetailed = v;
+ }}
+ />
+
-
-
- }
- value={storage.enabledCommands.pluginList}
- onValueChange={(v) => {
- storage.enabledCommands.pluginList = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
- }
- value={storage.enabledCommands.themeList}
- onValueChange={(v) => {
- storage.enabledCommands.themeList = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
-
-
- );
+
+
+ }
+ value={storage.enabledCommands.pluginList}
+ onValueChange={(v) => {
+ storage.enabledCommands.pluginList = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+
+ }
+ value={storage.enabledCommands.themeList}
+ onValueChange={(v) => {
+ storage.enabledCommands.themeList = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+
+
+
+ );
}
// Image Settings Page
function ImageSettingsPage() {
- useProxy(storage);
- const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
+ useProxy(storage);
+ const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
- return (
-
-
-
-
- }
- value={storage.enabledCommands.petpet}
- onValueChange={(v) => {
- storage.enabledCommands.petpet = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
+
+
+
+ }
+ value={storage.enabledCommands.petpet}
+ onValueChange={(v) => {
+ storage.enabledCommands.petpet = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+
-
- }
- value={storage.enabledCommands.konoself}
- onValueChange={(v) => {
- storage.enabledCommands.konoself = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
- }
- value={storage.enabledCommands.konosend}
- onValueChange={(v) => {
- storage.enabledCommands.konosend = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
-
-
- );
+
+ }
+ value={storage.enabledCommands.konoself}
+ onValueChange={(v) => {
+ storage.enabledCommands.konoself = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+ }
+ value={storage.enabledCommands.konosend}
+ onValueChange={(v) => {
+ storage.enabledCommands.konosend = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+
+
+
+ );
}
// Spotify Settings Page
function SpotifySettingsPage() {
- useProxy(storage);
- const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
+ useProxy(storage);
+ const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
- const styles = stylesheet.createThemedStyleSheet({
- infoText: {
- fontSize: 14,
- color: semanticColors.TEXT_MUTED,
- textAlign: "center",
- lineHeight: 20,
- },
- });
+ const styles = stylesheet.createThemedStyleSheet({
+ infoText: {
+ fontSize: 14,
+ color: semanticColors.TEXT_MUTED,
+ textAlign: "center",
+ lineHeight: 20,
+ },
+ });
- return (
-
-
-
-
- }
- value={storage.enabledCommands.spotifyTrack}
- onValueChange={(v) => {
- storage.enabledCommands.spotifyTrack = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
- }
- value={storage.enabledCommands.spotifyAlbum}
- onValueChange={(v) => {
- storage.enabledCommands.spotifyAlbum = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
- }
- value={storage.enabledCommands.spotifyArtists}
- onValueChange={(v) => {
- storage.enabledCommands.spotifyArtists = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
- }
- value={storage.enabledCommands.spotifyCover}
- onValueChange={(v) => {
- storage.enabledCommands.spotifyCover = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
+
+
+
+ }
+ value={storage.enabledCommands.spotifyTrack}
+ onValueChange={(v) => {
+ storage.enabledCommands.spotifyTrack = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+
+ }
+ value={storage.enabledCommands.spotifyAlbum}
+ onValueChange={(v) => {
+ storage.enabledCommands.spotifyAlbum = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+
+ }
+ value={storage.enabledCommands.spotifyArtists}
+ onValueChange={(v) => {
+ storage.enabledCommands.spotifyArtists = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+
+ }
+ value={storage.enabledCommands.spotifyCover}
+ onValueChange={(v) => {
+ storage.enabledCommands.spotifyCover = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+
-
-
+
+
These commands allow you to share your current Spotify activity in
Discord. Make sure you have Spotify connected to Discord for these
commands to work properly.
-
-
-
-
- );
+
+
+
+
+ );
}
// Other Settings Page - Updated with IP command
function OtherSettingsPage() {
- useProxy(storage);
- const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
+ useProxy(storage);
+ const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
- return (
-
-
-
- }
- value={storage.enabledCommands.firstmessage}
- onValueChange={(v) => {
- storage.enabledCommands.firstmessage = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
+
+
+ }
+ value={storage.enabledCommands.firstmessage}
+ onValueChange={(v) => {
+ storage.enabledCommands.firstmessage = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+
-
- }
- value={storage.enabledCommands.sysinfo}
- onValueChange={(v) => {
- storage.enabledCommands.sysinfo = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
-
-
- );
+
+ }
+ value={storage.enabledCommands.sysinfo}
+ onValueChange={(v) => {
+ storage.enabledCommands.sysinfo = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+
+
+
+ );
}
// Credits Page
function AliucordPage() {
- useProxy(storage);
- const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
- const navigation = NavigationNative.useNavigation();
+ useProxy(storage);
+ const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
+ const navigation = NavigationNative.useNavigation();
- return (
-
-
- }
- value={storage.enabledCommands.ip}
- onValueChange={(v) => {
- storage.enabledCommands.ip = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
+ return (
+
+
+ }
+ value={storage.enabledCommands.ip}
+ onValueChange={(v) => {
+ storage.enabledCommands.ip = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+
-
- }
- value={storage.enabledCommands.nekoslife}
- onValueChange={(v) => {
- storage.enabledCommands.nekoslife = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
- }
- trailing={}
- onPress={() =>
- navigation.push("VendettaCustomPage", {
- title: "NekosLife Categories",
- render: NekosLifeCategoriesPage,
- })
- }
- />
-
+
+ }
+ value={storage.enabledCommands.nekoslife}
+ onValueChange={(v) => {
+ storage.enabledCommands.nekoslife = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+ }
+ trailing={}
+ onPress={() =>
+ navigation.push("VendettaCustomPage", {
+ title: "NekosLife Categories",
+ render: NekosLifeCategoriesPage,
+ })
+ }
+ />
+
-
-
- );
+
+
+ );
}
function CreditsPage() {
- const styles = stylesheet.createThemedStyleSheet({
- container: {
- flex: 1,
- backgroundColor: semanticColors.BACKGROUND_PRIMARY,
- },
- creditItem: {
- flexDirection: "row",
- alignItems: "center",
- paddingVertical: 16,
- paddingHorizontal: 20,
- backgroundColor: semanticColors.CARD_PRIMARY_BG,
- borderRadius: 12,
- marginHorizontal: 16,
- marginVertical: 6,
- shadowColor: semanticColors.BLACK,
- shadowOffset: { width: 0, height: 1 },
- shadowOpacity: 0.1,
- shadowRadius: 2,
- elevation: 2,
- },
- avatar: {
- width: 48,
- height: 48,
- borderRadius: 24,
- marginRight: 16,
- backgroundColor: semanticColors.BACKGROUND_SECONDARY,
- },
- textContainer: {
- flex: 1,
- },
- commandText: {
- fontSize: 17,
- fontWeight: "600",
- color: semanticColors.TEXT_NORMAL,
- marginBottom: 4,
- },
- authorText: {
- fontSize: 15,
- color: semanticColors.TEXT_MUTED,
- marginBottom: 2,
- },
- linkText: {
- fontSize: 13,
- color: semanticColors.TEXT_BRAND,
- fontWeight: "500",
- },
- infoText: {
- fontSize: 14,
- color: semanticColors.TEXT_MUTED,
- marginBottom: 20,
- textAlign: "center",
- lineHeight: 20,
- },
- versionText: {
- fontSize: 15,
- color: semanticColors.TEXT_NORMAL,
- textAlign: "center",
- fontWeight: "600",
- lineHeight: 22,
- },
- });
+ const styles = stylesheet.createThemedStyleSheet({
+ container: {
+ flex: 1,
+ backgroundColor: semanticColors.BACKGROUND_PRIMARY,
+ },
+ creditItem: {
+ flexDirection: "row",
+ alignItems: "center",
+ paddingVertical: 16,
+ paddingHorizontal: 20,
+ backgroundColor: semanticColors.CARD_PRIMARY_BG,
+ borderRadius: 12,
+ marginHorizontal: 16,
+ marginVertical: 6,
+ shadowColor: semanticColors.BLACK,
+ shadowOffset: { width: 0, height: 1 },
+ shadowOpacity: 0.1,
+ shadowRadius: 2,
+ elevation: 2,
+ },
+ avatar: {
+ width: 48,
+ height: 48,
+ borderRadius: 24,
+ marginRight: 16,
+ backgroundColor: semanticColors.BACKGROUND_SECONDARY,
+ },
+ textContainer: {
+ flex: 1,
+ },
+ commandText: {
+ fontSize: 17,
+ fontWeight: "600",
+ color: semanticColors.TEXT_NORMAL,
+ marginBottom: 4,
+ },
+ authorText: {
+ fontSize: 15,
+ color: semanticColors.TEXT_MUTED,
+ marginBottom: 2,
+ },
+ linkText: {
+ fontSize: 13,
+ color: semanticColors.TEXT_BRAND,
+ fontWeight: "500",
+ },
+ infoText: {
+ fontSize: 14,
+ color: semanticColors.TEXT_MUTED,
+ marginBottom: 20,
+ textAlign: "center",
+ lineHeight: 20,
+ },
+ versionText: {
+ fontSize: 15,
+ color: semanticColors.TEXT_NORMAL,
+ textAlign: "center",
+ fontWeight: "600",
+ lineHeight: 22,
+ },
+ });
- const credits = [
- {
- command: "Facts Commands",
- author: "jdev082",
- avatar: "https://github.com/jdev082.png",
- github: "https://github.com/jdev082",
- },
- {
- command: "List Commands",
- author: "Kitomanari",
- avatar: "https://github.com/Kitosight.png",
- github: "https://github.com/Kitosight",
- },
- {
- command: "PetPet",
- author: "wolfieeee",
- avatar: "https://github.com/WolfPlugs.png",
- github: "https://github.com/WolfPlugs",
- },
- {
- command: "KonoChan Commands",
- author: "btmc727 & Rico040",
- avatar: "https://github.com/OTKUSteyler.png",
- github: "https://github.com/OTKUSteyler",
- },
- {
- command: "FirstMessage Command",
- author: "sapphire",
- avatar: "https://github.com/aeongdesu.png",
- github: "https://github.com/aeongdesu",
- },
- {
- command: "Sysinfo Command",
- author: "mugman",
- avatar: "https://github.com/mugman174.png",
- github: "https://github.com/mugman174",
- },
- {
- command: "Spotify Commands",
- author: "Kitomanari",
- avatar: "https://github.com/Kitosight.png",
- github: "https://github.com/Kitosight",
- },
- {
- command: "Gary Command",
- author: "Zach Orange",
- avatar: "https://github.com/IAmGaryTheCat.png",
- github: "https://github.com/IAmGaryTheCat",
- },
- {
- command: "IP & NekosLife Commands",
- author: "scruzism",
- avatar: "https://github.com/scrazzz.png",
- github: "https://github.com/scrazzz",
- },
- ];
+ const credits = [
+ {
+ command: "Facts Commands",
+ author: "jdev082",
+ avatar: "https://github.com/jdev082.png",
+ github: "https://github.com/jdev082",
+ },
+ {
+ command: "List Commands",
+ author: "Kitomanari",
+ avatar: "https://github.com/Kitosight.png",
+ github: "https://github.com/Kitosight",
+ },
+ {
+ command: "PetPet",
+ author: "wolfieeee",
+ avatar: "https://github.com/WolfPlugs.png",
+ github: "https://github.com/WolfPlugs",
+ },
+ {
+ command: "KonoChan Commands",
+ author: "btmc727 & Rico040",
+ avatar: "https://github.com/OTKUSteyler.png",
+ github: "https://github.com/OTKUSteyler",
+ },
+ {
+ command: "FirstMessage Command",
+ author: "sapphire",
+ avatar: "https://github.com/aeongdesu.png",
+ github: "https://github.com/aeongdesu",
+ },
+ {
+ command: "Sysinfo Command",
+ author: "mugman",
+ avatar: "https://github.com/mugman174.png",
+ github: "https://github.com/mugman174",
+ },
+ {
+ command: "Spotify Commands",
+ author: "Kitomanari",
+ avatar: "https://github.com/Kitosight.png",
+ github: "https://github.com/Kitosight",
+ },
+ {
+ command: "Gary Command",
+ author: "Zach Orange",
+ avatar: "https://github.com/IAmGaryTheCat.png",
+ github: "https://github.com/IAmGaryTheCat",
+ },
+ {
+ command: "IP & NekosLife Commands",
+ author: "scruzism",
+ avatar: "https://github.com/scrazzz.png",
+ github: "https://github.com/scrazzz",
+ },
+ ];
- const handleProfilePress = (githubUrl: string) => {
- RN.Linking.openURL(githubUrl);
- };
+ const handleProfilePress = (githubUrl: string) => {
+ RN.Linking.openURL(githubUrl);
+ };
- return (
-
-
-
-
+ return (
+
+
+
+
Thanks to this developers for creating such a nice plugins!{"\n"}
Tap on any developer to visit their GitHub profile.
-
-
+
+
- {credits.map((credit, index) => (
- handleProfilePress(credit.github)}
- android_ripple={{
- color: semanticColors.ANDROID_RIPPLE,
- radius: 200,
- }}
- >
-
-
- {credit.command}
- by {credit.author}
-
- {credit.github.replace("https://github.com/", "@")}
-
-
-
- ))}
+ {credits.map((credit, index) => (
+ handleProfilePress(credit.github)}
+ android_ripple={{
+ color: semanticColors.ANDROID_RIPPLE,
+ radius: 200,
+ }}
+ >
+
+
+ {credit.command}
+ by {credit.author}
+
+ {credit.github.replace("https://github.com/", "@")}
+
+
+
+ ))}
-
-
+
+
Commands Plugin Collection{"\n"}
Version 1.2.0
-
-
-
-
- );
+
+
+
+
+ );
}
// MAIN SETTINGS COMPONENT
export default function Settings() {
- useProxy(storage);
- const navigation = NavigationNative.useNavigation();
- const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
+ useProxy(storage);
+ const navigation = NavigationNative.useNavigation();
+ const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
- React.useEffect(() => {
- return () => {
- if (storage.pendingRestart) {
- storage.pendingRestart = false;
- alerts.showConfirmationAlert({
- title: "Restart Required",
- content:
+ React.useEffect(() => {
+ return () => {
+ if (storage.pendingRestart) {
+ storage.pendingRestart = false;
+ alerts.showConfirmationAlert({
+ title: "Restart Required",
+ content:
"You have made changes to commands. Please restart Discord to apply these changes.",
- confirmText: "Okay",
- cancelText: null,
- });
- }
- };
- }, []);
-
- return (
-
-
-
-
- }
- trailing={}
- onPress={() =>
- navigation.push("VendettaCustomPage", {
- title: "Facts Commands",
- render: FactsSettingsPage,
- })
- }
- />
- }
- trailing={}
- onPress={() =>
- navigation.push("VendettaCustomPage", {
- title: "List Commands",
- render: ListSettingsPage,
- })
- }
- />
- }
- trailing={}
- onPress={() =>
- navigation.push("VendettaCustomPage", {
- title: "Image Commands",
- render: ImageSettingsPage,
- })
- }
- />
- }
- trailing={}
- onPress={() =>
- navigation.push("VendettaCustomPage", {
- title: "Gary Commands",
- render: GaryAPIPage,
- })
- }
- />
-
- }
- trailing={}
- onPress={() =>
- navigation.push("VendettaCustomPage", {
- title: "Spotify Commands",
- render: SpotifySettingsPage,
- })
- }
- />
-
- }
- trailing={}
- onPress={() =>
- navigation.push("VendettaCustomPage", {
- title: "Aliucord",
- render: AliucordPage,
- })
- }
- />
- }
- trailing={}
- onPress={() =>
- navigation.push("VendettaCustomPage", {
- title: "Other Commands",
- render: OtherSettingsPage,
- })
- }
- />
-
-
- {storage.hiddenSettings?.visible && (
-
- }
- trailing={}
- onPress={() =>
- navigation.push("VendettaCustomPage", {
- title: "Hidden Settings",
- render: HiddenSettingsPage,
- })
+ confirmText: "Okay",
+ cancelText: null,
+ });
}
- />
-
- )}
+ };
+ }, []);
-
- }
- trailing={}
- onPress={() =>
- navigation.push("VendettaCustomPage", {
- title: "Credits",
- render: CreditsPage,
- })
- }
- />
-
+ return (
+
+
-
-
- );
+
+ }
+ trailing={}
+ onPress={() =>
+ navigation.push("VendettaCustomPage", {
+ title: "Facts Commands",
+ render: FactsSettingsPage,
+ })
+ }
+ />
+ }
+ trailing={}
+ onPress={() =>
+ navigation.push("VendettaCustomPage", {
+ title: "List Commands",
+ render: ListSettingsPage,
+ })
+ }
+ />
+ }
+ trailing={}
+ onPress={() =>
+ navigation.push("VendettaCustomPage", {
+ title: "Image Commands",
+ render: ImageSettingsPage,
+ })
+ }
+ />
+ }
+ trailing={}
+ onPress={() =>
+ navigation.push("VendettaCustomPage", {
+ title: "Gary Commands",
+ render: GaryAPIPage,
+ })
+ }
+ />
+
+ }
+ trailing={}
+ onPress={() =>
+ navigation.push("VendettaCustomPage", {
+ title: "Spotify Commands",
+ render: SpotifySettingsPage,
+ })
+ }
+ />
+
+ }
+ trailing={}
+ onPress={() =>
+ navigation.push("VendettaCustomPage", {
+ title: "Aliucord",
+ render: AliucordPage,
+ })
+ }
+ />
+ }
+ trailing={}
+ onPress={() =>
+ navigation.push("VendettaCustomPage", {
+ title: "Other Commands",
+ render: OtherSettingsPage,
+ })
+ }
+ />
+
+
+ {storage.hiddenSettings?.visible && (
+
+ }
+ trailing={}
+ onPress={() =>
+ navigation.push("VendettaCustomPage", {
+ title: "Hidden Settings",
+ render: HiddenSettingsPage,
+ })
+ }
+ />
+
+ )}
+
+
+ }
+ trailing={}
+ onPress={() =>
+ navigation.push("VendettaCustomPage", {
+ title: "Credits",
+ render: CreditsPage,
+ })
+ }
+ />
+
+
+
+
+ );
}
diff --git a/plugins/Commands/src/commands/lovefemboys.ts b/plugins/Commands/src/commands/lovefemboys.ts
index 72b7344..7a23050 100644
--- a/plugins/Commands/src/commands/lovefemboys.ts
+++ b/plugins/Commands/src/commands/lovefemboys.ts
@@ -8,166 +8,166 @@ const BotMessage = findByProps("createBotMessage");
const Avatars = findByProps("BOT_AVATARS");
function sendReply(channelID, content, embed) {
- const channel = channelID ?? Channels?.getChannelId?.();
- const msg = BotMessage.createBotMessage({
- channelId: channel,
- content: "",
- embeds: embed,
- });
- msg.author.username = "Astolfo";
- msg.author.avatar = "Astolfo";
- Avatars.BOT_AVATARS.Astolfo =
+ const channel = channelID ?? Channels?.getChannelId?.();
+ const msg = BotMessage.createBotMessage({
+ channelId: channel,
+ content: "",
+ embeds: embed,
+ });
+ msg.author.username = "Astolfo";
+ msg.author.avatar = "Astolfo";
+ Avatars.BOT_AVATARS.Astolfo =
"https://i.pinimg.com/736x/50/77/1f/50771f45b1c015cfbb8b0853ba7b8521.jpg";
- if (typeof content === "string") {
- msg.content = content;
- } else {
- Object.assign(msg, content);
- }
- MessageActions.receiveMessage(channel, msg);
+ if (typeof content === "string") {
+ msg.content = content;
+ } else {
+ Object.assign(msg, content);
+ }
+ MessageActions.receiveMessage(channel, msg);
}
export const lovefemboysCommand = {
- name: "lovefemboys",
- displayName: "lovefemboys",
- description: "Get an image of a femboy",
- displayDescription: "Get an image of a femboy",
- options: [
- {
- name: "nsfw",
- displayName: "nsfw",
- description: "Get the result from r/femboys instead of r/femboy (NSFW)",
- displayDescription:
+ name: "lovefemboys",
+ displayName: "lovefemboys",
+ description: "Get an image of a femboy",
+ displayDescription: "Get an image of a femboy",
+ options: [
+ {
+ name: "nsfw",
+ displayName: "nsfw",
+ description: "Get the result from r/femboys instead of r/femboy (NSFW)",
+ displayDescription:
"Get the result from r/femboys instead of r/femboy (NSFW)",
- required: false,
- type: 5,
- },
- {
- name: "sort",
- displayName: "sort",
- description: "Changes the way reddit sorts.",
- displayDescription: "Changes the way reddit sorts",
- required: false,
- type: 3,
- },
- {
- name: "silent",
- displayName: "silent",
- description: "Makes it so only you can see the message.",
- displayDescription: "Makes it so only you can see the message.",
- required: false,
- type: 5,
- },
- ],
- applicationId: "-1",
- inputType: 1,
- type: 1,
- execute: async (args, ctx) => {
- try {
- // Some code taken from enmity gotfemboys plugin by spinfal & was modified with the help of meqativ
- let nsfw = args.find((arg) => arg.name === "nsfw")?.value;
- let sort = args.find((arg) => arg.name === "sort")?.value;
- let silent = args.find((arg) => arg.name === "silent")?.value;
+ required: false,
+ type: 5,
+ },
+ {
+ name: "sort",
+ displayName: "sort",
+ description: "Changes the way reddit sorts.",
+ displayDescription: "Changes the way reddit sorts",
+ required: false,
+ type: 3,
+ },
+ {
+ name: "silent",
+ displayName: "silent",
+ description: "Makes it so only you can see the message.",
+ displayDescription: "Makes it so only you can see the message.",
+ required: false,
+ type: 5,
+ },
+ ],
+ applicationId: "-1",
+ inputType: 1,
+ type: 1,
+ execute: async (args, ctx) => {
+ try {
+ // Some code taken from enmity gotfemboys plugin by spinfal & was modified with the help of meqativ
+ const nsfw = args.find((arg) => arg.name === "nsfw")?.value;
+ let sort = args.find((arg) => arg.name === "sort")?.value;
+ const silent = args.find((arg) => arg.name === "silent")?.value;
- if (typeof sort === "undefined") sort = storage.sortdefs;
- if (
- !["best", "hot", "new", "rising", "top", "controversial"].includes(sort)
- ) {
- sendReply(
- ctx.channel.id,
- "Incorrect sorting type. Valid options are\n`best`, `hot`, `new`, `rising`, `top`, `controversial`.",
- [],
- );
- return;
- }
+ if (typeof sort === "undefined") sort = storage.sortdefs;
+ if (
+ !["best", "hot", "new", "rising", "top", "controversial"].includes(sort)
+ ) {
+ sendReply(
+ ctx.channel.id,
+ "Incorrect sorting type. Valid options are\n`best`, `hot`, `new`, `rising`, `top`, `controversial`.",
+ [],
+ );
+ return;
+ }
- // Show NSFW warning popup only when nsfw option is selected
- if (nsfw) {
- const shouldProceed = await new Promise((resolve) => {
- alerts.showConfirmationAlert({
- title: "⚠️ NSFW Content Warning",
- content:
+ // Show NSFW warning popup only when nsfw option is selected
+ if (nsfw) {
+ const shouldProceed = await new Promise((resolve) => {
+ alerts.showConfirmationAlert({
+ title: "⚠️ NSFW Content Warning",
+ content:
"This command will send NSFW (Not Safe For Work) content from r/femboys. Are you sure you want to continue?",
- confirmText: "Yes, Continue",
- onConfirm: () => resolve(true),
- cancelText: "Cancel",
- onCancel: () => resolve(false),
- });
- });
+ confirmText: "Yes, Continue",
+ onConfirm: () => resolve(true),
+ cancelText: "Cancel",
+ onCancel: () => resolve(false),
+ });
+ });
- if (!shouldProceed) {
- return { type: 4 };
- }
- }
+ if (!shouldProceed) {
+ return { type: 4 };
+ }
+ }
- let response = await fetch(
- `https://www.reddit.com/r/femboy/${sort}.json?limit=100`,
- ).then((res) => res.json());
+ let response = await fetch(
+ `https://www.reddit.com/r/femboy/${sort}.json?limit=100`,
+ ).then((res) => res.json());
- if (!ctx.channel.nsfw_ && nsfw && storage.nsfwwarn && !(silent ?? true)) {
- sendReply(
- ctx.channel.id,
- "This channel is not marked as NSFW\n(You can disable this check in plugin settings)",
- [],
- );
- return;
- }
+ if (!ctx.channel.nsfw_ && nsfw && storage.nsfwwarn && !(silent ?? true)) {
+ sendReply(
+ ctx.channel.id,
+ "This channel is not marked as NSFW\n(You can disable this check in plugin settings)",
+ [],
+ );
+ return;
+ }
- if (nsfw) {
- response = await fetch(
- `https://www.reddit.com/r/femboys/${sort}.json?limit=100`,
- ).then((res) => res.json());
- }
+ if (nsfw) {
+ response = await fetch(
+ `https://www.reddit.com/r/femboys/${sort}.json?limit=100`,
+ ).then((res) => res.json());
+ }
- response =
+ response =
response.data?.children?.[
- Math.floor(Math.random() * response.data?.children?.length)
+ Math.floor(Math.random() * response.data?.children?.length)
]?.data;
- let author = await fetch(
- `https://www.reddit.com/u/${response?.author}/about.json`,
- ).then((res) => res.json());
+ const author = await fetch(
+ `https://www.reddit.com/u/${response?.author}/about.json`,
+ ).then((res) => res.json());
- if (silent ?? true) {
- sendReply(ctx.channel.id, "", [
- {
- type: "rich",
- title: response?.title,
- url: `https://reddit.com${response?.permalink}`,
- author: {
- name: `u/${response?.author} • r/${response?.subreddit}`,
- proxy_icon_url: author?.data.icon_img.split("?")[0],
- icon_url: author?.data.icon_img.split("?")[0],
- },
- image: {
- proxy_url:
+ if (silent ?? true) {
+ sendReply(ctx.channel.id, "", [
+ {
+ type: "rich",
+ title: response?.title,
+ url: `https://reddit.com${response?.permalink}`,
+ author: {
+ name: `u/${response?.author} • r/${response?.subreddit}`,
+ proxy_icon_url: author?.data.icon_img.split("?")[0],
+ icon_url: author?.data.icon_img.split("?")[0],
+ },
+ image: {
+ proxy_url:
response?.url_overridden_by_dest?.replace(/.gifv$/g, ".gif") ??
response?.url.replace(/.gifv$/g, ".gif"),
- url:
+ url:
response?.url_overridden_by_dest?.replace(/.gifv$/g, ".gif") ??
response?.url?.replace(/.gifv$/g, ".gif"),
- width: response?.preview?.images?.[0]?.source?.width,
- height: response?.preview?.images?.[0]?.source?.height,
- },
- color: "0xf4b8e4",
- },
- ]);
- } else {
- const fixNonce = Date.now().toString();
- MessageActions.sendMessage(
- ctx.channel.id,
- {
- content: response?.url_overridden_by_dest ?? response?.url,
- },
- void 0,
- { nonce: fixNonce },
- );
- }
- } catch (err) {
- console.error("[LoveFemboys] Error:", err);
- sendReply(
- ctx.channel.id,
- "ERROR !!!!!!!!!!!! 😭😭😭 Check debug logs!! 🥺🥺🥺",
- [],
- );
- }
- },
+ width: response?.preview?.images?.[0]?.source?.width,
+ height: response?.preview?.images?.[0]?.source?.height,
+ },
+ color: "0xf4b8e4",
+ },
+ ]);
+ } else {
+ const fixNonce = Date.now().toString();
+ MessageActions.sendMessage(
+ ctx.channel.id,
+ {
+ content: response?.url_overridden_by_dest ?? response?.url,
+ },
+ void 0,
+ { nonce: fixNonce },
+ );
+ }
+ } catch (err) {
+ console.error("[LoveFemboys] Error:", err);
+ sendReply(
+ ctx.channel.id,
+ "ERROR !!!!!!!!!!!! 😭😭😭 Check debug logs!! 🥺🥺🥺",
+ [],
+ );
+ }
+ },
};
diff --git a/plugins/Commands/src/utils/api.ts b/plugins/Commands/src/utils/api.ts
index 2c81fec..63f11cd 100644
--- a/plugins/Commands/src/utils/api.ts
+++ b/plugins/Commands/src/utils/api.ts
@@ -2,64 +2,71 @@
// Facts APIs
export const uselessFact = async () => {
- const response = await fetch("https://uselessfacts.jsph.pl/api/v2/facts/random");
- const resp = await response.json();
- return {
- text: resp["text"],
- source: resp["source"],
- language: resp["language"],
- };
+ const response = await fetch(
+ "https://uselessfacts.jsph.pl/api/v2/facts/random",
+ );
+ const resp = await response.json();
+ return {
+ text: resp["text"],
+ source: resp["source"],
+ language: resp["language"],
+ };
};
export const dogFact = async () => {
- const response = await fetch("https://dogapi.dog/api/v2/facts?limit=1");
- const resp = await response.json();
- return {
- text: resp["data"]["0"]["attributes"]["body"],
- };
+ const response = await fetch("https://dogapi.dog/api/v2/facts?limit=1");
+ const resp = await response.json();
+ return {
+ text: resp["data"]["0"]["attributes"]["body"],
+ };
};
export const catFact = async () => {
- const response = await fetch("https://catfact.ninja/fact");
- const resp = await response.json();
- return {
- text: resp["fact"],
- length: resp["length"],
- };
+ const response = await fetch("https://catfact.ninja/fact");
+ const resp = await response.json();
+ return {
+ text: resp["fact"],
+ length: resp["length"],
+ };
};
// Image APIs
export const getPetPetData = async (image: string) => {
- const data = await fetch(
- `https://api.obamabot.me/v2/image/petpet?image=${image.replace("webp", "png")}`
- );
- const body = await data.json();
- return body;
+ const data = await fetch(
+ `https://api.obamabot.me/v2/image/petpet?image=${image.replace("webp", "png")}`,
+ );
+ const body = await data.json();
+ return body;
};
// Gary API function
export const getGaryUrl = async (source = "gary"): Promise => {
- try {
- switch (source) {
- case "gary":
- const response = await fetch("https://api.garythe.cat/gary");
- const json = await response.json();
- return json.url;
- case "goober":
- const gooberResponse = await fetch("https://api.garythe.cat/goober");
- const gooberJson = await gooberResponse.json();
- return gooberJson.url;
- case "catapi":
- const catResponse = await fetch("https://api.thecatapi.com/v1/images/search");
- const catJson = await catResponse.json();
- return catJson[0].url;
- case "minker":
- return "https://minky.materii.dev/";
- default:
- throw new Error("Invalid Gary image source value");
- }
- } catch (error) {
- console.error(`[Gary API] Error fetching image from ${source}:`, error);
- return null;
+ try {
+ switch (source) {
+ case "gary": {
+ const response = await fetch("https://api.garythe.cat/gary");
+ const json = await response.json();
+ return json.url;
+ }
+ case "goober": {
+ const gooberResponse = await fetch("https://api.garythe.cat/goober");
+ const gooberJson = await gooberResponse.json();
+ return gooberJson.url;
+ }
+ case "catapi": {
+ const catResponse = await fetch(
+ "https://api.thecatapi.com/v1/images/search",
+ );
+ const catJson = await catResponse.json();
+ return catJson[0].url;
+ }
+ case "minker":
+ return "https://minky.materii.dev/";
+ default:
+ throw new Error("Invalid Gary image source value");
}
+ } catch (error) {
+ console.error(`[Gary API] Error fetching image from ${source}:`, error);
+ return null;
+ }
};
diff --git a/plugins/multi-scrobbler/src/constants.ts b/plugins/multi-scrobbler/src/constants.ts
index 9fbb96b..b67f6af 100644
--- a/plugins/multi-scrobbler/src/constants.ts
+++ b/plugins/multi-scrobbler/src/constants.ts
@@ -1,79 +1,79 @@
import { ServiceConfig, ServiceType } from "../../defs";
const Constants = {
- DEFAULT_APP_NAME: "Music",
- DEFAULT_TIME_INTERVAL: 5,
- // Discord application ID
- APPLICATION_ID: "1054951789318909972",
- // Don't check more than once every 3 seconds to avoid getting rate limited
- MIN_UPDATE_INTERVAL: 3,
- // How many times to retry failed API calls
- MAX_RETRY_ATTEMPTS: 3,
- // Wait 5 seconds between retries
- RETRY_DELAY: 5000,
+ DEFAULT_APP_NAME: "Music",
+ DEFAULT_TIME_INTERVAL: 5,
+ // Discord application ID
+ APPLICATION_ID: "1054951789318909972",
+ // Don't check more than once every 3 seconds to avoid getting rate limited
+ MIN_UPDATE_INTERVAL: 3,
+ // How many times to retry failed API calls
+ MAX_RETRY_ATTEMPTS: 3,
+ // Wait 5 seconds between retries
+ RETRY_DELAY: 5000,
- // Configuration for each supported service
- SERVICES: {
- lastfm: {
- name: "Last.fm",
- baseUrl: "http://ws.audioscrobbler.com/2.0",
- requiresApiKey: true,
- requiresToken: false,
+ // Configuration for each supported service
+ SERVICES: {
+ lastfm: {
+ name: "Last.fm",
+ baseUrl: "http://ws.audioscrobbler.com/2.0",
+ requiresApiKey: true,
+ requiresToken: false,
+ },
+ librefm: {
+ name: "Libre.fm",
+ baseUrl: "http://libre.fm/api",
+ requiresApiKey: true,
+ requiresToken: false,
+ },
+ listenbrainz: {
+ name: "ListenBrainz",
+ baseUrl: "http://api.listenbrainz.org/1",
+ requiresApiKey: false,
+ requiresToken: true,
+ },
+ } as Record,
+
+ // Last.fm/Libre.fm use these hashes for their generic album covers
+ DEFAULT_COVER_HASHES: [
+ "2a96cbd8b46e442fc41c2b86b821562f",
+ "c6f59c1e5e7240a4c0d427abd71f3dbb",
+ ],
+
+ // Plugin defaults
+ DEFAULT_SETTINGS: {
+ username: "",
+ apiKey: "",
+ appName: "Music",
+ timeInterval: 5,
+ showTimestamp: true,
+ listeningTo: true,
+ ignoreSpotify: true,
+ verboseLogging: false,
+ service: undefined as ServiceType | undefined,
+ librefmUsername: "",
+ librefmApiKey: "",
+ listenbrainzUsername: "",
+ listenbrainzToken: "",
},
- librefm: {
- name: "Libre.fm",
- baseUrl: "http://libre.fm/api",
- requiresApiKey: true,
- requiresToken: false,
- },
- listenbrainz: {
- name: "ListenBrainz",
- baseUrl: "http://api.listenbrainz.org/1",
- requiresApiKey: false,
- requiresToken: true,
- },
- } as Record,
- // Last.fm/Libre.fm use these hashes for their generic album covers
- DEFAULT_COVER_HASHES: [
- "2a96cbd8b46e442fc41c2b86b821562f",
- "c6f59c1e5e7240a4c0d427abd71f3dbb",
- ],
-
- // Plugin defaults
- DEFAULT_SETTINGS: {
- username: "",
- apiKey: "",
- appName: "Music",
- timeInterval: 5,
- showTimestamp: true,
- listeningTo: true,
- ignoreSpotify: true,
- verboseLogging: false,
- service: undefined as ServiceType | undefined,
- librefmUsername: "",
- librefmApiKey: "",
- listenbrainzUsername: "",
- listenbrainzToken: "",
- },
-
- // Last.fm/Libre.fm API error codes
- API_ERROR_CODES: {
- 2: "Invalid service",
- 3: "Invalid method",
- 4: "Invalid format",
- 5: "Invalid parameters",
- 6: "Invalid resource specified",
- 7: "Invalid session key",
- 8: "Invalid API key",
- 9: "Invalid session",
- 10: "Invalid API signature",
- 11: "Service offline",
- 13: "Invalid method signature supplied",
- 16: "Service temporarily unavailable",
- 26: "Suspended API key",
- 29: "Rate limit exceeded",
- } as Record,
+ // Last.fm/Libre.fm API error codes
+ API_ERROR_CODES: {
+ 2: "Invalid service",
+ 3: "Invalid method",
+ 4: "Invalid format",
+ 5: "Invalid parameters",
+ 6: "Invalid resource specified",
+ 7: "Invalid session key",
+ 8: "Invalid API key",
+ 9: "Invalid session",
+ 10: "Invalid API signature",
+ 11: "Service offline",
+ 13: "Invalid method signature supplied",
+ 16: "Service temporarily unavailable",
+ 26: "Suspended API key",
+ 29: "Rate limit exceeded",
+ } as Record,
} as const;
export default Constants;
diff --git a/plugins/multi-scrobbler/src/index.tsx b/plugins/multi-scrobbler/src/index.tsx
index e024620..896887f 100644
--- a/plugins/multi-scrobbler/src/index.tsx
+++ b/plugins/multi-scrobbler/src/index.tsx
@@ -11,10 +11,10 @@ import Settings from "./ui/pages/Settings";
import patchSidebar from "./sidebar";
export const pluginState = {
- pluginStopped: false,
- lastActivity: undefined,
- updateInterval: undefined,
- lastTrackUrl: undefined,
+ pluginStopped: false,
+ lastActivity: undefined,
+ updateInterval: undefined,
+ lastTrackUrl: undefined,
} as {
pluginStopped: boolean;
lastActivity?: any;
@@ -28,20 +28,20 @@ let sidebarUnpatch: (() => void) | undefined;
// Set up default plugin settings
const defaultSettings: LFMSettings = Constants.DEFAULT_SETTINGS;
for (const key of Object.keys(defaultSettings)) {
- plugin.storage[key] =
+ plugin.storage[key] =
plugin.storage[key] ?? defaultSettings[key as keyof typeof defaultSettings];
}
plugin.storage.addToSidebar ??= false;
export const currentSettings = new Proxy(plugin.storage, {
- get(target, prop) {
- return target[prop];
- },
- set(target, prop, value) {
- target[prop] = value;
- return true;
- },
+ get(target, prop) {
+ return target[prop];
+ },
+ set(target, prop, value) {
+ target[prop] = value;
+ return true;
+ },
});
// Keep track of connection attempts
@@ -51,182 +51,182 @@ const RECONNECT_DELAY = 5000;
const log = (...args: any[]) => console.log("[Scrobble Plugin]", ...args);
const logError = (...args: any[]) =>
- console.error("[Scrobble Plugin] Error:", ...args);
+ console.error("[Scrobble Plugin] Error:", ...args);
async function tryInitialize() {
- try {
- await initialize();
- connectionAttempts = 0;
- log("Plugin initialized successfully");
- } catch (error) {
- logError("Initialization failed:", error);
- connectionAttempts++;
+ try {
+ await initialize();
+ connectionAttempts = 0;
+ log("Plugin initialized successfully");
+ } catch (error) {
+ logError("Initialization failed:", error);
+ connectionAttempts++;
- if (connectionAttempts < MAX_CONNECTION_ATTEMPTS) {
- log(
- `Retrying connection (${connectionAttempts}/${MAX_CONNECTION_ATTEMPTS})...`,
- );
- setTimeout(tryInitialize, RECONNECT_DELAY);
- } else {
- logError(`Failed to connect after ${MAX_CONNECTION_ATTEMPTS} attempts`);
+ if (connectionAttempts < MAX_CONNECTION_ATTEMPTS) {
+ log(
+ `Retrying connection (${connectionAttempts}/${MAX_CONNECTION_ATTEMPTS})...`,
+ );
+ setTimeout(tryInitialize, RECONNECT_DELAY);
+ } else {
+ logError(`Failed to connect after ${MAX_CONNECTION_ATTEMPTS} attempts`);
+ }
}
- }
}
async function validateAndInitialize() {
- // Check if user has selected a service first
- if (!currentSettings.service) {
- log("No service selected. Please configure a service in plugin settings.");
- return;
- }
+ // Check if user has selected a service first
+ if (!currentSettings.service) {
+ log("No service selected. Please configure a service in plugin settings.");
+ return;
+ }
- const serviceName = serviceFactory.getCurrentService().getServiceName();
+ const serviceName = serviceFactory.getCurrentService().getServiceName();
- // Make sure we have credentials for whatever service is selected
- const service = currentSettings.service;
- let hasCredentials = false;
+ // Make sure we have credentials for whatever service is selected
+ const service = currentSettings.service;
+ let hasCredentials = false;
- switch (service) {
- case "lastfm":
- hasCredentials = !!(currentSettings.username && currentSettings.apiKey);
- break;
- case "librefm":
- hasCredentials = !!(
- currentSettings.librefmUsername && currentSettings.librefmApiKey
- );
- break;
- case "listenbrainz":
- hasCredentials = !!currentSettings.listenbrainzUsername;
- break;
- }
+ switch (service) {
+ case "lastfm":
+ hasCredentials = !!(currentSettings.username && currentSettings.apiKey);
+ break;
+ case "librefm":
+ hasCredentials = !!(
+ currentSettings.librefmUsername && currentSettings.librefmApiKey
+ );
+ break;
+ case "listenbrainz":
+ hasCredentials = !!currentSettings.listenbrainzUsername;
+ break;
+ }
- if (!hasCredentials) {
- logError(
- `Missing credentials for ${serviceName}. Please configure in plugin settings.`,
- );
- return;
- }
+ if (!hasCredentials) {
+ logError(
+ `Missing credentials for ${serviceName}. Please configure in plugin settings.`,
+ );
+ return;
+ }
- log(`Starting with ${serviceName}...`);
+ log(`Starting with ${serviceName}...`);
- if (UserStore.getCurrentUser()) {
- log("Discord is already connected, initializing immediately...");
- await tryInitialize();
- } else {
- log("Waiting for Discord connection...");
- const waitForUser = () => {
- if (UserStore.getCurrentUser()) {
- log("Discord connection established");
- tryInitialize();
- FluxDispatcher.unsubscribe("CONNECTION_OPEN", waitForUser);
- }
- };
+ if (UserStore.getCurrentUser()) {
+ log("Discord is already connected, initializing immediately...");
+ await tryInitialize();
+ } else {
+ log("Waiting for Discord connection...");
+ const waitForUser = () => {
+ if (UserStore.getCurrentUser()) {
+ log("Discord connection established");
+ tryInitialize();
+ FluxDispatcher.unsubscribe("CONNECTION_OPEN", waitForUser);
+ }
+ };
- FluxDispatcher.subscribe("CONNECTION_OPEN", waitForUser);
- }
+ FluxDispatcher.subscribe("CONNECTION_OPEN", waitForUser);
+ }
}
export default {
- onLoad() {
- log("Plugin loading...");
- pluginState.pluginStopped = false;
+ onLoad() {
+ log("Plugin loading...");
+ pluginState.pluginStopped = false;
- // Check if service is configured
- if (!currentSettings.service) {
- log("No service configured. Please select a service in plugin settings.");
- } else {
- // Show what we're starting with
- const serviceName = serviceFactory.getCurrentService().getServiceName();
- log(
- `Configuration: Service=${serviceName}, Update Interval=${currentSettings.timeInterval}s, Verbose=${currentSettings.verboseLogging}`,
- );
- }
-
- // Add to sidebar if user wants it
- try {
- sidebarUnpatch = patchSidebar();
- cleanupFunctions.push(() => {
- if (sidebarUnpatch) {
- sidebarUnpatch();
- sidebarUnpatch = undefined;
+ // Check if service is configured
+ if (!currentSettings.service) {
+ log("No service configured. Please select a service in plugin settings.");
+ } else {
+ // Show what we're starting with
+ const serviceName = serviceFactory.getCurrentService().getServiceName();
+ log(
+ `Configuration: Service=${serviceName}, Update Interval=${currentSettings.timeInterval}s, Verbose=${currentSettings.verboseLogging}`,
+ );
}
- });
- } catch (error) {
- log("Sidebar setup failed:", error);
- }
- validateAndInitialize();
- },
-
- onUnload() {
- log("Plugin unloading...");
- pluginState.pluginStopped = true;
-
- // Run all cleanup functions
- cleanupFunctions.forEach((cleanup) => {
- try {
- cleanup();
- } catch (error) {
- // ignore cleanup errors
- }
- });
- cleanupFunctions.length = 0;
-
- stop();
- log("Plugin unloaded");
- },
-
- // When user changes settings
- async onSettingsUpdate(newSettings: any) {
- const oldService = currentSettings.service;
- const newService = newSettings.service;
- const oldSidebar = currentSettings.addToSidebar;
- const newSidebar = newSettings.addToSidebar;
-
- // Apply the new settings
- Object.assign(currentSettings, newSettings);
-
- log("Settings updated:", Object.keys(newSettings));
-
- // Check if sidebar setting changed
- if (oldSidebar !== newSidebar) {
- if (newSidebar) {
+ // Add to sidebar if user wants it
try {
- sidebarUnpatch = patchSidebar();
- log("Sidebar enabled");
+ sidebarUnpatch = patchSidebar();
+ cleanupFunctions.push(() => {
+ if (sidebarUnpatch) {
+ sidebarUnpatch();
+ sidebarUnpatch = undefined;
+ }
+ });
} catch (error) {
- log("Failed to enable sidebar:", error);
+ log("Sidebar setup failed:", error);
}
- } else {
- if (sidebarUnpatch) {
- sidebarUnpatch();
- sidebarUnpatch = undefined;
- log("Sidebar disabled");
+
+ validateAndInitialize();
+ },
+
+ onUnload() {
+ log("Plugin unloading...");
+ pluginState.pluginStopped = true;
+
+ // Run all cleanup functions
+ cleanupFunctions.forEach((cleanup) => {
+ try {
+ cleanup();
+ } catch (error) {
+ // ignore cleanup errors
+ }
+ });
+ cleanupFunctions.length = 0;
+
+ stop();
+ log("Plugin unloaded");
+ },
+
+ // When user changes settings
+ async onSettingsUpdate(newSettings: any) {
+ const oldService = currentSettings.service;
+ const newService = newSettings.service;
+ const oldSidebar = currentSettings.addToSidebar;
+ const newSidebar = newSettings.addToSidebar;
+
+ // Apply the new settings
+ Object.assign(currentSettings, newSettings);
+
+ log("Settings updated:", Object.keys(newSettings));
+
+ // Check if sidebar setting changed
+ if (oldSidebar !== newSidebar) {
+ if (newSidebar) {
+ try {
+ sidebarUnpatch = patchSidebar();
+ log("Sidebar enabled");
+ } catch (error) {
+ log("Failed to enable sidebar:", error);
+ }
+ } else {
+ if (sidebarUnpatch) {
+ sidebarUnpatch();
+ sidebarUnpatch = undefined;
+ log("Sidebar disabled");
+ }
+ }
}
- }
- }
- // Switch services if needed
- if (oldService !== newService && newService) {
- log(`Service changed from ${oldService || "none"} to ${newService}`);
- await switchService(newService);
- } else if (!pluginState.pluginStopped && currentSettings.service) {
- // Just restart with the updated settings (only if service is selected)
- log("Restarting with updated settings...");
- await tryInitialize();
- } else if (!currentSettings.service) {
- // Service was unselected, stop the plugin
- log("Service unselected, stopping plugin...");
- stop();
- }
- },
+ // Switch services if needed
+ if (oldService !== newService && newService) {
+ log(`Service changed from ${oldService || "none"} to ${newService}`);
+ await switchService(newService);
+ } else if (!pluginState.pluginStopped && currentSettings.service) {
+ // Just restart with the updated settings (only if service is selected)
+ log("Restarting with updated settings...");
+ await tryInitialize();
+ } else if (!currentSettings.service) {
+ // Service was unselected, stop the plugin
+ log("Service unselected, stopping plugin...");
+ stop();
+ }
+ },
- onDiscordReconnect() {
- if (!pluginState.pluginStopped) {
- log("Discord reconnected, reinitializing plugin...");
- tryInitialize();
- }
- },
+ onDiscordReconnect() {
+ if (!pluginState.pluginStopped) {
+ log("Discord reconnected, reinitializing plugin...");
+ tryInitialize();
+ }
+ },
- settings: Settings,
+ settings: Settings,
};
diff --git a/plugins/multi-scrobbler/src/manager.ts b/plugins/multi-scrobbler/src/manager.ts
index 95a251f..6d27700 100644
--- a/plugins/multi-scrobbler/src/manager.ts
+++ b/plugins/multi-scrobbler/src/manager.ts
@@ -4,16 +4,16 @@ import Constants from "./constants";
import { SelfPresenceStore } from "./modules";
import { clearActivity, fetchAsset, sendRequest } from "./utils/activity";
import {
- setDebugInfo,
- recordSuccessfulUpdate,
- recordServiceError,
- incrementApiCall,
+ setDebugInfo,
+ recordSuccessfulUpdate,
+ recordServiceError,
+ incrementApiCall,
} from "./utils/debug";
import { serviceFactory } from "./services/ServiceFactory";
import {
- formatDuration,
- validateTimestamps,
- getCurrentTimestamp,
+ formatDuration,
+ validateTimestamps,
+ getCurrentTimestamp,
} from "./utils/time";
enum ActivityType {
@@ -25,382 +25,382 @@ enum ActivityType {
const log = (...message: any[]) => console.log("[Scrobble Plugin]", ...message);
const logError = (...message: any[]) =>
- console.error("[Scrobble Plugin] Error:", ...message);
+ console.error("[Scrobble Plugin] Error:", ...message);
const logVerbose = (...message: any[]) =>
- currentSettings.verboseLogging &&
+ currentSettings.verboseLogging &&
console.log("[Scrobble Plugin] Verbose:", ...message);
class PluginManager {
- private static instance: PluginManager;
- private updateTimer?: any;
- private reconnectTimer?: any;
- private consecutiveFailures = 0;
- private isReconnecting = false;
- private currentActivity?: Activity;
- private lastUpdateTime = 0;
+ private static instance: PluginManager;
+ private updateTimer?: any;
+ private reconnectTimer?: any;
+ private consecutiveFailures = 0;
+ private isReconnecting = false;
+ private currentActivity?: Activity;
+ private lastUpdateTime = 0;
- private constructor() {
+ private constructor() {
// singleton pattern
- }
-
- public static getInstance(): PluginManager {
- if (!PluginManager.instance) {
- PluginManager.instance = new PluginManager();
- }
- return PluginManager.instance;
- }
-
- // Check for new tracks and update Discord status when something changes
- private async updateActivity() {
- if (pluginState.pluginStopped) {
- logVerbose("Plugin is stopped, skipping update");
- this.stopUpdates();
- return;
}
- const serviceName = serviceFactory.getCurrentService().getServiceName();
- logVerbose(`Fetching latest track from ${serviceName}...`);
-
- // keep track of whether we actually need to update Discord
- let willUpdateRPC = false;
-
- try {
- // skip if Spotify is playing and user wants to ignore it
- if (currentSettings.ignoreSpotify) {
- const spotifyActivity = SelfPresenceStore.findActivity(
- (act) => act.sync_id,
- );
- if (spotifyActivity) {
- logVerbose("Spotify is currently playing, clearing activity");
- setDebugInfo("isSpotifyIgnored", true);
- clearActivity();
- return;
+ public static getInstance(): PluginManager {
+ if (!PluginManager.instance) {
+ PluginManager.instance = new PluginManager();
}
- setDebugInfo("isSpotifyIgnored", false);
- }
+ return PluginManager.instance;
+ }
- incrementApiCall();
- const lastTrack = await serviceFactory
- .getCurrentService()
- .fetchLatestScrobble();
- setDebugInfo("lastTrack", lastTrack);
+ // Check for new tracks and update Discord status when something changes
+ private async updateActivity() {
+ if (pluginState.pluginStopped) {
+ logVerbose("Plugin is stopped, skipping update");
+ this.stopUpdates();
+ return;
+ }
- if (!lastTrack.nowPlaying) {
- logVerbose("Track is not currently playing");
- clearActivity();
- return;
- }
+ const serviceName = serviceFactory.getCurrentService().getServiceName();
+ logVerbose(`Fetching latest track from ${serviceName}...`);
- // use simple URL comparison like Last.fm plugin
- if (pluginState.lastTrackUrl === lastTrack.url) {
- logVerbose("Track hasn't changed");
- recordSuccessfulUpdate();
- this.consecutiveFailures = 0;
- return;
- }
+ // keep track of whether we actually need to update Discord
+ let willUpdateRPC = false;
- willUpdateRPC = true;
+ try {
+ // skip if Spotify is playing and user wants to ignore it
+ if (currentSettings.ignoreSpotify) {
+ const spotifyActivity = SelfPresenceStore.findActivity(
+ (act) => act.sync_id,
+ );
+ if (spotifyActivity) {
+ logVerbose("Spotify is currently playing, clearing activity");
+ setDebugInfo("isSpotifyIgnored", true);
+ clearActivity();
+ return;
+ }
+ setDebugInfo("isSpotifyIgnored", false);
+ }
- log(`🎵 Track changed: ${lastTrack.artist} - ${lastTrack.name}`);
+ incrementApiCall();
+ const lastTrack = await serviceFactory
+ .getCurrentService()
+ .fetchLatestScrobble();
+ setDebugInfo("lastTrack", lastTrack);
- // set up timestamps for the track
- let activityTimestamps;
- if (
- lastTrack.nowPlaying &&
+ if (!lastTrack.nowPlaying) {
+ logVerbose("Track is not currently playing");
+ clearActivity();
+ return;
+ }
+
+ // use simple URL comparison like Last.fm plugin
+ if (pluginState.lastTrackUrl === lastTrack.url) {
+ logVerbose("Track hasn't changed");
+ recordSuccessfulUpdate();
+ this.consecutiveFailures = 0;
+ return;
+ }
+
+ willUpdateRPC = true;
+
+ log(`🎵 Track changed: ${lastTrack.artist} - ${lastTrack.name}`);
+
+ // set up timestamps for the track
+ let activityTimestamps;
+ if (
+ lastTrack.nowPlaying &&
currentSettings.showTimestamp &&
lastTrack.from
- ) {
- // figure out when this track actually started
- const now = getCurrentTimestamp();
- let startTime = lastTrack.from;
+ ) {
+ // figure out when this track actually started
+ const now = getCurrentTimestamp();
+ let startTime = lastTrack.from;
- // if the timestamp is way old, estimate when it started
- if (startTime < now - 3600) {
- // more than an hour old - probably wrong
- if (lastTrack.duration && lastTrack.duration > 0) {
- // guess we're about 10% in or 30 seconds, whatever's smaller
- const estimatedElapsed = Math.min(lastTrack.duration * 0.1, 30);
- startTime = now - estimatedElapsed;
- } else {
- startTime = now;
- }
- logVerbose("had to estimate start time");
- }
+ // if the timestamp is way old, estimate when it started
+ if (startTime < now - 3600) {
+ // more than an hour old - probably wrong
+ if (lastTrack.duration && lastTrack.duration > 0) {
+ // guess we're about 10% in or 30 seconds, whatever's smaller
+ const estimatedElapsed = Math.min(lastTrack.duration * 0.1, 30);
+ startTime = now - estimatedElapsed;
+ } else {
+ startTime = now;
+ }
+ logVerbose("had to estimate start time");
+ }
- activityTimestamps = {
- start: startTime * 1000, // Discord wants milliseconds
- };
+ activityTimestamps = {
+ start: startTime * 1000, // Discord wants milliseconds
+ };
- if (lastTrack.to) {
- activityTimestamps.end = lastTrack.to * 1000;
- }
- }
+ if (lastTrack.to) {
+ activityTimestamps.end = lastTrack.to * 1000;
+ }
+ }
- logVerbose(
- `🎯 Preparing RPC update for: ${lastTrack.artist} - ${lastTrack.name}`,
- );
+ logVerbose(
+ `🎯 Preparing RPC update for: ${lastTrack.artist} - ${lastTrack.name}`,
+ );
- const activity: Activity = {
- name: currentSettings.appName || Constants.DEFAULT_APP_NAME,
- flags: 0,
- type: currentSettings.listeningTo
- ? ActivityType.LISTENING
- : ActivityType.PLAYING,
- details: lastTrack.name,
- state: `by ${lastTrack.artist}`,
- status_display_type: 1,
- application_id: Constants.APPLICATION_ID,
- };
+ const activity: Activity = {
+ name: currentSettings.appName || Constants.DEFAULT_APP_NAME,
+ flags: 0,
+ type: currentSettings.listeningTo
+ ? ActivityType.LISTENING
+ : ActivityType.PLAYING,
+ details: lastTrack.name,
+ state: `by ${lastTrack.artist}`,
+ status_display_type: 1,
+ application_id: Constants.APPLICATION_ID,
+ };
- // replace template variables in app name if user is using them
- if (activity.name.includes("{{")) {
- const variables = {
- artist: lastTrack.artist,
- name: lastTrack.name,
- album: lastTrack.album,
- service: serviceName,
- };
+ // replace template variables in app name if user is using them
+ if (activity.name.includes("{{")) {
+ const variables = {
+ artist: lastTrack.artist,
+ name: lastTrack.name,
+ album: lastTrack.album,
+ service: serviceName,
+ };
- for (const [key, value] of Object.entries(variables)) {
- activity.name = activity.name.replace(
- new RegExp(`{{${key}}}`, "g"),
- value || "",
- );
- }
- }
+ for (const [key, value] of Object.entries(variables)) {
+ activity.name = activity.name.replace(
+ new RegExp(`{{${key}}}`, "g"),
+ value || "",
+ );
+ }
+ }
- // add the timestamps we figured out
- if (activityTimestamps) {
- activity.timestamps = activityTimestamps;
+ // add the timestamps we figured out
+ if (activityTimestamps) {
+ activity.timestamps = activityTimestamps;
- logVerbose("Timestamps set:", {
- start: new Date(activityTimestamps.start).toISOString(),
- end: activityTimestamps.end
- ? new Date(activityTimestamps.end).toISOString()
- : "none",
- duration: lastTrack.duration
- ? formatDuration(lastTrack.duration)
- : "unknown",
- });
- }
+ logVerbose("Timestamps set:", {
+ start: new Date(activityTimestamps.start).toISOString(),
+ end: activityTimestamps.end
+ ? new Date(activityTimestamps.end).toISOString()
+ : "none",
+ duration: lastTrack.duration
+ ? formatDuration(lastTrack.duration)
+ : "unknown",
+ });
+ }
- // set up album art and tooltip text
- if (lastTrack.album || lastTrack.albumArt) {
- const assetUrls = lastTrack.albumArt ? [lastTrack.albumArt] : [];
- const assets = await fetchAsset(assetUrls);
+ // set up album art and tooltip text
+ if (lastTrack.album || lastTrack.albumArt) {
+ const assetUrls = lastTrack.albumArt ? [lastTrack.albumArt] : [];
+ const assets = await fetchAsset(assetUrls);
- if (assets[0]) {
- const largeText = lastTrack.album
- ? `on ${lastTrack.album}`
- : `${lastTrack.artist} - ${lastTrack.name}`;
+ if (assets[0]) {
+ const largeText = lastTrack.album
+ ? `on ${lastTrack.album}`
+ : `${lastTrack.artist} - ${lastTrack.name}`;
- // include track length in the tooltip if we have it
- const durationText =
+ // include track length in the tooltip if we have it
+ const durationText =
currentSettings.showTimestamp && lastTrack.duration
- ? ` • ${formatDuration(lastTrack.duration)}`
- : "";
+ ? ` • ${formatDuration(lastTrack.duration)}`
+ : "";
- activity.assets = {
- large_image: assets[0],
- large_text: largeText + durationText,
- };
+ activity.assets = {
+ large_image: assets[0],
+ large_text: largeText + durationText,
+ };
- logVerbose("Album art set:", assets[0]);
- } else if (lastTrack.album) {
- // no album art but we can still show the album name
- activity.assets = {
- large_text: `on ${lastTrack.album}`,
- };
+ logVerbose("Album art set:", assets[0]);
+ } else if (lastTrack.album) {
+ // no album art but we can still show the album name
+ activity.assets = {
+ large_text: `on ${lastTrack.album}`,
+ };
+ }
+ }
+
+ logVerbose("Setting Discord activity:", activity);
+ setDebugInfo("lastActivity", activity);
+
+ await sendRequest(activity);
+ pluginState.lastTrackUrl = lastTrack.url;
+ this.currentActivity = activity;
+ pluginState.lastActivity = activity;
+ this.consecutiveFailures = 0;
+ this.lastUpdateTime = getCurrentTimestamp();
+ recordSuccessfulUpdate();
+
+ log(
+ `✅ RPC updated successfully: ${lastTrack.artist} - ${lastTrack.name}`,
+ );
+ } catch (error) {
+ logError("Update failed:", error);
+ recordServiceError(currentSettings.service, (error as Error).message);
+ this.handleError(error as Error);
}
- }
-
- logVerbose("Setting Discord activity:", activity);
- setDebugInfo("lastActivity", activity);
-
- await sendRequest(activity);
- pluginState.lastTrackUrl = lastTrack.url;
- this.currentActivity = activity;
- pluginState.lastActivity = activity;
- this.consecutiveFailures = 0;
- this.lastUpdateTime = getCurrentTimestamp();
- recordSuccessfulUpdate();
-
- log(
- `✅ RPC updated successfully: ${lastTrack.artist} - ${lastTrack.name}`,
- );
- } catch (error) {
- logError("Update failed:", error);
- recordServiceError(currentSettings.service, (error as Error).message);
- this.handleError(error as Error);
- }
- }
-
- private handleError(error: Error) {
- this.consecutiveFailures++;
- setDebugInfo("lastError", error);
-
- logError(
- `Failure ${this.consecutiveFailures}/${Constants.MAX_RETRY_ATTEMPTS}:`,
- error.message,
- );
-
- if (this.consecutiveFailures >= Constants.MAX_RETRY_ATTEMPTS) {
- logError("Max retry attempts reached, initiating reconnection...");
- this.startReconnection();
- }
- }
-
- private startReconnection() {
- if (this.isReconnecting) return;
-
- this.isReconnecting = true;
- this.stopUpdates();
-
- log("Starting reconnection process...");
- this.reconnectTimer = setInterval(() => {
- log("Attempting to reconnect...");
- this.initialize() // don't reset state
- .then(() => {
- log("Reconnection successful!");
- this.stopReconnection();
- })
- .catch((error) => {
- logError("Reconnection attempt failed:", error.message);
- });
- }, Constants.RETRY_DELAY);
- }
-
- private stopReconnection() {
- try {
- if (this.reconnectTimer) {
- clearInterval(this.reconnectTimer);
- this.reconnectTimer = undefined;
- }
- this.isReconnecting = false;
- this.consecutiveFailures = 0;
- } catch (error) {
- // Silently handle reconnection cleanup errors
- console.error("[Scrobble Plugin] Reconnection cleanup error:", error);
- }
- }
-
- // clean up all timers
- private stopUpdates() {
- try {
- if (this.updateTimer) {
- clearInterval(this.updateTimer);
- this.updateTimer = undefined;
- }
- } catch (error) {
- // Silently handle timer cleanup errors
- console.error("[Scrobble Plugin] Timer cleanup error:", error);
- }
- }
-
- // start everything up
- public async initialize() {
- if (pluginState.pluginStopped) {
- throw new Error("Plugin is stopped");
}
- const serviceName = serviceFactory.getCurrentService().getServiceName();
- log(`Initializing with ${serviceName}...`);
+ private handleError(error: Error) {
+ this.consecutiveFailures++;
+ setDebugInfo("lastError", error);
- // make sure credentials work before starting
- try {
- const isValid = await serviceFactory.validateCurrentService();
- if (!isValid) {
- throw new Error(`Invalid credentials for ${serviceName}`);
- }
- log(`${serviceName} credentials validated successfully`);
- } catch (error) {
- logError(`Failed to validate ${serviceName} credentials:`, error);
- throw error;
+ logError(
+ `Failure ${this.consecutiveFailures}/${Constants.MAX_RETRY_ATTEMPTS}:`,
+ error.message,
+ );
+
+ if (this.consecutiveFailures >= Constants.MAX_RETRY_ATTEMPTS) {
+ logError("Max retry attempts reached, initiating reconnection...");
+ this.startReconnection();
+ }
}
- this.stopUpdates();
+ private startReconnection() {
+ if (this.isReconnecting) return;
- // check right away in case something is already playing
- log("🎵 checking for already playing songs...");
- // Don't reset track state - let updateActivity() handle whether to update
- await this.updateActivity();
+ this.isReconnecting = true;
+ this.stopUpdates();
- const interval = Math.max(
- (Number(currentSettings.timeInterval) ||
+ log("Starting reconnection process...");
+ this.reconnectTimer = setInterval(() => {
+ log("Attempting to reconnect...");
+ this.initialize() // don't reset state
+ .then(() => {
+ log("Reconnection successful!");
+ this.stopReconnection();
+ })
+ .catch((error) => {
+ logError("Reconnection attempt failed:", error.message);
+ });
+ }, Constants.RETRY_DELAY);
+ }
+
+ private stopReconnection() {
+ try {
+ if (this.reconnectTimer) {
+ clearInterval(this.reconnectTimer);
+ this.reconnectTimer = undefined;
+ }
+ this.isReconnecting = false;
+ this.consecutiveFailures = 0;
+ } catch (error) {
+ // Silently handle reconnection cleanup errors
+ console.error("[Scrobble Plugin] Reconnection cleanup error:", error);
+ }
+ }
+
+ // clean up all timers
+ private stopUpdates() {
+ try {
+ if (this.updateTimer) {
+ clearInterval(this.updateTimer);
+ this.updateTimer = undefined;
+ }
+ } catch (error) {
+ // Silently handle timer cleanup errors
+ console.error("[Scrobble Plugin] Timer cleanup error:", error);
+ }
+ }
+
+ // start everything up
+ public async initialize() {
+ if (pluginState.pluginStopped) {
+ throw new Error("Plugin is stopped");
+ }
+
+ const serviceName = serviceFactory.getCurrentService().getServiceName();
+ log(`Initializing with ${serviceName}...`);
+
+ // make sure credentials work before starting
+ try {
+ const isValid = await serviceFactory.validateCurrentService();
+ if (!isValid) {
+ throw new Error(`Invalid credentials for ${serviceName}`);
+ }
+ log(`${serviceName} credentials validated successfully`);
+ } catch (error) {
+ logError(`Failed to validate ${serviceName} credentials:`, error);
+ throw error;
+ }
+
+ this.stopUpdates();
+
+ // check right away in case something is already playing
+ log("🎵 checking for already playing songs...");
+ // Don't reset track state - let updateActivity() handle whether to update
+ await this.updateActivity();
+
+ const interval = Math.max(
+ (Number(currentSettings.timeInterval) ||
Constants.DEFAULT_SETTINGS.timeInterval) * 1000,
- Constants.MIN_UPDATE_INTERVAL * 1000,
- );
+ Constants.MIN_UPDATE_INTERVAL * 1000,
+ );
- this.updateTimer = setInterval(() => this.updateActivity(), interval);
- log(
- `Update timer started with interval: ${interval}ms (${interval / 1000}s)`,
- );
- }
-
- // stop everything and clean up
- public stop() {
- if (pluginState.pluginStopped) {
- return; // already stopped
+ this.updateTimer = setInterval(() => this.updateActivity(), interval);
+ log(
+ `Update timer started with interval: ${interval}ms (${interval / 1000}s)`,
+ );
}
- log("Stopping plugin...");
- pluginState.pluginStopped = true;
+ // stop everything and clean up
+ public stop() {
+ if (pluginState.pluginStopped) {
+ return; // already stopped
+ }
- try {
- this.stopUpdates();
- this.stopReconnection();
- clearActivity();
- // Don't reset track state on stop - only when actually switching services
- log("Plugin stopped successfully");
- } catch (error) {
- // don't let cleanup errors break things
- console.error("[Scrobble Plugin] Stop error:", error);
- }
- }
+ log("Stopping plugin...");
+ pluginState.pluginStopped = true;
- // change to a different scrobble service
- public async switchService(newService: string) {
- if (pluginState.pluginStopped) {
- return;
+ try {
+ this.stopUpdates();
+ this.stopReconnection();
+ clearActivity();
+ // Don't reset track state on stop - only when actually switching services
+ log("Plugin stopped successfully");
+ } catch (error) {
+ // don't let cleanup errors break things
+ console.error("[Scrobble Plugin] Stop error:", error);
+ }
}
- log(`Switching to ${newService}...`);
+ // change to a different scrobble service
+ public async switchService(newService: string) {
+ if (pluginState.pluginStopped) {
+ return;
+ }
- // stop what we're doing first
- const wasRunning = !pluginState.pluginStopped;
- this.stop();
+ log(`Switching to ${newService}...`);
- try {
- // clear cache so we get fresh service instances
- serviceFactory.clearCache();
+ // stop what we're doing first
+ const wasRunning = !pluginState.pluginStopped;
+ this.stop();
- // Reset track state when switching services
- pluginState.lastTrackUrl = undefined;
- this.currentActivity = undefined;
- this.lastUpdateTime = 0;
+ try {
+ // clear cache so we get fresh service instances
+ serviceFactory.clearCache();
- // start back up with the new service
- if (wasRunning) {
- pluginState.pluginStopped = false;
- await this.initialize();
- }
- } catch (error) {
- logError("Failed to switch service:", error);
+ // Reset track state when switching services
+ pluginState.lastTrackUrl = undefined;
+ this.currentActivity = undefined;
+ this.lastUpdateTime = 0;
+
+ // start back up with the new service
+ if (wasRunning) {
+ pluginState.pluginStopped = false;
+ await this.initialize();
+ }
+ } catch (error) {
+ logError("Failed to switch service:", error);
+ }
}
- }
- // get info about what's currently happening
- public getStatus() {
- const serviceName = serviceFactory.getCurrentService().getServiceName();
- return {
- running: !pluginState.pluginStopped,
- service: serviceName,
- consecutiveFailures: this.consecutiveFailures,
- isReconnecting: this.isReconnecting,
- lastTrackUrl: pluginState.lastTrackUrl,
- updateInterval: this.updateTimer ? "Active" : "Inactive",
- };
- }
+ // get info about what's currently happening
+ public getStatus() {
+ const serviceName = serviceFactory.getCurrentService().getServiceName();
+ return {
+ running: !pluginState.pluginStopped,
+ service: serviceName,
+ consecutiveFailures: this.consecutiveFailures,
+ isReconnecting: this.isReconnecting,
+ lastTrackUrl: pluginState.lastTrackUrl,
+ updateInterval: this.updateTimer ? "Active" : "Inactive",
+ };
+ }
}
// expose the manager functions
@@ -408,5 +408,5 @@ const manager = PluginManager.getInstance();
export const initialize = () => manager.initialize();
export const stop = () => manager.stop();
export const switchService = (service: string) =>
- manager.switchService(service);
+ manager.switchService(service);
export const getStatus = () => manager.getStatus();
diff --git a/plugins/multi-scrobbler/src/services/BaseService.ts b/plugins/multi-scrobbler/src/services/BaseService.ts
index 3df5845..da49b80 100644
--- a/plugins/multi-scrobbler/src/services/BaseService.ts
+++ b/plugins/multi-scrobbler/src/services/BaseService.ts
@@ -3,119 +3,119 @@ import Constants from "../constants";
import { incrementApiCall, recordServiceError } from "../utils/debug";
export abstract class BaseService implements ServiceClient {
- protected retryCount = 0;
- protected lastError = 0;
+ protected retryCount = 0;
+ protected lastError = 0;
abstract fetchLatestScrobble(): Promise