feat(plugin): Favorite Gif Search (#1386)
Co-authored-by: V <vendicated@riseup.net>
This commit is contained in:
parent
c69c6f8cb7
commit
208371c471
241
src/plugins/favGifSearch.tsx
Normal file
241
src/plugins/favGifSearch.tsx
Normal file
|
@ -0,0 +1,241 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
import { findByPropsLazy } from "@webpack";
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "@webpack/common";
|
||||||
|
|
||||||
|
interface SearchBarComponentProps {
|
||||||
|
ref?: React.MutableRefObject<any>;
|
||||||
|
autoFocus: boolean;
|
||||||
|
className: string;
|
||||||
|
size: string;
|
||||||
|
onChange: (query: string) => void;
|
||||||
|
onClear: () => void;
|
||||||
|
query: string;
|
||||||
|
placeholder: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TSearchBarComponent =
|
||||||
|
React.FC<SearchBarComponentProps> & { Sizes: Record<"SMALL" | "MEDIUM" | "LARGE", string>; };
|
||||||
|
|
||||||
|
interface Gif {
|
||||||
|
format: number;
|
||||||
|
src: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
order: number;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Instance {
|
||||||
|
dead?: boolean;
|
||||||
|
state: {
|
||||||
|
resultType?: string;
|
||||||
|
};
|
||||||
|
props: {
|
||||||
|
favCopy: Gif[],
|
||||||
|
|
||||||
|
favorites: Gif[],
|
||||||
|
},
|
||||||
|
forceUpdate: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const containerClasses: { searchBar: string; } = findByPropsLazy("searchBar", "searchHeader", "gutterSize");
|
||||||
|
|
||||||
|
export const settings = definePluginSettings({
|
||||||
|
searchOption: {
|
||||||
|
type: OptionType.SELECT,
|
||||||
|
description: "The part of the url you want to search",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: "Entire Url",
|
||||||
|
value: "url"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Path Only (/somegif.gif)",
|
||||||
|
value: "path"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Host & Path (tenor.com somgif.gif)",
|
||||||
|
value: "hostandpath",
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
] as const
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "FavoriteGifSearch",
|
||||||
|
authors: [Devs.Aria],
|
||||||
|
description: "Adds a search bar for favorite gifs",
|
||||||
|
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: "renderCategoryExtras",
|
||||||
|
replacement: [
|
||||||
|
{
|
||||||
|
// https://regex101.com/r/4uHtTE/1
|
||||||
|
// ($1 renderHeaderContent=function { ... switch (x) ... case FAVORITES:return) ($2) ($3 case default:return r.jsx(($<searchComp>), {...props}))
|
||||||
|
match: /(renderHeaderContent=function.{1,150}FAVORITES:return)(.{1,150};)(case.{1,200}default:return\(0,\i\.jsx\)\((?<searchComp>\i\.\i))/,
|
||||||
|
replace: "$1 this.state.resultType === \"Favorites\" ? $self.renderSearchBar(this, $<searchComp>) : $2; $3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// to persist filtered favorites when component re-renders.
|
||||||
|
// 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
|
||||||
|
match: /(,suggestions:\i,favorites:)(\i),/,
|
||||||
|
replace: "$1$self.getFav($2),favCopy:$2,"
|
||||||
|
}
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
settings,
|
||||||
|
|
||||||
|
getTargetString,
|
||||||
|
|
||||||
|
instance: null as Instance | null,
|
||||||
|
renderSearchBar(instance: Instance, SearchBarComponent: TSearchBarComponent) {
|
||||||
|
this.instance = instance;
|
||||||
|
return (
|
||||||
|
<ErrorBoundary noop={true}>
|
||||||
|
<SearchBar instance={instance} SearchBarComponent={SearchBarComponent} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
getFav(favorites: Gif[]) {
|
||||||
|
if (!this.instance || this.instance.dead) return favorites;
|
||||||
|
const { favorites: filteredFavorites } = this.instance.props;
|
||||||
|
|
||||||
|
return filteredFavorites != null && filteredFavorites?.length !== favorites.length ? filteredFavorites : favorites;
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function SearchBar({ instance, SearchBarComponent }: { instance: Instance; SearchBarComponent: TSearchBarComponent; }) {
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
const ref = useRef<{ containerRef?: React.MutableRefObject<HTMLDivElement>; } | null>(null);
|
||||||
|
|
||||||
|
const onChange = useCallback((searchQuery: string) => {
|
||||||
|
setQuery(searchQuery);
|
||||||
|
const { props } = instance;
|
||||||
|
|
||||||
|
// return early
|
||||||
|
if (searchQuery === "") {
|
||||||
|
props.favorites = props.favCopy;
|
||||||
|
instance.forceUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// scroll back to top
|
||||||
|
ref.current?.containerRef?.current
|
||||||
|
.closest("#gif-picker-tab-panel")
|
||||||
|
?.querySelector("[class|=\"content\"]")
|
||||||
|
?.firstElementChild?.scrollTo(0, 0);
|
||||||
|
|
||||||
|
|
||||||
|
const result =
|
||||||
|
props.favCopy
|
||||||
|
.map(gif => ({
|
||||||
|
score: fuzzySearch(searchQuery.toLowerCase(), getTargetString(gif.url ?? gif.src).replace(/(%20|[_-])/g, " ").toLowerCase()),
|
||||||
|
gif,
|
||||||
|
}))
|
||||||
|
.filter(m => m.score != null) as { score: number; gif: Gif; }[];
|
||||||
|
|
||||||
|
result.sort((a, b) => b.score - a.score);
|
||||||
|
props.favorites = result.map(e => e.gif);
|
||||||
|
|
||||||
|
instance.forceUpdate();
|
||||||
|
}, [instance.state]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
instance.dead = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SearchBarComponent
|
||||||
|
ref={ref}
|
||||||
|
autoFocus={true}
|
||||||
|
className={containerClasses.searchBar}
|
||||||
|
size={SearchBarComponent.Sizes.MEDIUM}
|
||||||
|
onChange={onChange}
|
||||||
|
onClear={() => {
|
||||||
|
setQuery("");
|
||||||
|
if (instance.props.favCopy != null) {
|
||||||
|
instance.props.favorites = instance.props.favCopy;
|
||||||
|
instance.forceUpdate();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
query={query}
|
||||||
|
placeholder="Search Favorite Gifs"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function getTargetString(urlStr: string) {
|
||||||
|
const url = new URL(urlStr);
|
||||||
|
switch (settings.store.searchOption) {
|
||||||
|
case "url":
|
||||||
|
return url.href;
|
||||||
|
case "path":
|
||||||
|
if (url.host === "media.discordapp.net" || url.host === "tenor.com")
|
||||||
|
// /attachments/899763415290097664/1095711736461537381/attachment-1.gif -> attachment-1.gif
|
||||||
|
// /view/some-gif-hi-24248063 -> some-gif-hi-24248063
|
||||||
|
return url.pathname.split("/").at(-1) ?? url.pathname;
|
||||||
|
return url.pathname;
|
||||||
|
case "hostandpath":
|
||||||
|
if (url.host === "media.discordapp.net" || url.host === "tenor.com")
|
||||||
|
return `${url.host} ${url.pathname.split("/").at(-1) ?? url.pathname}`;
|
||||||
|
return `${url.host} ${url.pathname}`;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fuzzySearch(searchQuery: string, searchString: string) {
|
||||||
|
let searchIndex = 0;
|
||||||
|
let score = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < searchString.length; i++) {
|
||||||
|
if (searchString[i] === searchQuery[searchIndex]) {
|
||||||
|
score++;
|
||||||
|
searchIndex++;
|
||||||
|
} else {
|
||||||
|
score--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchIndex === searchQuery.length) {
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
Loading…
Reference in a new issue