diff --git a/src/equicordplugins/questify/index.tsx b/src/equicordplugins/questify/index.tsx index 835943aa9..00dd5a987 100644 --- a/src/equicordplugins/questify/index.tsx +++ b/src/equicordplugins/questify/index.tsx @@ -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) { // 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: { diff --git a/src/equicordplugins/questify/settings.tsx b/src/equicordplugins/questify/settings.tsx index 0625d2c3f..c6c4321a1 100644 --- a/src/equicordplugins/questify/settings.tsx +++ b/src/equicordplugins/questify/settings.tsx @@ -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 = 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) { + 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 { /> +
+
+ + Left Click Action + + value === currentQuestButtonMiddleClickAction} + select={handleMiddleClickActionChange} + /> +
+
+ + Right Click Action + + + Only count Quests with these reward types as unclaimed when determining button + visibility, badge count, and when playing the alert sound. + + value === currentQuestButtonLeftClickAction} - select={handleLeftClickActionChange} - /> -
-
- - Middle Click Action - - value === currentQuestButtonRightClickAction} - select={handleRightClickActionChange} - /> -
+ maxVisibleItems={questButtonRewardDisplayOptions.length} + clearable={true} + multi={true} + value={currentRewardsOptions as any} + options={questButtonRewardDisplayOptions} + onChange={handleQuestRewardDisplayChange} + closeOnSelect={false} + > + +
@@ -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 - 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.

Comma-separated list must contain all of: UNCLAIMED, CLAIMED, IGNORED, EXPIRED.
@@ -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; }} /> +
+
+ + Remember Sort Choice + + value === rememberQuestPageFilters} + select={(value: boolean) => { settings.store.rememberQuestPageFilters = value; }} + /> +
+
+
+ + 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. + +
@@ -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.", diff --git a/src/equicordplugins/questify/utils/components.tsx b/src/equicordplugins/questify/utils/components.tsx index a5c17f630..d182e35e0 100644 --- a/src/equicordplugins/questify/utils/components.tsx +++ b/src/equicordplugins/questify/utils/components.tsx @@ -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?: { diff --git a/src/equicordplugins/questify/utils/misc.tsx b/src/equicordplugins/questify/utils/misc.tsx index 8db0ec06e..25a14f820 100644 --- a/src/equicordplugins/questify/utils/misc.tsx +++ b/src/equicordplugins/questify/utils/misc.tsx @@ -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;