/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./style.css";
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { DataStore } from "@api/index";
import { definePluginSettings } from "@api/Settings";
import { Flex } from "@components/Flex";
import { Devs, EquicordDevs } from "@utils/constants";
import { ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types";
import { Alerts, Button, Clipboard, ContextMenuApi, FluxDispatcher, Forms, Menu, React, showToast, TextInput, Toasts, useCallback, useState } from "@webpack/common";
import { addToCollection, cache_collections, createCollection, DATA_COLLECTION_NAME, deleteCollection, fixPrefix, getCollections, getGifById, getItemCollectionNameFromId, moveGifToCollection, refreshCacheCollection, removeFromCollection, renameCollection } from "./utils/collectionManager";
import { getFormat } from "./utils/getFormat";
import { getGif } from "./utils/getGif";
import { downloadCollections, uploadGifCollections } from "./utils/settingsUtils";
import { uuidv4 } from "./utils/uuidv4";
let GIF_COLLECTION_PREFIX: string;
let GIF_ITEM_PREFIX: string;
export const SortingOptions = {
NAME: 1,
CREATION_DATE: 2,
MODIFIED_DATE: 3
};
const addCollectionContextMenuPatch: NavContextMenuPatchCallback = (children, props) => {
if (!props) return;
const { message, itemSrc, itemHref, target } = props;
const gif = getGif(message, itemSrc ?? itemHref, target);
if (!gif) return;
const group = findGroupChildrenByChildId("open-native-link", children) ?? findGroupChildrenByChildId("copy-link", children);
if (group && !group.some(child => child?.props?.id === "add-to-collection")) {
const collections = cache_collections;
group.push(
{collections.length > 0 && collections.map(col => (
addToCollection(col.name, gif)}
/>
))}
{collections.length > 0 && }
{
openModal(modalProps => (
));
}}
/>
);
}
};
export const settings = definePluginSettings({
itemPrefix: {
description: "The prefix for gif items",
type: OptionType.STRING,
default: "gc-item:",
onChange: value => {
const normalizedValue = value.replace(/:+$/, "") + ":";
if (normalizedValue === GIF_ITEM_PREFIX) return;
GIF_ITEM_PREFIX = normalizedValue;
settings.store.itemPrefix = normalizedValue;
fixPrefix(normalizedValue);
},
restartNeeded: true
},
collectionPrefix: {
description: "The prefix for collections",
type: OptionType.STRING,
default: "gc:",
onChange: value => {
const normalizedValue = value.replace(/:+$/, "") + ":";
if (normalizedValue === GIF_COLLECTION_PREFIX) return;
GIF_COLLECTION_PREFIX = normalizedValue;
settings.store.collectionPrefix = normalizedValue;
fixPrefix(normalizedValue);
},
restartNeeded: true
},
onlyShowCollections: {
description: "Only show collections",
type: OptionType.BOOLEAN,
default: false,
restartNeeded: true
},
stopWarnings: {
description: "Stop deletion warnings",
type: OptionType.BOOLEAN,
default: false,
},
defaultEmptyCollectionImage: {
description: "The image / gif that will be shown when a collection has no images / gifs",
type: OptionType.STRING,
default: "https://c.tenor.com/YEG33HsLEaIAAAAC/parksandrec-oops.gif"
},
collectionsSortType: {
description: "The type of sorting for collections",
type: OptionType.NUMBER,
default: SortingOptions.NAME,
hidden: true
},
collectionsSortOrder: {
description: "The order of sorting for collections",
type: OptionType.STRING,
default: "asc",
hidden: true
},
collectionsSort: {
type: OptionType.COMPONENT,
description: "Decide how to sort collections",
component: () => {
const [sortType, setSortType] = useState(settings.store.collectionsSortType || SortingOptions.NAME);
const [sortOrder, setSortOrder] = useState(settings.store.collectionsSortOrder || "asc");
const handleSortTypeChange = value => {
setSortType(value);
settings.store.collectionsSortType = value;
};
const handleSortOrderChange = value => {
setSortOrder(value);
settings.store.collectionsSortOrder = value;
};
return (
Sort Collections
Choose a sorting criteria for your collections
);
}
},
importGifs: {
type: OptionType.COMPONENT,
description: "Import Collections",
component: () =>
,
},
exportGifs: {
type: OptionType.COMPONENT,
description: "Export Collections",
component: () =>
},
resetCollections: {
type: OptionType.COMPONENT,
description: "Reset Collections",
component: () =>
}
});
export default definePlugin({
name: "GifCollections",
description: "Allows you to create collections of gifs",
authors: [Devs.Aria, EquicordDevs.creations],
patches: [
{
find: "renderCategoryExtras",
replacement: [
{
match: /(render\(\){)(.{1,50}getItemGrid)/,
replace: "$1;$self.insertCollections(this);$2"
},
{
match: /(className:\w\.categoryName,children:)(\i)/,
replace: "$1$self.hidePrefix($2),"
},
]
},
{
find: "renderEmptyFavorite",
replacement: {
match: /render\(\){.{1,500}onClick:this\.handleClick,/,
replace: "$&onContextMenu: (e) => $self.collectionContextMenu(e, this),"
}
},
{
find: "renderHeaderContent()",
replacement: [
{
match: /(renderContent\(\){)(.{1,50}resultItems)/,
replace: "$1$self.renderContent(this);$2"
},
]
},
{
find: "type:\"GIF_PICKER_QUERY\"",
replacement: {
match: /(function \i\(.{1,10}\){)(.{1,100}.GIFS_SEARCH,query:)/,
replace: "$1if($self.shouldStopFetch(arguments[0])) return;$2"
}
},
],
settings,
start() {
refreshCacheCollection();
addContextMenuPatch("message", addCollectionContextMenuPatch);
GIF_COLLECTION_PREFIX = settings.store.collectionPrefix;
GIF_ITEM_PREFIX = settings.store.itemPrefix;
},
stop() {
removeContextMenuPatch("message", addCollectionContextMenuPatch);
},
get collections() {
refreshCacheCollection();
return this.sortedCollections();
},
sortedCollections() {
return cache_collections.sort((a, b) => {
const sortType = settings.store.collectionsSortType;
const sortOrder = settings.store.collectionsSortOrder === "asc" ? 1 : -1;
switch (sortType) {
case SortingOptions.NAME:
return a.name.localeCompare(b.name) * sortOrder;
case SortingOptions.CREATION_DATE:
return ((a.createdAt ?? 0) - (b.createdAt ?? 0)) * sortOrder;
case SortingOptions.MODIFIED_DATE:
return ((a.lastUpdated ?? 0) - (b.lastUpdated ?? 0)) * sortOrder;
default:
return 0;
}
});
},
renderContent(instance) {
if (instance.props.query.startsWith(GIF_COLLECTION_PREFIX)) {
const collection = this.collections.find(c => c.name === instance.props.query);
if (collection) {
instance.props.resultItems = collection.gifs.map(g => ({
id: g.id,
format: getFormat(g.src),
src: g.src,
url: g.url,
width: g.width,
height: g.height
})).reverse();
}
}
},
hidePrefix(name) {
return name.split(":".length > 1) ? name.replace(/.+?:/, "") : name;
},
insertCollections(instance) {
const shouldRemoveAll = settings.store.onlyShowCollections;
try {
if (instance.props.trendingCategories.length && instance.props.trendingCategories[0].type === "Trending") {
this.oldTrendingCat = instance.props.trendingCategories;
}
if (shouldRemoveAll) {
instance.props.trendingCategories = this.sortedCollections().concat(instance.props.favorites);
} else if (this.oldTrendingCat != null) {
instance.props.trendingCategories = this.sortedCollections().concat(this.oldTrendingCat);
}
} catch (err) {
console.error(err);
}
},
shouldStopFetch(query) {
return query.startsWith(GIF_COLLECTION_PREFIX) && this.collections.find(c => c.name === query) != null;
},
collectionContextMenu(e, instance) {
const { item } = instance.props;
if (item?.name?.startsWith(GIF_COLLECTION_PREFIX)) {
return ContextMenuApi.openContextMenu(e, () =>
);
}
if (item?.id?.startsWith(GIF_ITEM_PREFIX)) {
return ContextMenuApi.openContextMenu(e, () =>
);
}
const { src, url, height, width } = item;
if (src && url && height != null && width != null && !item.id?.startsWith(GIF_ITEM_PREFIX)) {
return ContextMenuApi.openContextMenu(e, () =>
FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
aria-label="Gif Collections"
>
{MenuThingy({ gif: { ...item, id: uuidv4(GIF_ITEM_PREFIX) } })}
);
}
return null;
},
});
const RemoveItemContextMenu = ({ type, nameOrId, instance }) => (
FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
aria-label={type === "collection" ? "Delete Collection" : "Remove"}
>
{type === "collection" && (
<>
{
const collection = cache_collections.find(c => c.name === nameOrId);
if (!collection) return;
openModal(modalProps => (
Collection Information
Name
{collection.name.replace(/.+?:/, "")}
Gifs
{collection.gifs.length}
Created At
{collection.createdAt ? new Date(collection.createdAt).toLocaleString() : "Unknown"}
Last Updated
{collection.lastUpdated ? new Date(collection.lastUpdated).toLocaleString() : "Unknown"}
));
}}
/>
openModal(modalProps => (
))}
/>
>
)}
{type === "gif" && (
<>
{
const gifInfo = getGifById(nameOrId);
if (!gifInfo) return;
openModal(modalProps => (
Information
Added At
{gifInfo.addedAt ? new Date(gifInfo.addedAt).toLocaleString() : "Unknown"}
Width
{gifInfo.width}
Height
{gifInfo.height}
));
}}
/>
{
const gifInfo = getGifById(nameOrId);
if (!gifInfo) return;
Clipboard.copy(gifInfo.url);
showToast("URL copied to clipboard", Toasts.Type.SUCCESS);
}}
/>
{
openModal(modalProps => (
Move To Collection
Select a collection to move the item to
{cache_collections
.filter(col => col.name !== getItemCollectionNameFromId(nameOrId))
.map(col => (
))}
));
}}
/>
>
)}
{
if (settings.store.stopWarnings) {
const collectionName = getItemCollectionNameFromId(nameOrId);
if (type === "collection") {
deleteCollection(nameOrId);
instance.forceUpdate();
} else {
await removeFromCollection(nameOrId);
FluxDispatcher.dispatch({
type: "GIF_PICKER_QUERY",
query: `${collectionName} `
});
FluxDispatcher.dispatch({
type: "GIF_PICKER_QUERY",
query: `${collectionName}`
});
}
return;
}
Alerts.show({
title: "Are you sure?",
body: `Do you really want to ${type === "collection" ? "delete this collection" : "remove this item"}?`,
confirmText: type === "collection" ? "Delete" : "Remove",
confirmColor: Button.Colors.RED,
cancelText: "Nevermind",
onConfirm: async () => {
const collectionName = type === "collection" ? nameOrId : getItemCollectionNameFromId(nameOrId);
if (type === "collection") {
deleteCollection(nameOrId);
instance.forceUpdate();
} else {
await removeFromCollection(nameOrId);
FluxDispatcher.dispatch({
type: "GIF_PICKER_QUERY",
query: `${collectionName} `
});
FluxDispatcher.dispatch({
type: "GIF_PICKER_QUERY",
query: `${collectionName}`
});
}
}
});
}}
/>
);
const MenuThingy = ({ gif }) => {
const collections = cache_collections;
return (
{collections.map(col => (
addToCollection(col.name, gif)}
/>
))}
{collections.length > 0 && }
openModal(modalProps => (
))}
/>
);
};
function CreateCollectionModal({ gif, onClose, modalProps }) {
const [name, setName] = useState("");
const onSubmit = useCallback(e => {
e.preventDefault();
if (!name.length) return;
createCollection(name, [gif]);
onClose();
}, [name]);
return (
);
}
function RenameCollectionModal({ name, onClose, modalProps }) {
const prefix = settings.store.collectionPrefix;
const strippedName = name.startsWith(prefix) ? name.slice(prefix.length) : name;
const [newName, setNewName] = useState(strippedName);
const onSubmit = useCallback(async e => {
e.preventDefault();
if (!newName.length || newName.length >= 25) return;
await renameCollection(name, newName);
onClose();
}, [newName, name, onClose]);
return (
);
}