Plugin
FavoriteGifSearch
Adds a search bar to favorite gifs.
1
import { definePluginSettings } from "@api/Settings";2
import ErrorBoundary from "@components/ErrorBoundary";3
import { Devs } from "@utils/constants";4
import definePlugin, { OptionType } from "@utils/types";5
import { useCallback, useEffect, useRef, useState } from "@webpack/common";6
7
interface SearchBarComponentProps {8
ref?: React.RefObject<any>;9
autoFocus: boolean;10
size: string;11
onChange: (query: string) => void;12
onClear: () => void;13
query: string;14
placeholder: string;15
className?: string;16
}17
18
type TSearchBarComponent =19
React.FC<SearchBarComponentProps>;20
21
interface Gif {22
format: number;23
src: string;24
width: number;25
height: number;26
order: number;27
url: string;28
}29
30
interface Instance {31
dead?: boolean;32
state: {33
resultType?: string;34
};35
props: {36
favCopy: Gif[],37
38
favorites: Gif[],39
},40
forceUpdate: () => void;41
}42
43
export const settings = definePluginSettings({44
searchOption: {45
type: OptionType.SELECT,46
description: "The part of the url you want to search",47
options: [48
{49
label: "Entire Url",50
value: "url"51
},52
{53
label: "Path Only (/somegif.gif)",54
value: "path"55
},56
{57
label: "Host & Path (tenor.com somgif.gif)",58
value: "hostandpath",59
default: true60
}61
] as const62
}63
});64
65
export default definePlugin({66
name: "FavoriteGifSearch",67
authors: [Devs.Aria],68
description: "Adds a search bar to favorite gifs.",69
tags: ["Media", "Customisation"],70
71
patches: [72
{73
find: "renderHeaderContent()",74
replacement: [75
{76
// https://regex101.com/r/07gpzP/177
// ($1 renderHeaderContent=function { ... switch (x) ... case FAVORITES:return) ($2) ($3 case default: ... return r.jsx(($<searchComp>), {...props}))78
match: /(renderHeaderContent\(\).{1,150}FAVORITES:return)(.{1,150});(case.{1,200}default:.{0,50}?return\(0,\i\.jsx\)\((?<searchComp>\i\.\i),)/,79
replace: "$1 this?.state?.resultType === 039;Favorites039; ? $self.renderSearchBar(this, $<searchComp>) : $2;$3"80
},81
{82
// to persist filtered favorites when component re-renders.83
// when resizing the window the component rerenders and we loose the filtered favorites and have to type in the search bar to get them again84
match: /(,suggestions:\i,favorites:)(\i),/,85
replace: "$1$self.getFav($2),favCopy:$2,"86
}87
88
]89
}90
],91
92
settings,93
94
getTargetString,95
96
instance: null as Instance | null,97
renderSearchBar(instance: Instance, SearchBarComponent: TSearchBarComponent) {98
this.instance = instance;99
return (100
<ErrorBoundary noop>101
<SearchBar instance={instance} SearchBarComponent={SearchBarComponent} />102
</ErrorBoundary>103
);104
},105
106
getFav(favorites: Gif[]) {107
if (!this.instance || this.instance.dead) return favorites;108
const { favorites: filteredFavorites } = this.instance.props;109
110
return filteredFavorites != null && filteredFavorites?.length !== favorites.length ? filteredFavorites : favorites;111
112
}113
});114
115
116
function SearchBar({ instance, SearchBarComponent }: { instance: Instance; SearchBarComponent: TSearchBarComponent; }) {117
const [query, setQuery] = useState("");118
const ref = useRef<HTMLElement>(null);119
120
const onChange = useCallback((searchQuery: string) => {121
setQuery(searchQuery);122
const { props } = instance;123
124
// return early125
if (searchQuery === "") {126
props.favorites = props.favCopy;127
instance.forceUpdate();128
return;129
}130
131
132
// scroll back to top133
ref.current134
?.closest("#gif-picker-tab-panel")135
?.querySelector(039;[class*="scrollerBase"]039;)136
?.scrollTo(0, 0);137
138
139
const result =140
props.favCopy141
.map(gif => ({142
score: fuzzySearch(searchQuery.toLowerCase(), getTargetString(gif.url ?? gif.src).replace(/(%20|[_-])/g, " ").toLowerCase()),143
gif,144
}))145
.filter(m => m.score != null) as { score: number; gif: Gif; }[];146
147
result.sort((a, b) => b.score - a.score);148
props.favorites = result.map(e => e.gif);149
150
instance.forceUpdate();151
}, [instance.state]);152
153
useEffect(() => {154
return () => {155
instance.dead = true;156
};157
}, []);158
159
return (160
<SearchBarComponent161
ref={ref}162
autoFocus={true}163
size="md"164
className=""165
onChange={onChange}166
onClear={() => {167
setQuery("");168
if (instance.props.favCopy != null) {169
instance.props.favorites = instance.props.favCopy;170
instance.forceUpdate();171
}172
}}173
query={query}174
placeholder="Search Favorite Gifs"175
/>176
);177
}178
179
180
181
export function getTargetString(urlStr: string) {182
let url: URL;183
try {184
url = new URL(urlStr);185
} catch (err) {186
// Can't resolve URL, return as-is187
return urlStr;188
}189
190
switch (settings.store.searchOption) {191
case "url":192
return url.href;193
case "path":194
if (url.host === "media.discordapp.net" || url.host === "tenor.com")195
// /attachments/899763415290097664/1095711736461537381/attachment-1.gif -> attachment-1.gif196
// /view/some-gif-hi-24248063 -> some-gif-hi-24248063197
return url.pathname.split("/").at(-1) ?? url.pathname;198
return url.pathname;199
case "hostandpath":200
if (url.host === "media.discordapp.net" || url.host === "tenor.com")201
return `${url.host} ${url.pathname.split("/").at(-1) ?? url.pathname}`;202
return `${url.host} ${url.pathname}`;203
204
default:205
return "";206
}207
}208
209
function fuzzySearch(searchQuery: string, searchString: string) {210
let searchIndex = 0;211
let score = 0;212
213
for (let i = 0; i < searchString.length; i++) {214
if (searchString[i] === searchQuery[searchIndex]) {215
score++;216
searchIndex++;217
} else {218
score--;219
}220
221
if (searchIndex === searchQuery.length) {222
return score;223
}224
}225
226
return null;227
}228