feat(multi-scrobbler): implement 60s rate limiting for Libre.fm
Enforces a minimum update interval of 60 seconds when Libre.fm is the active service, as requested by the Libre.fm team to reduce server load. - Added LIBREFM_MIN_UPDATE_INTERVAL to Constants. - Updated PluginManager to enforce the new minimum for Libre.fm. - Disabled user-configurable update interval in UI when Libre.fm is active. Closes #4
This commit is contained in:
parent
84c70a3c7b
commit
6b5ff87d66
9 changed files with 42 additions and 562 deletions
|
|
@ -1,178 +0,0 @@
|
||||||
import { after } from "@api/patcher";
|
|
||||||
import { findByNameLazy, findByProps, findByPropsLazy, findByStoreName, findByTypeDisplayName } from "@metro";
|
|
||||||
import { ReactNative } from "@metro/common";
|
|
||||||
import { definePlugin } from "@plugins";
|
|
||||||
import { Contributors } from "@rain/Developers";
|
|
||||||
import React, { useEffect, useRef } from "react";
|
|
||||||
|
|
||||||
import ChatboxAvatarSettings from "./settings";
|
|
||||||
import { useChatboxAvatarSettings } from "./storage";
|
|
||||||
|
|
||||||
const Flux = findByProps("useStateFromStores");
|
|
||||||
const ChatInputActions = findByTypeDisplayName("ChatInputActions");
|
|
||||||
const ChatInputSendButton = findByTypeDisplayName("ChatInputSendButton");
|
|
||||||
let hasText = false;
|
|
||||||
let sendBtnRef: { setHasText?: (hasText: boolean) => void } | undefined;
|
|
||||||
const { Pressable, View, Animated } = ReactNative;
|
|
||||||
|
|
||||||
const avatarCollapse = new Animated.Value(0);
|
|
||||||
|
|
||||||
const Avatar = findByPropsLazy("default", "AvatarSizes", "getStatusSize")?.default;
|
|
||||||
|
|
||||||
const UserStore = findByStoreName("UserStore");
|
|
||||||
const SelectedChannelStore = findByStoreName("SelectedChannelStore");
|
|
||||||
const ChannelStore = findByStoreName("ChannelStore");
|
|
||||||
const SelfPresenceStore = findByStoreName("SelfPresenceStore");
|
|
||||||
const showUserProfileActionSheet = findByNameLazy("showUserProfileActionSheet");
|
|
||||||
const showYouAccountActionSheetByProp = findByPropsLazy("showYouAccountActionSheet");
|
|
||||||
|
|
||||||
function AvatarAction() {
|
|
||||||
const [textState, setTextState] = React.useState(false);
|
|
||||||
const self = Flux?.useStateFromStores?.([UserStore], () => UserStore?.getCurrentUser?.());
|
|
||||||
const status = Flux?.useStateFromStores?.([SelfPresenceStore], () => SelfPresenceStore?.getStatus?.());
|
|
||||||
const settings = useChatboxAvatarSettings();
|
|
||||||
const channelId = Flux?.useStateFromStores?.([SelectedChannelStore], () => SelectedChannelStore?.getCurrentlySelectedChannelId?.());
|
|
||||||
const channel = Flux?.useStateFromStores?.([ChannelStore], () => ChannelStore?.getChannel?.(channelId), [channelId]);
|
|
||||||
|
|
||||||
const animated = useRef(avatarCollapse).current;
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
setTextState(hasText);
|
|
||||||
}, 100);
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const shouldCollapse = settings.collapseWhileTyping && textState;
|
|
||||||
Animated.parallel([
|
|
||||||
Animated.timing(animated, {
|
|
||||||
toValue: shouldCollapse ? 1 : 0,
|
|
||||||
duration: 200,
|
|
||||||
useNativeDriver: false,
|
|
||||||
}),
|
|
||||||
]).start();
|
|
||||||
}, [textState, settings.collapseWhileTyping, animated]);
|
|
||||||
|
|
||||||
if (!self) return null;
|
|
||||||
|
|
||||||
const openAccountSheet = () => {
|
|
||||||
const fn = showYouAccountActionSheetByProp?.showYouAccountActionSheet;
|
|
||||||
if (typeof fn === "function") {
|
|
||||||
try {
|
|
||||||
fn(true, true);
|
|
||||||
return;
|
|
||||||
} catch (err) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
showUserProfileActionSheet?.({ userId: self.id, channelId: channel?.id ?? channelId });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePress = () => {
|
|
||||||
switch (settings.pressAction) {
|
|
||||||
case "profile":
|
|
||||||
showUserProfileActionSheet?.({ userId: self.id, channelId: channel?.id ?? channelId });
|
|
||||||
break;
|
|
||||||
case "server":
|
|
||||||
openAccountSheet();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLongPress = () => {
|
|
||||||
switch (settings.longPressAction) {
|
|
||||||
case "profile":
|
|
||||||
showUserProfileActionSheet?.({ userId: self.id, channelId: channel?.id ?? channelId });
|
|
||||||
break;
|
|
||||||
case "server":
|
|
||||||
openAccountSheet();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Animated.View
|
|
||||||
style={{
|
|
||||||
height: 40,
|
|
||||||
width: animated.interpolate({ inputRange: [0, 1], outputRange: [40, 0] }),
|
|
||||||
marginHorizontal: animated.interpolate({ inputRange: [0, 1], outputRange: [4, 0] }),
|
|
||||||
flexShrink: 0,
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
overflow: settings.collapseWhileTyping ? "hidden" : "visible",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Pressable
|
|
||||||
onPress={handlePress}
|
|
||||||
onLongPress={handleLongPress}
|
|
||||||
>
|
|
||||||
{Avatar && (
|
|
||||||
<Avatar
|
|
||||||
user={self}
|
|
||||||
guildId={channel?.guild_id}
|
|
||||||
status={settings.showStatusCutout ? status : undefined}
|
|
||||||
avatarDecoration={self?.avatarDecoration}
|
|
||||||
animate={!settings.collapseWhileTyping || !textState}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Pressable>
|
|
||||||
</Animated.View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const unpatches: (() => void)[] = [];
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "ChatboxAvatar",
|
|
||||||
description: "Adds your avatar to the chatbox.",
|
|
||||||
author: [Contributors.LampDelivery],
|
|
||||||
id: "chatboxavatar",
|
|
||||||
version: "v1.0.0",
|
|
||||||
requiresRestart: true,
|
|
||||||
settings: ChatboxAvatarSettings,
|
|
||||||
async start() {
|
|
||||||
if (!ChatInputActions?.type || !ChatInputSendButton?.type) return;
|
|
||||||
unpatches.push(
|
|
||||||
after("render", ChatInputSendButton.type, (args, ret) => {
|
|
||||||
setImmediate(() => {
|
|
||||||
setImmediate(() => {
|
|
||||||
if (args?.[1]?.current) {
|
|
||||||
sendBtnRef = args[1].current;
|
|
||||||
const origSetHasText = sendBtnRef?.setHasText;
|
|
||||||
unpatches.push(() => { if (sendBtnRef && origSetHasText) sendBtnRef.setHasText = origSetHasText; });
|
|
||||||
if (sendBtnRef) {
|
|
||||||
sendBtnRef.setHasText = (hasText_: boolean) => {
|
|
||||||
hasText = hasText_;
|
|
||||||
if (origSetHasText) return origSetHasText.call(sendBtnRef, hasText_);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
},
|
|
||||||
async eagerStart() {
|
|
||||||
if (!ChatInputActions?.type || !ChatInputSendButton?.type) return;
|
|
||||||
unpatches.push(
|
|
||||||
after("render", ChatInputActions.type, (args, ret) => {
|
|
||||||
return React.createElement(
|
|
||||||
View,
|
|
||||||
{ style: { flexDirection: "row", alignItems: "center" } },
|
|
||||||
ret,
|
|
||||||
React.createElement(AvatarAction)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
stop() {
|
|
||||||
for (const unpatch of unpatches) unpatch?.();
|
|
||||||
unpatches.length = 0;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
import { Stack, TableRadioGroup, TableRadioRow } from "@metro/common/components";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { ChatboxAvatarSettings, useChatboxAvatarSettings } from "./storage";
|
|
||||||
export default function ChatboxAvatarSettings() {
|
|
||||||
const settings = useChatboxAvatarSettings();
|
|
||||||
const [, forceUpdate] = React.useReducer(x => ~x, 0);
|
|
||||||
const setPressAction = (value: string) => {
|
|
||||||
useChatboxAvatarSettings.getState().updateSettings?.({ pressAction: value as ChatboxAvatarSettings["pressAction"] });
|
|
||||||
forceUpdate();
|
|
||||||
};
|
|
||||||
const setLongPressAction = (value: string) => {
|
|
||||||
useChatboxAvatarSettings.getState().updateSettings?.({ longPressAction: value as ChatboxAvatarSettings["longPressAction"] });
|
|
||||||
forceUpdate();
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<Stack style={{ padding: 16 }} spacing={16}>
|
|
||||||
<TableRadioGroup
|
|
||||||
value={settings.pressAction}
|
|
||||||
onChange={setPressAction}
|
|
||||||
title="Avatar Press Action"
|
|
||||||
>
|
|
||||||
<TableRadioRow value="profile" label="Open Profile" />
|
|
||||||
<TableRadioRow value="server" label="Open Status Picker" />
|
|
||||||
</TableRadioGroup>
|
|
||||||
<TableRadioGroup
|
|
||||||
value={settings.longPressAction}
|
|
||||||
onChange={setLongPressAction}
|
|
||||||
title="Avatar Long-Press Action"
|
|
||||||
>
|
|
||||||
<TableRadioRow value="profile" label="Open Profile" />
|
|
||||||
<TableRadioRow value="server" label="Open Status Picker" />
|
|
||||||
</TableRadioGroup>
|
|
||||||
<TableRadioGroup
|
|
||||||
value={settings.showStatusCutout ? "true" : "false"}
|
|
||||||
onChange={v => {
|
|
||||||
useChatboxAvatarSettings.getState().updateSettings?.({ showStatusCutout: v === "true" });
|
|
||||||
forceUpdate();
|
|
||||||
}}
|
|
||||||
title="Status Icon"
|
|
||||||
>
|
|
||||||
<TableRadioRow value="true" label="Show" />
|
|
||||||
<TableRadioRow value="false" label="Hide" />
|
|
||||||
</TableRadioGroup>
|
|
||||||
<TableRadioGroup
|
|
||||||
value={settings.collapseWhileTyping ? "true" : "false"}
|
|
||||||
onChange={v => {
|
|
||||||
useChatboxAvatarSettings.getState().updateSettings?.({ collapseWhileTyping: v === "true" });
|
|
||||||
forceUpdate();
|
|
||||||
}}
|
|
||||||
title="Collapse While Typing"
|
|
||||||
>
|
|
||||||
<TableRadioRow value="true" label="Collapse avatar while typing" />
|
|
||||||
<TableRadioRow value="false" label="Always show avatar" />
|
|
||||||
</TableRadioGroup>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
import { createPluginStore } from "@api/storage";
|
|
||||||
|
|
||||||
export interface ChatboxAvatarSettings {
|
|
||||||
replaceButton: "gallery" | "apps" | "gift";
|
|
||||||
pressAction: "profile" | "server";
|
|
||||||
longPressAction: "profile" | "server";
|
|
||||||
serverId?: string;
|
|
||||||
showStatusCutout?: boolean;
|
|
||||||
collapseWhileTyping?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const {
|
|
||||||
useStore: useChatboxAvatarSettings,
|
|
||||||
settings: chatboxAvatarSettings,
|
|
||||||
} = createPluginStore<ChatboxAvatarSettings>("chatboxavatar", {
|
|
||||||
replaceButton: "gallery",
|
|
||||||
pressAction: "profile",
|
|
||||||
longPressAction: "server",
|
|
||||||
serverId: "",
|
|
||||||
showStatusCutout: false,
|
|
||||||
collapseWhileTyping: false,
|
|
||||||
});
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Chatbox Avatar",
|
|
||||||
"description": "Quickly view your profile from the chatbox.",
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Cynosphere",
|
|
||||||
"id": "150745989836308480"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "kmmiio99o",
|
|
||||||
"id": "879393496627306587"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"main": "src/index.tsx",
|
|
||||||
"vendetta": {
|
|
||||||
"icon": "UserSquareIcon"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,165 +0,0 @@
|
||||||
import { after } from "@vendetta/patcher";
|
|
||||||
import { findByName, findByProps, findByStoreName, findByDisplayName } from "@vendetta/metro";
|
|
||||||
import { ReactNative } from "@vendetta/metro/common";
|
|
||||||
import { plugin } from "@vendetta";
|
|
||||||
import React, { useEffect, useRef } from "react";
|
|
||||||
|
|
||||||
import ChatboxAvatarSettings from "./settings";
|
|
||||||
|
|
||||||
const Flux = findByProps("useStateFromStores");
|
|
||||||
const ChatInputActions = findByDisplayName("ChatInputActions");
|
|
||||||
const ChatInputSendButton = findByDisplayName("ChatInputSendButton");
|
|
||||||
let hasText = false;
|
|
||||||
let sendBtnRef: { setHasText?: (hasText: boolean) => void } | undefined;
|
|
||||||
const { Pressable, View, Animated } = ReactNative;
|
|
||||||
|
|
||||||
const avatarCollapse = new Animated.Value(0);
|
|
||||||
|
|
||||||
const Avatar = findByProps("default", "AvatarSizes", "getStatusSize")?.default;
|
|
||||||
|
|
||||||
const UserStore = findByStoreName("UserStore");
|
|
||||||
const SelectedChannelStore = findByStoreName("SelectedChannelStore");
|
|
||||||
const ChannelStore = findByStoreName("ChannelStore");
|
|
||||||
const SelfPresenceStore = findByStoreName("SelfPresenceStore");
|
|
||||||
const showUserProfileActionSheet = findByName("showUserProfileActionSheet");
|
|
||||||
const showYouAccountActionSheetByProp = findByProps("showYouAccountActionSheet");
|
|
||||||
|
|
||||||
function AvatarAction() {
|
|
||||||
const [textState, setTextState] = React.useState(false);
|
|
||||||
const self = Flux?.useStateFromStores?.([UserStore], () => UserStore?.getCurrentUser?.());
|
|
||||||
const status = Flux?.useStateFromStores?.([SelfPresenceStore], () => SelfPresenceStore?.getStatus?.());
|
|
||||||
const settings = plugin.storage;
|
|
||||||
const channelId = Flux?.useStateFromStores?.([SelectedChannelStore], () => SelectedChannelStore?.getCurrentlySelectedChannelId?.());
|
|
||||||
const channel = Flux?.useStateFromStores?.([ChannelStore], () => ChannelStore?.getChannel?.(channelId), [channelId]);
|
|
||||||
|
|
||||||
const animated = useRef(avatarCollapse).current;
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
setTextState(hasText);
|
|
||||||
}, 100);
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const shouldCollapse = settings.collapseWhileTyping && textState;
|
|
||||||
Animated.parallel([
|
|
||||||
Animated.timing(animated, {
|
|
||||||
toValue: shouldCollapse ? 1 : 0,
|
|
||||||
duration: 200,
|
|
||||||
useNativeDriver: false,
|
|
||||||
}),
|
|
||||||
]).start();
|
|
||||||
}, [textState, settings.collapseWhileTyping, animated]);
|
|
||||||
|
|
||||||
if (!self) return null;
|
|
||||||
|
|
||||||
const openAccountSheet = () => {
|
|
||||||
const fn = showYouAccountActionSheetByProp?.showYouAccountActionSheet;
|
|
||||||
if (typeof fn === "function") {
|
|
||||||
try {
|
|
||||||
fn(true, true);
|
|
||||||
return;
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
showUserProfileActionSheet?.({ userId: self.id, channelId: channel?.id ?? channelId });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePress = () => {
|
|
||||||
switch (settings.pressAction) {
|
|
||||||
case "profile":
|
|
||||||
showUserProfileActionSheet?.({ userId: self.id, channelId: channel?.id ?? channelId });
|
|
||||||
break;
|
|
||||||
case "server":
|
|
||||||
openAccountSheet();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLongPress = () => {
|
|
||||||
switch (settings.longPressAction) {
|
|
||||||
case "profile":
|
|
||||||
showUserProfileActionSheet?.({ userId: self.id, channelId: channel?.id ?? channelId });
|
|
||||||
break;
|
|
||||||
case "server":
|
|
||||||
openAccountSheet();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Animated.View
|
|
||||||
style={{
|
|
||||||
height: 40,
|
|
||||||
width: animated.interpolate({ inputRange: [0, 1], outputRange: [40, 0] }),
|
|
||||||
marginHorizontal: animated.interpolate({ inputRange: [0, 1], outputRange: [4, 0] }),
|
|
||||||
flexShrink: 0,
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
overflow: settings.collapseWhileTyping ? "hidden" : "visible",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Pressable
|
|
||||||
onPress={handlePress}
|
|
||||||
onLongPress={handleLongPress}
|
|
||||||
>
|
|
||||||
{Avatar && (
|
|
||||||
<Avatar
|
|
||||||
user={self}
|
|
||||||
guildId={channel?.guild_id}
|
|
||||||
status={settings.showStatusCutout ? status : undefined}
|
|
||||||
avatarDecoration={self?.avatarDecoration}
|
|
||||||
animate={!settings.collapseWhileTyping || !textState}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Pressable>
|
|
||||||
</Animated.View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const unpatches: (() => void)[] = [];
|
|
||||||
|
|
||||||
export default {
|
|
||||||
onLoad() {
|
|
||||||
if (!ChatInputActions?.prototype?.render || !ChatInputSendButton?.prototype?.render) return;
|
|
||||||
unpatches.push(
|
|
||||||
after("render", ChatInputActions.prototype, (args, ret) => {
|
|
||||||
return React.createElement(
|
|
||||||
View,
|
|
||||||
{ style: { flexDirection: "row", alignItems: "center" } },
|
|
||||||
ret,
|
|
||||||
React.createElement(AvatarAction)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
unpatches.push(
|
|
||||||
after("render", ChatInputSendButton.prototype, (args, ret) => {
|
|
||||||
setImmediate(() => {
|
|
||||||
setImmediate(() => {
|
|
||||||
if (args?.[1]?.current) {
|
|
||||||
sendBtnRef = args[1].current;
|
|
||||||
const origSetHasText = sendBtnRef?.setHasText;
|
|
||||||
unpatches.push(() => { if (sendBtnRef && origSetHasText) sendBtnRef.setHasText = origSetHasText; });
|
|
||||||
if (sendBtnRef) {
|
|
||||||
sendBtnRef.setHasText = (hasText_: boolean) => {
|
|
||||||
hasText = hasText_;
|
|
||||||
if (origSetHasText) return origSetHasText.call(sendBtnRef, hasText_);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onUnload() {
|
|
||||||
for (const unpatch of unpatches) unpatch?.();
|
|
||||||
unpatches.length = 0;
|
|
||||||
},
|
|
||||||
settings: ChatboxAvatarSettings,
|
|
||||||
};
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
import { plugin } from "@vendetta";
|
|
||||||
import { useProxy } from "@vendetta/storage";
|
|
||||||
import { React, ReactNative } from "@vendetta/metro/common";
|
|
||||||
import { findByProps } from "@vendetta/metro";
|
|
||||||
|
|
||||||
const { ScrollView } = ReactNative;
|
|
||||||
const { TableRowGroup, TableCheckboxRow, Stack } = findByProps(
|
|
||||||
"TableCheckboxRow", "TableRowGroup", "Stack"
|
|
||||||
);
|
|
||||||
|
|
||||||
plugin.storage.pressAction ??= "profile";
|
|
||||||
plugin.storage.longPressAction ??= "server";
|
|
||||||
plugin.storage.showStatusCutout ??= false;
|
|
||||||
plugin.storage.collapseWhileTyping ??= false;
|
|
||||||
|
|
||||||
export default function ChatboxAvatarSettings() {
|
|
||||||
useProxy(plugin.storage);
|
|
||||||
const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 10 }}>
|
|
||||||
<Stack spacing={8}>
|
|
||||||
<TableRowGroup title="Avatar Press Action">
|
|
||||||
<TableCheckboxRow
|
|
||||||
label="Open Profile"
|
|
||||||
checked={plugin.storage.pressAction === "profile"}
|
|
||||||
onPress={() => {
|
|
||||||
plugin.storage.pressAction = "profile";
|
|
||||||
forceUpdate();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TableCheckboxRow
|
|
||||||
label="Open Status Picker"
|
|
||||||
checked={plugin.storage.pressAction === "server"}
|
|
||||||
onPress={() => {
|
|
||||||
plugin.storage.pressAction = "server";
|
|
||||||
forceUpdate();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TableRowGroup>
|
|
||||||
|
|
||||||
<TableRowGroup title="Avatar Long-Press Action">
|
|
||||||
<TableCheckboxRow
|
|
||||||
label="Open Profile"
|
|
||||||
checked={plugin.storage.longPressAction === "profile"}
|
|
||||||
onPress={() => {
|
|
||||||
plugin.storage.longPressAction = "profile";
|
|
||||||
forceUpdate();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TableCheckboxRow
|
|
||||||
label="Open Status Picker"
|
|
||||||
checked={plugin.storage.longPressAction === "server"}
|
|
||||||
onPress={() => {
|
|
||||||
plugin.storage.longPressAction = "server";
|
|
||||||
forceUpdate();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TableRowGroup>
|
|
||||||
|
|
||||||
<TableRowGroup title="Status Icon">
|
|
||||||
<TableCheckboxRow
|
|
||||||
label="Show"
|
|
||||||
checked={!!plugin.storage.showStatusCutout}
|
|
||||||
onPress={() => {
|
|
||||||
plugin.storage.showStatusCutout = true;
|
|
||||||
forceUpdate();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TableCheckboxRow
|
|
||||||
label="Hide"
|
|
||||||
checked={!plugin.storage.showStatusCutout}
|
|
||||||
onPress={() => {
|
|
||||||
plugin.storage.showStatusCutout = false;
|
|
||||||
forceUpdate();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TableRowGroup>
|
|
||||||
|
|
||||||
<TableRowGroup title="Collapse While Typing">
|
|
||||||
<TableCheckboxRow
|
|
||||||
label="Collapse avatar while typing"
|
|
||||||
checked={!!plugin.storage.collapseWhileTyping}
|
|
||||||
onPress={() => {
|
|
||||||
plugin.storage.collapseWhileTyping = true;
|
|
||||||
forceUpdate();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TableCheckboxRow
|
|
||||||
label="Always show avatar"
|
|
||||||
checked={!plugin.storage.collapseWhileTyping}
|
|
||||||
onPress={() => {
|
|
||||||
plugin.storage.collapseWhileTyping = false;
|
|
||||||
forceUpdate();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TableRowGroup>
|
|
||||||
</Stack>
|
|
||||||
</ScrollView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -7,6 +7,8 @@ const Constants = {
|
||||||
APPLICATION_ID: "1368513179272871956",
|
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,
|
||||||
|
// Libre.fm specifically requested a 1-minute minimum to reduce load
|
||||||
|
LIBREFM_MIN_UPDATE_INTERVAL: 60,
|
||||||
// How many times to retry failed API calls
|
// How many times to retry failed API calls
|
||||||
MAX_RETRY_ATTEMPTS: 3,
|
MAX_RETRY_ATTEMPTS: 3,
|
||||||
// Wait 5 seconds between retries
|
// Wait 5 seconds between retries
|
||||||
|
|
|
||||||
|
|
@ -381,10 +381,18 @@ class PluginManager {
|
||||||
// Don't reset track state - let updateActivity() handle whether to update
|
// Don't reset track state - let updateActivity() handle whether to update
|
||||||
await this.updateActivity();
|
await this.updateActivity();
|
||||||
|
|
||||||
|
const currentService = currentSettings.service;
|
||||||
|
let minInterval: number = Constants.MIN_UPDATE_INTERVAL;
|
||||||
|
|
||||||
|
if (currentService === "librefm") {
|
||||||
|
minInterval = Constants.LIBREFM_MIN_UPDATE_INTERVAL;
|
||||||
|
log(`Libre.fm active: enforcing ${minInterval}s minimum update interval`);
|
||||||
|
}
|
||||||
|
|
||||||
const interval = Math.max(
|
const interval = Math.max(
|
||||||
(Number(currentSettings.timeInterval) ||
|
(Number(currentSettings.timeInterval) ||
|
||||||
Constants.DEFAULT_SETTINGS.timeInterval) * 1000,
|
Constants.DEFAULT_SETTINGS.timeInterval) * 1000,
|
||||||
Constants.MIN_UPDATE_INTERVAL * 1000,
|
minInterval * 1000,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.updateTimer = setInterval(() => this.updateActivity(), interval);
|
this.updateTimer = setInterval(() => this.updateActivity(), interval);
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,9 @@ export default function DisplaySettingsPage() {
|
||||||
useProxy(plugin.storage);
|
useProxy(plugin.storage);
|
||||||
const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
|
const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
|
||||||
|
|
||||||
|
const currentService = getStorage("service");
|
||||||
|
const isLibreFm = currentService === "librefm";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 10 }}>
|
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 10 }}>
|
||||||
<Stack spacing={8}>
|
<Stack spacing={8}>
|
||||||
|
|
@ -29,24 +32,33 @@ export default function DisplaySettingsPage() {
|
||||||
}}
|
}}
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
<TextInput
|
{isLibreFm ? (
|
||||||
placeholder={`Update Interval (Default: ${Constants.DEFAULT_SETTINGS.timeInterval}s)`}
|
<TableRow
|
||||||
value={String(
|
label="Update Interval Disabled"
|
||||||
getStorage(
|
subLabel="Libre.fm requires a fixed 60s update interval to prevent rate limiting"
|
||||||
"timeInterval",
|
disabled={true}
|
||||||
Constants.DEFAULT_SETTINGS.timeInterval,
|
dimmed={true}
|
||||||
),
|
/>
|
||||||
)}
|
) : (
|
||||||
onChange={(v: string) => {
|
<TextInput
|
||||||
const interval = Number(v);
|
placeholder={`Update Interval (Default: ${Constants.DEFAULT_SETTINGS.timeInterval}s)`}
|
||||||
if (interval >= Constants.MIN_UPDATE_INTERVAL) {
|
value={String(
|
||||||
setStorage("timeInterval", interval);
|
getStorage(
|
||||||
forceUpdate();
|
"timeInterval",
|
||||||
}
|
Constants.DEFAULT_SETTINGS.timeInterval,
|
||||||
}}
|
),
|
||||||
keyboardType="numeric"
|
)}
|
||||||
isClearable
|
onChange={(v: string) => {
|
||||||
/>
|
const interval = Number(v);
|
||||||
|
if (interval >= Constants.MIN_UPDATE_INTERVAL) {
|
||||||
|
setStorage("timeInterval", interval);
|
||||||
|
forceUpdate();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
keyboardType="numeric"
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</TableRowGroup>
|
</TableRowGroup>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue