Plugin

FavoriteGifSearch

Adds a search bar to favorite gifs.

Media Customisation
index.tsx
Download

Source

src/plugins/favGifSearch/index.tsx
1import { definePluginSettings } from "@api/Settings";
2import ErrorBoundary from "@components/ErrorBoundary";
3import { Devs } from "@utils/constants";
4import definePlugin, { OptionType } from "@utils/types";
5import { useCallback, useEffect, useRef, useState } from "@webpack/common";
6
7interface 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
18type TSearchBarComponent =
19 React.FC<SearchBarComponentProps>;
20
21interface Gif {
22 format: number;
23 src: string;
24 width: number;
25 height: number;
26 order: number;
27 url: string;
28}
29
30interface 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
43export 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: true
60 }
61 ] as const
62 }
63});
64
65export 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/1
77 // ($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;Favorites&#039; ? $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 again
84 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
116function 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 early
125 if (searchQuery === "") {
126 props.favorites = props.favCopy;
127 instance.forceUpdate();
128 return;
129 }
130
131
132 // scroll back to top
133 ref.current
134 ?.closest("#gif-picker-tab-panel")
135 ?.querySelector(&#039;[class*="scrollerBase"]&#039;)
136 ?.scrollTo(0, 0);
137
138
139 const result =
140 props.favCopy
141 .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 <SearchBarComponent
161 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
181export 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-is
187 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.gif
196 // /view/some-gif-hi-24248063 -> some-gif-hi-24248063
197 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
209function 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