Questify: Fixes + improvements. (#408)

* Fixes and improvements.

* Lint.
This commit is contained in:
Etorix 2025-09-20 14:34:19 -07:00 • committed by GitHub
parent 60dcdc966b
commit eff7bbe628
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 494 additions and 102 deletions

View file

@ -12,10 +12,10 @@ import { EquicordDevs } from "@utils/constants";
import { getIntlMessage } from "@utils/index";
import definePlugin, { StartAt } from "@utils/types";
import { onceReady } from "@webpack";
import { ContextMenuApi, Menu, NavigationRouter } from "@webpack/common";
import { ContextMenuApi, Menu, NavigationRouter, useEffect, useState } from "@webpack/common";
import { JSX } from "react";
import { addIgnoredQuest, autoFetchCompatible, fetchAndAlertQuests, maximumAutoFetchIntervalValue, minimumAutoFetchIntervalValue, questIsIgnored, removeIgnoredQuest, rerenderQuests, settings, startAutoFetchingQuests, stopAutoFetchingQuests, validateAndOverwriteIgnoredQuests } from "./settings";
import { addIgnoredQuest, addRerenderCallback, autoFetchCompatible, fetchAndAlertQuests, maximumAutoFetchIntervalValue, minimumAutoFetchIntervalValue, questIsIgnored, removeIgnoredQuest, rerenderQuests, settings, startAutoFetchingQuests, stopAutoFetchingQuests, validateAndOverwriteIgnoredQuests } from "./settings";
import { ExcludedQuestMap, GuildlessServerListItem, Quest, QuestIcon, QuestMap, QuestStatus, RGB } from "./utils/components";
import { adjustRGB, decimalToRGB, fetchAndDispatchQuests, formatLowerBadge, getFormattedNow, getIgnoredQuestIDs, getQuestStatus, isDarkish, leftClick, middleClick, normalizeQuestName, q, QuestifyLogger, questPath, QuestsStore, refreshQuest, reportPlayGameQuestProgress, reportVideoQuestProgress, rightClick, setIgnoredQuestIDs, waitUntilEnrolled } from "./utils/misc";
@ -51,16 +51,16 @@ function questMenuIgnoreAllClicked(): void {
settings.store.unclaimedUnignoredQuests = 0;
}
function showQuestsButton(questButtonDisplay: string, unclaimedUnignoredQuests: number, onQuestsPage: boolean): boolean {
const canShow = questButtonDisplay !== "never";
const alwaysShow = questButtonDisplay === "always";
return canShow && (alwaysShow || !!unclaimedUnignoredQuests || onQuestsPage);
}
export function QuestButton(): JSX.Element {
const { questButtonDisplay, questButtonUnclaimed, questButtonBadgeColor, unclaimedUnignoredQuests, onQuestsPage } = settings.use(["questButtonDisplay", "questButtonUnclaimed", "questButtonBadgeColor", "unclaimedUnignoredQuests", "onQuestsPage"]);
const questButtonBadgeColorRGB = questButtonBadgeColor === null ? null : decimalToRGB(questButtonBadgeColor);
function showQuestsButton(): boolean {
const canShow = questButtonDisplay !== "never";
const alwaysShow = questButtonDisplay === "always";
return canShow && (alwaysShow || !!unclaimedUnignoredQuests || onQuestsPage);
}
function handleClick(event: React.MouseEvent<Element>) {
// ListItem does not support onAuxClick, so we have to listen for mousedown events.
// Ignore left and right clicks sent via mousedown events to prevent double events.
@ -128,7 +128,7 @@ export function QuestButton(): JSX.Element {
icon={QuestIcon(26, 26)}
tooltip="Quests"
showPill={true}
isVisible={showQuestsButton()}
isVisible={showQuestsButton(questButtonDisplay, unclaimedUnignoredQuests, onQuestsPage)}
isSelected={onQuestsPage}
hasUnread={!!unclaimedUnignoredQuests && ["pill", "both"].includes(questButtonUnclaimed)}
lowerBadgeProps={lowerBadgeProps}
@ -325,32 +325,8 @@ export function getQuestTileClasses(originalClasses: string, quest: Quest, color
return returnClasses.join(" ");
}
function preprocessQuests(quests: Quest[]): Quest[] {
const {
ignoredQuestIDs,
ignoredQuestProfile,
reorderQuests,
unclaimedSubsort,
claimedSubsort,
ignoredSubsort,
expiredSubsort,
makeMobileQuestsDesktopCompatible,
completeVideoQuestsInBackground,
completeGameQuestsInBackground,
triggerQuestsRerender
} = settings.use([
"ignoredQuestIDs",
"ignoredQuestProfile",
"reorderQuests",
"unclaimedSubsort",
"claimedSubsort",
"ignoredSubsort",
"expiredSubsort",
"makeMobileQuestsDesktopCompatible",
"completeVideoQuestsInBackground",
"completeGameQuestsInBackground",
"triggerQuestsRerender"
]);
function makeDesktopCompatible(quests: Quest[]): void {
const { makeMobileQuestsDesktopCompatible, triggerQuestsRerender } = settings.use(["makeMobileQuestsDesktopCompatible", "triggerQuestsRerender"]);
if (makeMobileQuestsDesktopCompatible) {
quests.forEach(quest => {
@ -378,8 +354,36 @@ function preprocessQuests(quests: Quest[]): Quest[] {
patchedMobileQuests.clear();
}
}
if (!reorderQuests || !reorderQuests.trim()) {
function sortQuests(quests: Quest[], skip?: boolean): Quest[] {
const {
ignoredQuestIDs,
ignoredQuestProfile,
reorderQuests,
unclaimedSubsort,
claimedSubsort,
ignoredSubsort,
expiredSubsort,
completeVideoQuestsInBackground,
completeGameQuestsInBackground,
triggerQuestsRerender
} = settings.use([
"ignoredQuestIDs",
"ignoredQuestProfile",
"reorderQuests",
"unclaimedSubsort",
"claimedSubsort",
"ignoredSubsort",
"expiredSubsort",
"completeVideoQuestsInBackground",
"completeGameQuestsInBackground",
"triggerQuestsRerender"
]);
makeDesktopCompatible(quests);
if (skip || !reorderQuests?.trim()) {
return quests;
}
@ -786,6 +790,44 @@ function getQuestPanelOverride(): Quest | null {
return closestQuest;
}
function disguiseHomeButton(location: string): boolean {
const { questButtonDisplay, unclaimedUnignoredQuests, onQuestsPage } = settings.use(["questButtonDisplay", "unclaimedUnignoredQuests", "onQuestsPage"]);
if (!showQuestsButton(questButtonDisplay, unclaimedUnignoredQuests, onQuestsPage)) {
return false;
}
return location === questPath;
}
function useQuestRerender(): number {
const { triggerQuestsRerender } = settings.use(["triggerQuestsRerender"]);
const [renderTrigger, setRenderTrigger] = useState(0);
useEffect(() => addRerenderCallback(() => setRenderTrigger(prev => prev + 1)), []);
return renderTrigger;
}
function getLastSortChoice(): string | null {
const { rememberQuestPageSort, lastQuestPageSort } = settings.store;
return rememberQuestPageSort ? lastQuestPageSort : "questify";
}
function getLastFilterChoices(): { group: string; filter: string; }[] | null {
const { rememberQuestPageFilters, lastQuestPageFilters } = settings.store;
return rememberQuestPageFilters ? Object.values(lastQuestPageFilters) : null;
}
function setLastSortChoice(sort: string): void {
const { rememberQuestPageFilters } = settings.use(["rememberQuestPageFilters"]);
settings.store.lastQuestPageSort = sort;
}
function setLastFilterChoices(filters: { group: string; filter: string; }[]): void {
const { rememberQuestPageFilters } = settings.use(["rememberQuestPageFilters"]);
if (!filters || !Object.keys(filters).length || !Object.values(filters).every(f => f.group && f.filter)) { return; }
settings.store.lastQuestPageFilters = JSON.parse(JSON.stringify(filters)).reduce((acc, item) => ({ ...acc, [item.filter]: item }), {});
}
export default definePlugin({
name: "Questify",
description: "Enhance your Quest experience with a suite of features, or disable them entirely if they're not your thing.",
@ -794,22 +836,29 @@ export default definePlugin({
startAt: StartAt.Init, // Needed in order to beat Read All Messages to inserting above the server list.
settings,
sortQuests,
formatLowerBadge,
getQuestTileStyle,
preprocessQuests,
getQuestTileClasses,
shouldPreloadQuestAssets,
makeDesktopCompatible,
shouldHideQuestPopup,
shouldHideDiscoveryTab,
shouldPreloadQuestAssets,
shouldPreventFetchingQuests,
shouldHideBadgeOnUserProfiles,
shouldHideGiftInventoryRelocationNotice,
shouldHideFriendsListActiveNowPromotion,
shouldDisableQuestAcceptedButton,
getQuestPanelOverride,
getQuestAcceptedButtonText,
processQuestForAutoComplete,
getQuestAcceptedButtonText,
getQuestPanelOverride,
setLastFilterChoices,
getLastFilterChoices,
activeQuestIntervals,
disguiseHomeButton,
getLastSortChoice,
setLastSortChoice,
useQuestRerender,
patches: [
{
@ -964,26 +1013,109 @@ export default definePlugin({
// Encourages reward icons to load quicker if the setting is enabled.
match: /(onReceiveErrorHints:\i,)isVisibleInViewport:(\i)/,
replace: "$1isVisibleInViewport:$self.shouldPreloadQuestAssets()?true:$2"
}
},
]
},
{
// Sorts the "All Quests" tab Quest tiles.
// Sorts the "Claimed Quests" tab Quest tiles.
// Also sets mobile-only Quests as desktop compatible if the setting is enabled.
// Sorts the "Claimed Quests" tabs.
find: ".ALL)}):(",
group: true,
replacement: [
{
match: /(claimedQuests:(\i).{0,50}?;)/,
replace: "$1$2=$self.preprocessQuests($2);"
replace: "$1$2=$self.sortQuests($2);"
},
]
},
{
// Sorts the "All Quests" tab.
find: "CLAIMED=\"claimed\",",
group: true,
replacement: [
{
// Run Questify's sort function every time due to hook requirements but return
// early if not applicable. If the sort method is set to "Questify", replace the
// quests with the sorted ones. Also, setup a trigger to rerender the memo.
match: /(?<=function \i\((\i).{0,100}?useRef\(\i\);)(return \i.useMemo\(\(\)=>{)/,
replace: "const questRerenderTrigger=$self.useQuestRerender();const questifySorted=$self.sortQuests($1,arguments[1].sortMethod!==\"questify\");$2if(arguments[1].sortMethod===\"questify\"){$1=questifySorted;};"
},
{
match: /(quests:(\i).{0,50}?;)/,
replace: "$1$2=$self.preprocessQuests($2);"
// Account for Quest status changes.
match: /return (\i).current;/,
replace: "null;"
},
{
// If we already applied Questify's sort, skip further sorting.
match: /(?<=(\i)\)\);)(return )((\i).sort)/,
replace: "$2$1===\"questify\"?$4:$3"
},
{
// Add the trigger to the memo for rerendering Quests order due to progress changes, etc.
match: /(?<=current=\i,\i},\[\i,\i,\i)/,
replace: ",questRerenderTrigger,questifySorted"
}
]
},
{
// Adds the "Questify" sort option to the sort enum.
find: "SUGGESTED=\"suggested\",",
replacement: {
match: /return ((\i).SUGGESTED="suggested",)/,
replace: "return $2.QUESTIFY=\"questify\",$1"
}
},
{
// Adds the "Questify" sort option to the sort dropdown.
find: "radioItemTitle,options",
group: true,
replacement: [
{
match: /(?=case (\i.\i).SUGGESTED)/,
replace: "case $1.QUESTIFY:return \"Questify\";"
}
]
},
{
// Loads the last used sort method and filter choices.
// Defaults to sorting by "Questify" and no filters.
find: "QUEST_HOME_SORT_METHOD_CHANGED,",
group: true,
replacement: [
{
// Set the initial sort method.
match: /(\i.\i.SUGGESTED)/,
replace: "$self.getLastSortChoice()??$1"
},
{
// Set the initial filters.
match: /(useState\()(\i\),{quests)/,
replace: "$1$self.getLastFilterChoices()??$2"
},
{
// Update the last used sort method when it changes.
match: /(onChange:)(\i)(.{0,40}?selectedSortMethod)/,
replace: "$1(value)=>{$self.settings.store.lastQuestPageSort=value;$2(value);}$3"
},
{
// Update the last used filter choices when they change.
match: /(onChange:)(\i)(.{0,40}?selectedFilters)/,
replace: "$1(value)=>{$self.settings.store.lastQuestPageFilters=value.reduce((acc,item)=>({...acc,[item.filter]:item}),{});$2(value);}$3"
},
{
// Update the last used sort and filter choices when the toggle setting for either is changed.
match: /(\[(\i),\i\]=\i.useState.{0,80}?\[(\i),\i\]=\i.useState.{0,350}?)(return \i.useEffect)/,
replace: "$1$self.setLastSortChoice($2);$self.setLastFilterChoices($3);$4"
}
]
},
{
// Prevent scrolling to a sponsored Quest.
find: "Id(\"quest-tile-\".concat",
replacement: {
match: /(?=document.getElementById)/,
replace: "null&&"
}
},
{
// Whether preloading assets is enabled or not, the placeholders loading
// before the assets causes a lot of element shifting, whereas if
@ -1005,14 +1137,19 @@ export default definePlugin({
replace: "$1$self.processQuestForAutoComplete($3)&&$2"
},
{
// Start Quest
match: /(onClick:.{0,10}?{)(.{0,5}?0,\i.\i\)\((\i))/,
replace: "$1const questifyContinue=$self.processQuestForAutoComplete($3);$2"
// Start Video & Play Game Quests.
match: /(?<=onClick:async\(\)=>{)/,
replace: "const startingAutoComplete=$self.processQuestForAutoComplete(arguments[0].quest);"
},
{
// Open Video Modal
// Prevent the new Video Quest entry point.
match: /(?<=QUEST_HOME_DESKTOP\))/,
replace: "&&false"
},
{
// Conditionally open the Video Quest modal.
match: /(\i\?)?(\(0,\i.openVideoQuestModal\)\({quest:(\i))/,
replace: "$1questifyContinue&&$2"
replace: "$1startingAutoComplete&&$2"
},
{
// The "Resume (XX:XX)" text is changed to "Watching (XX:XX)" if the Quest is active.
@ -1020,12 +1157,26 @@ export default definePlugin({
replace: "$1$self.getQuestAcceptedButtonText(arguments[0].quest)??",
},
{
// Add trigger to memo for rerendering the progress label.
// Setup a trigger to rerender the memo.
match: /(?=return \i.useMemo)/,
replace: "const questRerenderTrigger=$self.useQuestRerender();"
},
{
// Add the trigger to the memo for rerendering the progress label.
match: /(\i.intl.string\(\i.\i#{intl::QUESTS_SEE_CODE}\)}\)}},\[)/,
replace: "$1$self.settings.store.triggerQuestsRerender,"
replace: "$1questRerenderTrigger,"
},
]
},
// This patch covers the new entry point blocked in the above group.
// If the old entry point gets removed in the future, this will be useful.
// {
// find: "CAPTCHA_FAILED:",
// replacement: {
// match: /(?<=SUCCESS:)(\i\({)/,
// replace: "$self.processQuestForAutoComplete(arguments[0])&&$1"
// }
// },
{
// Sets intervals to progress Play Game Quests in the background.
// Triggers if a Quest has already been started but was interrupted, such as by a reload.
@ -1052,6 +1203,15 @@ export default definePlugin({
}
]
},
{
// Prevents the new Quests location from counting as part of the
// DM button highlight logic while the Quest button is visible.
find: "GLOBAL_DISCOVERY),",
replacement: {
match: /(pathname:(\i)}.{0,250}?return )/,
replace: "$1$self.disguiseHomeButton($2)?false:"
}
}
],
contextMenus: {

View file

@ -13,7 +13,7 @@ import { Button, ColorPicker, ContextMenuApi, Forms, Menu, Select, TextInput, us
import { JSX } from "react";
import { activeQuestIntervals, getQuestTileClasses, getQuestTileStyle } from "./index";
import { DynamicDropdown, DynamicDropdownSettingOption, ExcludedQuest, GuildlessServerListItem, Quest, QuestIcon, QuestStatus, QuestTile, RadioGroup, RadioOption, SelectOption, SoundIcon } from "./utils/components";
import { DynamicDropdown, DynamicDropdownSettingOption, ExcludedQuest, GuildlessServerListItem, Quest, QuestIcon, QuestRewardType, QuestStatus, QuestTile, RadioGroup, RadioOption, SelectOption, SoundIcon } from "./utils/components";
import { decimalToRGB, fetchAndDispatchQuests, getFormattedNow, getIgnoredQuestIDs, getQuestStatus, isDarkish, isSoundAllowed, leftClick, middleClick, q, QuestifyLogger, QuestsStore, rightClick, setIgnoredQuestIDs, validCommaSeparatedList } from "./utils/misc";
let autoFetchInterval: null | ReturnType<typeof setInterval> = null;
@ -32,11 +32,29 @@ const defaultFetchQuestsAlert = "discodo";
export const minimumAutoFetchIntervalValue = 30 * 60;
export const maximumAutoFetchIntervalValue = 12 * 60 * 60;
const rerenderCallbacks = new Set<() => void>();
export function addRerenderCallback(callback: () => void): () => void {
rerenderCallbacks.add(callback);
return () => rerenderCallbacks.delete(callback);
}
export function rerenderQuests(): void {
settings.store.triggerQuestsRerender = !settings.store.triggerQuestsRerender;
rerenderCallbacks.forEach(callback => callback());
}
export function fetchAndAlertQuests(source: string, logger: Logger): void {
const { questRewardIncludeRewardCode, questRewardIncludeNitroCode, questRewardIncludeCollectibles, questRewardIncludeInGame, questRewardIncludeOrbs } = settings.store;
const rewardTypeToSettingMap = {
[QuestRewardType.REWARD_CODE]: questRewardIncludeRewardCode,
[QuestRewardType.IN_GAME]: questRewardIncludeInGame,
[QuestRewardType.COLLECTIBLE]: questRewardIncludeCollectibles,
[QuestRewardType.VIRTUAL_CURRENCY]: questRewardIncludeOrbs,
[QuestRewardType.FRACTIONAL_PREMIUM]: questRewardIncludeNitroCode
};
const currentQuests = Array.from(QuestsStore.quests.values()) as Quest[];
fetchAndDispatchQuests(source, logger).then(newQuests => {
@ -44,10 +62,15 @@ export function fetchAndAlertQuests(source: string, logger: Logger): void {
const currentIds = new Set(currentQuests.map((q: Quest) => q.id));
const newOnly = newQuests.filter((q: Quest) => !currentIds.has(q.id));
const newOnlyFiltered = newOnly.filter(quest => {
const rewardType = getQuestRewardType(quest);
return rewardType in rewardTypeToSettingMap && rewardTypeToSettingMap[rewardType];
});
if (newOnly.length > 0) {
const shouldAlert = settings.store.fetchingQuestsAlert;
if (shouldAlert) {
if (shouldAlert && newOnlyFiltered.length > 0) {
logger.info(`[${getFormattedNow()}] New Quests detected. Playing alert sound.`);
playAudio(shouldAlert);
} else {
@ -144,7 +167,23 @@ export function questIsIgnored(questID: string): boolean {
return ignoredQuests.includes(questID);
}
function getQuestRewardType(quest: Quest): QuestRewardType {
const reward = quest.config.rewardsConfig.rewards[0];
if (!(reward.type in QuestRewardType)) return QuestRewardType.UNKNOWN;
return reward.type as QuestRewardType;
}
export function validateIgnoredQuests(ignoredQuests?: string[], questsData?: Quest[]): [string[], number] {
const { questRewardIncludeRewardCode, questRewardIncludeNitroCode, questRewardIncludeCollectibles, questRewardIncludeInGame, questRewardIncludeOrbs } = settings.store;
const rewardTypeToSettingMap = {
[QuestRewardType.REWARD_CODE]: questRewardIncludeRewardCode,
[QuestRewardType.IN_GAME]: questRewardIncludeInGame,
[QuestRewardType.COLLECTIBLE]: questRewardIncludeCollectibles,
[QuestRewardType.VIRTUAL_CURRENCY]: questRewardIncludeOrbs,
[QuestRewardType.FRACTIONAL_PREMIUM]: questRewardIncludeNitroCode
};
const quests = questsData ?? Array.from(QuestsStore.quests.values()) as Quest[];
const excludedQuests = Array.from(QuestsStore.excludedQuests.values()) as ExcludedQuest[];
const currentlyIgnored = ignoredQuests ? new Set(ignoredQuests) : new Set(getIgnoredQuestIDs(questsData?.[0]?.userStatus?.userId));
@ -157,7 +196,11 @@ export function validateIgnoredQuests(ignoredQuests?: string[], questsData?: Que
if (currentlyIgnored.has(quest.id)) {
validIgnored.add(quest.id);
} else if (questStatus === QuestStatus.Unclaimed) {
numUnclaimedUnignoredQuests++;
const rewardType = getQuestRewardType(quest);
if (rewardTypeToSettingMap[rewardType]) {
numUnclaimedUnignoredQuests++;
}
}
}
@ -174,6 +217,7 @@ export function validateAndOverwriteIgnoredQuests(ignoredQuests?: string[], ques
const [validIgnored, numUnclaimedUnignoredQuests] = validateIgnoredQuests(ignoredQuests, questsData);
settings.store.unclaimedUnignoredQuests = numUnclaimedUnignoredQuests;
setIgnoredQuestIDs(validIgnored, questsData?.[0]?.userStatus?.userId);
rerenderQuests();
return validIgnored;
}
@ -347,6 +391,11 @@ function QuestButtonSettings(): JSX.Element {
const {
questButtonDisplay,
questRewardIncludeRewardCode,
questRewardIncludeNitroCode,
questRewardIncludeCollectibles,
questRewardIncludeInGame,
questRewardIncludeOrbs,
questButtonUnclaimed,
questButtonBadgeColor,
questButtonLeftClickAction,
@ -354,6 +403,11 @@ function QuestButtonSettings(): JSX.Element {
questButtonRightClickAction
} = settings.use([
"questButtonDisplay",
"questRewardIncludeRewardCode",
"questRewardIncludeNitroCode",
"questRewardIncludeCollectibles",
"questRewardIncludeInGame",
"questRewardIncludeOrbs",
"questButtonUnclaimed",
"questButtonBadgeColor",
"questButtonLeftClickAction",
@ -381,6 +435,15 @@ function QuestButtonSettings(): JSX.Element {
{ label: "Nothing", value: "nothing" }
];
const questButtonRewardDisplayOptions: DynamicDropdownSettingOption[] = [
{ label: "Orbs", value: "orbs", selected: questRewardIncludeOrbs },
{ label: "Nitro Codes", value: "nitro-code", selected: questRewardIncludeNitroCode },
{ label: "Reward Codes", value: "reward-code", selected: questRewardIncludeRewardCode },
{ label: "In Game Items", value: "in-game", selected: questRewardIncludeInGame },
{ label: "Profile Collectibles", value: "collectibles", selected: questRewardIncludeCollectibles },
];
const [currentRewardsOptions, setCurrentRewardsOptions] = useState(questButtonRewardDisplayOptions.filter(option => option.selected));
const [currentQuestButtonDisplay, setCurrentQuestButtonDisplay] = useState((questButtonDisplayOptions.find(option => option.value === questButtonDisplay) as RadioOption));
const [currentQuestButtonUnclaimed, setCurrentQuestButtonUnclaimed] = useState((questButtonUnclaimedOptions.find(option => option.value === questButtonUnclaimed) as RadioOption));
const [currentQuestButtonLeftClickAction, setCurrentQuestButtonLeftClickAction] = useState<"open-quests" | "plugin-settings" | "context-menu" | "nothing">(questButtonLeftClickAction as "open-quests" | "plugin-settings" | "context-menu" | "nothing");
@ -389,6 +452,40 @@ function QuestButtonSettings(): JSX.Element {
const [currentBadgeColor, setCurrentBadgeColor] = useState((questButtonBadgeColor as number | null));
const [dummySelected, setDummySelected] = useState(false);
function updateSettingsTruthy(enabled: DynamicDropdownSettingOption[]) {
const enabledValues = enabled.map(option => option.value);
questButtonRewardDisplayOptions.forEach(option => {
option.selected = enabledValues.includes(option.value);
});
settings.store.questRewardIncludeRewardCode = enabledValues.includes("reward-code");
settings.store.questRewardIncludeNitroCode = enabledValues.includes("nitro-code");
settings.store.questRewardIncludeCollectibles = enabledValues.includes("collectibles");
settings.store.questRewardIncludeInGame = enabledValues.includes("in-game");
settings.store.questRewardIncludeOrbs = enabledValues.includes("orbs");
setCurrentRewardsOptions(enabled);
validateAndOverwriteIgnoredQuests();
}
function handleQuestRewardDisplayChange(values: Array<DynamicDropdownSettingOption | string>) {
if (values.length === 0) {
updateSettingsTruthy([]);
return;
}
const stringlessValues = values.filter(v => typeof v !== "string") as DynamicDropdownSettingOption[];
const selectedOption = values.find(v => typeof v === "string") as string;
const option = questButtonRewardDisplayOptions.find(option => option.value === selectedOption) as DynamicDropdownSettingOption;
if (option.selected) {
updateSettingsTruthy(stringlessValues.filter(v => v.value !== selectedOption));
} else {
updateSettingsTruthy([...stringlessValues, option]);
}
}
function handleQuestButtonDisplayChange(value: RadioOption) {
setCurrentQuestButtonDisplay(value);
settings.store.questButtonDisplay = value.value;
@ -449,6 +546,47 @@ function QuestButtonSettings(): JSX.Element {
/>
</div>
</div>
<div className={q("main-inline-group")}>
<div className={q("inline-group-item")}>
<Forms.FormTitle className={q("form-subtitle", "form-subtitle-spacier")}>
Left Click Action
</Forms.FormTitle>
<Select
options={questButtonClickOptions}
className={q("select")}
popoutPosition="top"
serialize={String}
isSelected={(value: string) => value === currentQuestButtonLeftClickAction}
select={handleLeftClickActionChange}
/>
</div>
<div className={q("inline-group-item")}>
<Forms.FormTitle className={q("form-subtitle", "form-subtitle-spacier")}>
Middle Click Action
</Forms.FormTitle>
<Select
options={questButtonClickOptions}
className={q("select")}
popoutPosition="top"
serialize={String}
isSelected={(value: string) => value === currentQuestButtonMiddleClickAction}
select={handleMiddleClickActionChange}
/>
</div>
<div className={q("inline-group-item")}>
<Forms.FormTitle className={q("form-subtitle", "form-subtitle-spacier")}>
Right Click Action
</Forms.FormTitle>
<Select
options={questButtonClickOptions}
className={q("select")}
popoutPosition="top"
serialize={String}
isSelected={(value: string) => value === currentQuestButtonRightClickAction}
select={handleRightClickActionChange}
/>
</div>
</div>
<div className={q("main-inline-group")}>
<div className={q("inline-group-item")}>
<Forms.FormTitle className={q("form-subtitle")}>
@ -494,45 +632,28 @@ function QuestButtonSettings(): JSX.Element {
</div>
</div>
<div className={q("main-inline-group")}>
<div className={q("inline-group-item")}>
<Forms.FormSection>
<Forms.FormTitle className={q("form-subtitle", "form-subtitle-spacier")}>
Left Click Action
Included Reward Types
</Forms.FormTitle>
<Select
options={questButtonClickOptions}
<Forms.FormText className={q("form-description")}>
Only count Quests with these reward types as unclaimed when determining button
visibility, badge count, and when playing the alert sound.
</Forms.FormText>
<DynamicDropdown
placeholder="Select which reward types to include in the unclaimed count..."
feedback="There's no supported Quest feature by that name."
className={q("select")}
popoutPosition="top"
serialize={String}
isSelected={(value: string) => value === currentQuestButtonLeftClickAction}
select={handleLeftClickActionChange}
/>
</div>
<div className={q("inline-group-item")}>
<Forms.FormTitle className={q("form-subtitle", "form-subtitle-spacier")}>
Middle Click Action
</Forms.FormTitle>
<Select
options={questButtonClickOptions}
className={q("select")}
popoutPosition="top"
serialize={String}
isSelected={(value: string) => value === currentQuestButtonMiddleClickAction}
select={handleMiddleClickActionChange}
/>
</div>
<div className={q("inline-group-item")}>
<Forms.FormTitle className={q("form-subtitle", "form-subtitle-spacier")}>
Right Click Action
</Forms.FormTitle>
<Select
options={questButtonClickOptions}
className={q("select")}
popoutPosition="top"
serialize={String}
isSelected={(value: string) => value === currentQuestButtonRightClickAction}
select={handleRightClickActionChange}
/>
</div>
maxVisibleItems={questButtonRewardDisplayOptions.length}
clearable={true}
multi={true}
value={currentRewardsOptions as any}
options={questButtonRewardDisplayOptions}
onChange={handleQuestRewardDisplayChange}
closeOnSelect={false}
>
</DynamicDropdown>
</Forms.FormSection>
</div>
</Forms.FormSection>
</div>
@ -919,13 +1040,17 @@ function ReorderQuestsSetting(): JSX.Element {
claimedSubsort,
ignoredSubsort,
expiredSubsort,
ignoredQuestProfile
ignoredQuestProfile,
rememberQuestPageSort,
rememberQuestPageFilters
} = settings.use([
"unclaimedSubsort",
"claimedSubsort",
"ignoredSubsort",
"expiredSubsort",
"ignoredQuestProfile"
"ignoredQuestProfile",
"rememberQuestPageSort",
"rememberQuestPageFilters"
]);
const getSubsortOptions = (source: string): SelectOption[] => {
@ -966,7 +1091,7 @@ function ReorderQuestsSetting(): JSX.Element {
Reorder Quests
</Forms.FormTitle>
<Forms.FormText className={q("form-description")}>
Sort Quests by their status. Leave empty for default sorting.
Sort Quests by their status. Applied when the "Questify" sort option is selected on the Quest page.
<br /><br />
Comma-separated list must contain all of: <span className={q("inline-code-block")}>UNCLAIMED, CLAIMED, IGNORED, EXPIRED</span>.
</Forms.FormText>
@ -988,9 +1113,9 @@ function ReorderQuestsSetting(): JSX.Element {
settings.store.reorderQuests = cleaned;
}
}}
placeholder="Using Discord's default sorting."
placeholder="You must include all of UNCLAIMED, CLAIMED, IGNORED, EXPIRED"
error={
validCommaSeparatedList(reorderQuests, ["UNCLAIMED", "CLAIMED", "IGNORED", "EXPIRED"], true, true, true, false)
validCommaSeparatedList(reorderQuests, ["UNCLAIMED", "CLAIMED", "IGNORED", "EXPIRED"], false, true, true, false)
? undefined
: "Invalid format."
}
@ -1075,12 +1200,51 @@ function ReorderQuestsSetting(): JSX.Element {
popoutPosition="bottom"
serialize={String}
isSelected={(value: string) => value === ignoredQuestProfile}
select={(value: string) => {
settings.store.ignoredQuestProfile = value;
}}
select={(value: string) => { settings.store.ignoredQuestProfile = value; }}
/>
</div>
</div>
<div className={q("main-inline-group")}>
<div className={q("inline-group-item")}>
<Forms.FormTitle className={q("form-subtitle")}>
Remember Sort Choice
</Forms.FormTitle>
<Select
options={[
{ label: "Yes", value: true },
{ label: "No", value: false }
]}
className={q("select")}
popoutPosition="bottom"
serialize={String}
isSelected={(value: boolean) => value === rememberQuestPageSort}
select={(value: boolean) => { settings.store.rememberQuestPageSort = value; }}
/>
</div>
<div className={q("inline-group-item")}>
<Forms.FormTitle className={q("form-subtitle")}>
Remember Filter Choice
</Forms.FormTitle>
<Select
options={[
{ label: "Yes", value: true },
{ label: "No", value: false }
]}
className={q("select")}
popoutPosition="bottom"
serialize={String}
isSelected={(value: boolean) => value === rememberQuestPageFilters}
select={(value: boolean) => { settings.store.rememberQuestPageFilters = value; }}
/>
</div>
</div>
<div className={q("main-inline-group")}>
<Forms.FormText className={q("form-description")}>
This sort and filter choice refers to the built-in sort and filter options on the Quest page.
The custom sorting above is only applied when the "Questify" sort option is selected on the Quest page.
If remembering is disabled, the sort or filter options will be reset each time you open the Quest page.
</Forms.FormText>
</div>
</Forms.FormSection>
</div>
</ErrorBoundary>
@ -1505,6 +1669,36 @@ export const settings = definePluginSettings({
default: defaultQuestButtonDisplay, // "always", "unclaimed", "never"
hidden: true,
},
questRewardIncludeRewardCode: {
type: OptionType.BOOLEAN,
description: "Include Quests with Reward Codes when displaying Quest counts.",
default: true,
hidden: true,
},
questRewardIncludeNitroCode: {
type: OptionType.BOOLEAN,
description: "Include Quests with Nitro Codes when displaying Quest counts.",
default: true,
hidden: true,
},
questRewardIncludeInGame: {
type: OptionType.BOOLEAN,
description: "Include Quests with In-Game rewards when displaying Quest counts.",
default: true,
hidden: true,
},
questRewardIncludeCollectibles: {
type: OptionType.BOOLEAN,
description: "Include Quests with Collectibles when displaying Quest counts.",
default: true,
hidden: true,
},
questRewardIncludeOrbs: {
type: OptionType.BOOLEAN,
description: "Include Quests with Orbs when displaying Quest counts.",
default: true,
hidden: true,
},
questButtonUnclaimed: {
type: OptionType.STRING,
description: "Which display type to use for the unclaimed indicator on the Quest button in the server list.",
@ -1647,6 +1841,30 @@ export const settings = definePluginSettings({
default: "private", // "shared", "private"
hidden: true
},
rememberQuestPageSort: {
type: OptionType.BOOLEAN,
description: "Remember the last used sort on the Quests page.",
default: true,
hidden: true
},
rememberQuestPageFilters: {
type: OptionType.BOOLEAN,
description: "Remember the last used filters on the Quests page.",
default: true,
hidden: true
},
lastQuestPageSort: {
type: OptionType.STRING,
description: "Remember the last used sort on the Quests page.",
default: "questify" as string, // sort key
hidden: true
},
lastQuestPageFilters: {
type: OptionType.CUSTOM,
description: "Remember the last used filters on the Quests page.",
default: {} as { [filter: string]: { group: string, filter: string; }; }, // Array of filters
hidden: true
},
ignoredQuestIDs: {
type: OptionType.CUSTOM,
description: "An array of Quest IDs that are ignored.",

View file

@ -19,6 +19,15 @@ export const ServerListItemUpperBadgeComponent = findComponentByCodeLazy("STATUS
export const RadioGroup = findComponentByCodeLazy("\"label\",\"description\"]);");
export const QuestTile = findComponentByCodeLazy(".rowIndex,trackGuildAndChannelMetadata");
export enum QuestRewardType {
UNKNOWN = 0,
REWARD_CODE = 1,
IN_GAME = 2,
COLLECTIBLE = 3,
VIRTUAL_CURRENCY = 4,
FRACTIONAL_PREMIUM = 5
}
export enum QuestStatus {
Claimed = "CLAIMED",
Unclaimed = "UNCLAIMED",
@ -76,6 +85,11 @@ export interface Quest {
messages: {
questName: string;
};
rewardsConfig: {
rewards: {
type: QuestRewardType;
}[];
},
taskConfigV2: {
tasks: {
WATCH_VIDEO?: {

View file

@ -15,7 +15,7 @@ import { Quest, QuestStatus, RGB } from "./components";
export const q = classNameFactory("questify-");
export const QuestifyLogger = new Logger("Questify");
export const QuestsStore = findStoreLazy("QuestsStore");
export const questPath = "/discovery/quests";
export const questPath = "/quest-home";
export const leftClick = 0;
export const middleClick = 1;
export const rightClick = 2;