Questify: Fixes + improvements. (#408)
* Fixes and improvements. * Lint.
This commit is contained in:
parent
60dcdc966b
commit
eff7bbe628
4 changed files with 494 additions and 102 deletions
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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?: {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue