diff --git a/src/equicordplugins/gifCollections/components/contextMenus.tsx b/src/equicordplugins/gifCollections/components/contextMenus.tsx
new file mode 100644
index 000000000..7b44bea7f
--- /dev/null
+++ b/src/equicordplugins/gifCollections/components/contextMenus.tsx
@@ -0,0 +1,207 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
+import { copyToClipboard } from "@utils/clipboard";
+import { Alerts, Button, ContextMenuApi, FluxDispatcher, Menu, showToast, Toasts } from "@webpack/common";
+
+import { settings } from "../settings";
+import { Gif, GifItem, GifPickerInstance } from "../types";
+import { addToCollection, cache_collections, deleteCollection, getGifById, getItemCollectionNameFromId, removeFromCollection } from "../utils/collectionManager";
+import { getGif } from "../utils/getGif";
+import { stripPrefix } from "../utils/misc";
+import { uuidv4 } from "../utils/uuidv4";
+import { openCollectionInfoModal, openCreateCollectionModal, openGifInfoModal, openMoveToCollectionModal, openRenameCollectionModal } from "./modals";
+
+function dispatchRefresh(collectionName: string) {
+ FluxDispatcher.dispatch({ type: "GIF_PICKER_QUERY", query: "" });
+ FluxDispatcher.dispatch({ type: "GIF_PICKER_QUERY", query: collectionName });
+}
+
+function AddToCollectionMenu(gif: Gif) {
+ return (
+
+ {cache_collections.length > 0 && cache_collections.map(col => (
+ addToCollection(col.name, gif)}
+ />
+ ))}
+ {cache_collections.length > 0 && }
+ openCreateCollectionModal(gif)}
+ />
+
+ );
+}
+
+export 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")) return;
+
+ if (settings.store.showCopyImageLink) {
+ group.push(
+ {
+ copyToClipboard(gif.url);
+ showToast("Image link copied to clipboard", Toasts.Type.SUCCESS);
+ }}
+ />
+ );
+ }
+
+ group.push(AddToCollectionMenu(gif));
+};
+
+export function RemoveItemContextMenu({ type, nameOrId, instance }: { type: "collection" | "gif"; nameOrId: string; instance: { forceUpdate: () => void; }; }) {
+ return (
+ 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) openCollectionInfoModal(collection);
+ }}
+ />
+
+ openRenameCollectionModal(nameOrId)}
+ />
+ >
+ )}
+ {type === "gif" && (
+ <>
+ {
+ const gif = getGifById(nameOrId);
+ if (gif) openGifInfoModal(gif);
+ }}
+ />
+
+ {
+ const gif = getGifById(nameOrId);
+ if (!gif) return;
+ copyToClipboard(gif.url);
+ showToast("URL copied to clipboard", Toasts.Type.SUCCESS);
+ }}
+ />
+ openMoveToCollectionModal(nameOrId)}
+ />
+
+ >
+ )}
+ {
+ const doDelete = async () => {
+ if (type === "collection") {
+ deleteCollection(nameOrId);
+ instance.forceUpdate();
+ } else {
+ const collectionName = getItemCollectionNameFromId(nameOrId);
+ await removeFromCollection(nameOrId);
+ if (collectionName) dispatchRefresh(collectionName);
+ }
+ };
+
+ if (settings.store.stopWarnings) {
+ doDelete();
+ 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: doDelete,
+ });
+ }}
+ />
+
+ );
+}
+
+export function GifPickerContextMenu({ gif }: { gif: Gif; }) {
+ return (
+ FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
+ aria-label="Gif Collections"
+ >
+ {settings.store.showCopyImageLink && (
+ {
+ copyToClipboard(gif.url);
+ showToast("Image link copied to clipboard", Toasts.Type.SUCCESS);
+ }}
+ />
+ )}
+ {AddToCollectionMenu(gif)}
+
+ );
+}
+
+export function buildGifPickerContextMenu(e: React.MouseEvent, item: GifItem, GIF_COLLECTION_PREFIX: string, GIF_ITEM_PREFIX: string, instance: GifPickerInstance) {
+ 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, () =>
+
+ );
+ }
+ return null;
+}
diff --git a/src/equicordplugins/gifCollections/components/modals.tsx b/src/equicordplugins/gifCollections/components/modals.tsx
new file mode 100644
index 000000000..3e36e52b6
--- /dev/null
+++ b/src/equicordplugins/gifCollections/components/modals.tsx
@@ -0,0 +1,172 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { Flex } from "@components/Flex";
+import { Heading } from "@components/Heading";
+import { Paragraph } from "@components/Paragraph";
+import { classes } from "@utils/misc";
+import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
+import { Button, FluxDispatcher, TextInput, useCallback, useState } from "@webpack/common";
+
+import { settings } from "../settings";
+import { Collection, Gif } from "../types";
+import { cache_collections, createCollection, getItemCollectionNameFromId, moveGifToCollection, renameCollection } from "../utils/collectionManager";
+import { cl, stripPrefix } from "../utils/misc";
+
+export function openCollectionInfoModal(collection: Collection) {
+ openModal(props => (
+
+ ));
+}
+
+export function openGifInfoModal(gif: Gif) {
+ openModal(props => (
+
+ ));
+}
+
+export function openMoveToCollectionModal(gifId: string) {
+ openModal(props => );
+}
+
+export function openCreateCollectionModal(gif: Gif) {
+ openModal(props => );
+}
+
+export function openRenameCollectionModal(name: string) {
+ openModal(props => );
+}
+
+function InfoModal({ props, title, rows }: { props: ModalProps; title: string; rows: { label: string; value: string; }[]; }) {
+ return (
+
+
+ {title}
+
+
+
+ {rows.map(row => (
+
+ {row.label}
+ {row.value}
+
+ ))}
+
+
+
+
+
+
+ );
+}
+
+function MoveToCollectionModal({ props, gifId }: { props: ModalProps; gifId: string; }) {
+ return (
+
+
+ Move To Collection
+
+
+ Select a collection to move the item to
+
+ {cache_collections
+ .filter(col => col.name !== getItemCollectionNameFromId(gifId))
+ .map(col => (
+
+ ))}
+
+
+
+
+
+
+ );
+}
+
+function CreateCollectionModal({ props, gif }: { props: ModalProps; gif: Gif; }) {
+ const [name, setName] = useState("");
+ const onSubmit = useCallback((e: React.FormEvent) => {
+ e.preventDefault();
+ if (!name.length) return;
+ createCollection(name, [gif]);
+ props.onClose();
+ }, [name, gif, props]);
+
+ return (
+
+
+
+ );
+}
+
+function RenameCollectionModal({ props, name }: { props: ModalProps; name: string; }) {
+ const prefix = settings.store.collectionPrefix;
+ const strippedName = name.startsWith(prefix) ? name.slice(prefix.length) : name;
+ const [newName, setNewName] = useState(strippedName);
+ const tooLong = newName.length >= 25;
+
+ const onSubmit = useCallback(async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!newName.length || tooLong) return;
+ await renameCollection(name, newName);
+ props.onClose();
+ }, [newName, name, tooLong, props]);
+
+ return (
+
+
+
+ );
+}
diff --git a/src/equicordplugins/gifCollections/index.tsx b/src/equicordplugins/gifCollections/index.tsx
index fad3c214e..54fb969a0 100644
--- a/src/equicordplugins/gifCollections/index.tsx
+++ b/src/equicordplugins/gifCollections/index.tsx
@@ -6,361 +6,81 @@
import "./style.css";
-import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
-import { DataStore } from "@api/index";
-import { definePluginSettings } from "@api/Settings";
-import { Divider } from "@components/Divider";
-import { Flex } from "@components/Flex";
-import { Heading } from "@components/Heading";
-import { Paragraph } from "@components/Paragraph";
-import { copyToClipboard } from "@utils/clipboard";
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, ContextMenuApi, FluxDispatcher, Menu, React, showToast, TextInput, Toasts, useCallback, useState } from "@webpack/common";
+import definePlugin from "@utils/types";
+import { FluxDispatcher } from "@webpack/common";
-import { addToCollection, cache_collections, createCollection, DATA_COLLECTION_NAME, deleteCollection, fixPrefix, getCollections, getGifById, getItemCollectionNameFromId, moveGifToCollection, refreshCacheCollection, removeFromCollection, renameCollection, updateGif } from "./utils/collectionManager";
+import { addCollectionContextMenuPatch, buildGifPickerContextMenu } from "./components/contextMenus";
+import { settings, SortingOptions } from "./settings";
+import { Category, Collection, Gif, GifPickerInstance } from "./types";
+import { cache_collections, refreshCacheCollection, updateGif } from "./utils/collectionManager";
import { getFormat } from "./utils/getFormat";
-import { getGif } from "./utils/getGif";
+import { logger, stripPrefix } from "./utils/misc";
import { batchRefreshAttachmentUrls, isCdnUrlExpired } from "./utils/refreshUrl";
-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;
-
- if (settings.store.showCopyImageLink) {
- group.push(
- {
- copyToClipboard(gif.url);
- showToast("Image link copied to clipboard", Toasts.Type.SUCCESS);
- }}
- />
- );
- }
-
- 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,
- },
- showCopyImageLink: {
- description: "Show 'Copy Image Link' option in context menus",
- type: OptionType.BOOLEAN,
- default: false,
- },
- preventDuplicates: {
- description: "Prevent adding the same GIF to a collection multiple times",
- 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
-
-
-
-
-
-
Order
-
-
-
-
-
-
-
-
- );
- }
- },
- importGifs: {
- type: OptionType.COMPONENT,
- description: "Import Collections",
- component: () =>
- ,
- },
- exportGifs: {
- type: OptionType.COMPONENT,
- description: "Export Collections",
- component: () =>
-
- },
- resetCollections: {
- type: OptionType.COMPONENT,
- description: "Reset Collections",
- component: () =>
-
- }
-});
+let refreshingUrls = false;
+let oldTrendingCat: Category[] | null = null;
export default definePlugin({
name: "GifCollections",
- description: "Allows you to create collections of gifs",
- authors: [Devs.Aria, EquicordDevs.creations],
+ description: "Allows you to create collections of gifs.",
+ authors: [EquicordDevs.creations],
+ settings,
+ contextMenus: {
+ "message": addCollectionContextMenuPatch,
+ },
+
patches: [
{
find: "renderCategoryExtras",
replacement: [
{
match: /(render\(\){)(.{1,50}getItemGrid)/,
- replace: "$1;$self.insertCollections(this);$2"
+ replace: "$1;$self.insertCollections(this);$2",
},
{
match: /("span",\{className:\i\.\i,children:)(\i)/,
- replace: "$1$self.hidePrefix($2),"
+ replace: "$1$self.hidePrefix($2),",
},
- ]
+ ],
},
{
find: "renderEmptyFavorite",
replacement: {
match: /render\(\){.{1,500}onClick:this\.handleClick,/,
- replace: "$&onContextMenu: (e) => $self.collectionContextMenu(e, this),"
- }
+ replace: "$&onContextMenu: (e) => $self.collectionContextMenu(e, this),",
+ },
},
{
find: "renderHeaderContent()",
- replacement: [
- {
- match: /(renderContent\(\){)(.{1,50}resultItems)/,
- replace: "$1$self.renderContent(this);$2"
- },
- ]
+ 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"
- }
+ replace: "$1if($self.shouldStopFetch(arguments[0])) return;$2",
+ },
},
],
- settings,
- contextMenus: {
- "message": addCollectionContextMenuPatch
- },
+
start() {
refreshCacheCollection();
GIF_COLLECTION_PREFIX = settings.store.collectionPrefix;
GIF_ITEM_PREFIX = settings.store.itemPrefix;
},
- 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;
+
+ sortedCollections(): Collection[] {
+ const sorted = [...cache_collections];
+ const sortType = settings.store.collectionsSortType;
+ const sortOrder = settings.store.collectionsSortOrder === "asc" ? 1 : -1;
+
+ return sorted.sort((a, b) => {
switch (sortType) {
case SortingOptions.NAME:
return a.name.localeCompare(b.name) * sortOrder;
@@ -373,448 +93,84 @@ export default definePlugin({
}
});
},
- 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();
- const expiredGifs = collection.gifs.filter(g => g.src && g.url && (isCdnUrlExpired(g.src) || isCdnUrlExpired(g.url)));
- if (expiredGifs.length > 0) {
- const allUrls = [...new Set(
- expiredGifs.flatMap(g => [g.src, g.url].filter((u): u is string => !!u && isCdnUrlExpired(u)))
- )];
- const refreshInBatches = async () => {
- const fullMap: Record = {};
- for (let i = 0; i < allUrls.length; i += 50) {
- const batch = allUrls.slice(i, i + 50);
- const result = await batchRefreshAttachmentUrls(batch);
- Object.assign(fullMap, result);
- }
- return fullMap;
- };
- refreshInBatches().then(async refreshMap => {
- if (!Object.keys(refreshMap).length) return;
- let anyUpdated = false;
- for (const gif of expiredGifs) {
- const newSrc = refreshMap[gif.src] ?? gif.src;
- const newUrl = refreshMap[gif.url] ?? gif.url;
- if (newSrc !== gif.src || newUrl !== gif.url) {
- await updateGif(gif.id, { ...gif, src: newSrc, url: newUrl });
- anyUpdated = true;
- }
- }
- if (!anyUpdated) return;
- const { query } = instance.props;
- FluxDispatcher.dispatch({ type: "GIF_PICKER_QUERY", query: `${query} ` });
- await new Promise(r => setTimeout(r, 2500));
- FluxDispatcher.dispatch({ type: "GIF_PICKER_QUERY", query });
- });
+ renderContent(instance: GifPickerInstance) {
+ if (!instance.props.query.startsWith(GIF_COLLECTION_PREFIX)) return;
+
+ const collection = cache_collections.find(c => c.name === instance.props.query);
+ if (!collection) return;
+
+ 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();
+
+ const expiredGifs = collection.gifs.filter(g => g.src && g.url && (isCdnUrlExpired(g.src) || isCdnUrlExpired(g.url)));
+ if (expiredGifs.length === 0) return;
+
+ const allUrls = [...new Set(
+ expiredGifs.flatMap(g => [g.src, g.url].filter((u): u is string => !!u && isCdnUrlExpired(u)))
+ )];
+
+ if (!refreshingUrls) this.refreshExpiredUrls(allUrls, expiredGifs, instance.props.query);
+ },
+
+ async refreshExpiredUrls(urls: string[], expiredGifs: Gif[], query: string) {
+ refreshingUrls = true;
+ try {
+ const fullMap: Record = {};
+ for (let i = 0; i < urls.length; i += 50) {
+ const result = await batchRefreshAttachmentUrls(urls.slice(i, i + 50));
+ Object.assign(fullMap, result);
+ }
+
+ if (!Object.keys(fullMap).length) return;
+
+ let anyUpdated = false;
+ for (const gif of expiredGifs) {
+ const newSrc = fullMap[gif.src] ?? gif.src;
+ const newUrl = fullMap[gif.url] ?? gif.url;
+ if (newSrc !== gif.src || newUrl !== gif.url) {
+ await updateGif(gif.id, { ...gif, src: newSrc, url: newUrl });
+ anyUpdated = true;
}
}
+
+ if (!anyUpdated) return;
+
+ FluxDispatcher.dispatch({ type: "GIF_PICKER_QUERY", query: "" });
+ FluxDispatcher.dispatch({ type: "GIF_PICKER_QUERY", query });
+ } finally {
+ refreshingUrls = false;
}
},
- hidePrefix(name) {
- return name.includes(":") ? name.replace(/.+?:/, "") : name;
- },
- insertCollections(instance) {
- const shouldRemoveAll = settings.store.onlyShowCollections;
+
+ hidePrefix: stripPrefix,
+
+ insertCollections(instance: GifPickerInstance) {
try {
if (instance.props.trendingCategories.length && instance.props.trendingCategories[0].type === "Trending") {
- this.oldTrendingCat = instance.props.trendingCategories;
+ 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);
+ if (settings.store.onlyShowCollections) {
+ instance.props.trendingCategories = [...this.sortedCollections(), ...instance.props.favorites];
+ } else if (oldTrendingCat != null) {
+ instance.props.trendingCategories = [...this.sortedCollections(), ...oldTrendingCat];
}
} catch (err) {
- console.error(err);
+ logger.error("Failed to insert collections", err);
}
},
- shouldStopFetch(query) {
- return query.startsWith(GIF_COLLECTION_PREFIX) && this.collections.find(c => c.name === query) != null;
+
+ shouldStopFetch(query: string) {
+ return query.startsWith(GIF_COLLECTION_PREFIX) && cache_collections.some(c => c.name === query);
},
- 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;
+
+ collectionContextMenu(e: React.MouseEvent, instance: GifPickerInstance) {
+ return buildGifPickerContextMenu(e, instance.props.item, GIF_COLLECTION_PREFIX, GIF_ITEM_PREFIX, instance);
},
});
-
-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;
- copyToClipboard(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;
- const menuItems: React.ReactNode[] = [];
-
- if (settings.store.showCopyImageLink) {
- menuItems.push(
- {
- copyToClipboard(gif.url);
- showToast("Image link copied to clipboard", Toasts.Type.SUCCESS);
- }}
- />
- );
- }
-
- menuItems.push(
-
- {collections.map(col => (
- addToCollection(col.name, gif)}
- />
- ))}
- {collections.length > 0 && }
- openModal(modalProps => (
-
- ))}
- />
-
- );
-
- return <>{menuItems}>;
-};
-
-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 (
-
-
-
- );
-}
diff --git a/src/equicordplugins/gifCollections/settings.tsx b/src/equicordplugins/gifCollections/settings.tsx
new file mode 100644
index 000000000..6752eb975
--- /dev/null
+++ b/src/equicordplugins/gifCollections/settings.tsx
@@ -0,0 +1,171 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { DataStore } from "@api/index";
+import { definePluginSettings } from "@api/Settings";
+import { Divider } from "@components/Divider";
+import { Heading } from "@components/Heading";
+import { Paragraph } from "@components/Paragraph";
+import { OptionType } from "@utils/types";
+import { Alerts, Button, useState } from "@webpack/common";
+
+import { DATA_COLLECTION_NAME, getCollections, refreshCacheCollection } from "./utils/collectionManager";
+import { cl } from "./utils/misc";
+import { downloadCollections, uploadGifCollections } from "./utils/settingsUtils";
+
+export const SortingOptions = {
+ NAME: 1,
+ CREATION_DATE: 2,
+ MODIFIED_DATE: 3,
+} as const;
+
+export const settings = definePluginSettings({
+ itemPrefix: {
+ description: "The prefix for gif items.",
+ type: OptionType.STRING,
+ default: "gc-item:",
+ restartNeeded: true,
+ },
+ collectionPrefix: {
+ description: "The prefix for collections.",
+ type: OptionType.STRING,
+ default: "gc:",
+ restartNeeded: true,
+ },
+ onlyShowCollections: {
+ description: "Only show collections in the GIF picker.",
+ type: OptionType.BOOLEAN,
+ default: false,
+ restartNeeded: true,
+ },
+ stopWarnings: {
+ description: "Skip deletion confirmation dialogs.",
+ type: OptionType.BOOLEAN,
+ default: false,
+ },
+ showCopyImageLink: {
+ description: "Show copy image link option in context menus.",
+ type: OptionType.BOOLEAN,
+ default: false,
+ },
+ preventDuplicates: {
+ description: "Prevent adding the same GIF to a collection multiple times.",
+ type: OptionType.BOOLEAN,
+ default: false,
+ },
+ defaultEmptyCollectionImage: {
+ description: "The image shown when a collection has no images.",
+ 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: SortSettingsComponent,
+ },
+ importGifs: {
+ type: OptionType.COMPONENT,
+ description: "Import collections.",
+ component: () =>
+ ,
+ },
+ exportGifs: {
+ type: OptionType.COMPONENT,
+ description: "Export collections.",
+ component: () =>
+ ,
+ },
+ resetCollections: {
+ type: OptionType.COMPONENT,
+ description: "Reset collections.",
+ component: () =>
+ ,
+ },
+});
+
+function RadioOption({ name, value, checked, onChange, label }: { name: string; value: string | number; checked: boolean; onChange: () => void; label: string; }) {
+ return (
+
+
+
+ );
+}
+
+function SortSettingsComponent() {
+ const [sortType, setSortType] = useState(settings.store.collectionsSortType ?? SortingOptions.NAME);
+ const [sortOrder, setSortOrder] = useState(settings.store.collectionsSortOrder ?? "asc");
+
+ const handleSortType = (value: number) => {
+ setSortType(value);
+ settings.store.collectionsSortType = value;
+ };
+
+ const handleSortOrder = (value: string) => {
+ setSortOrder(value);
+ settings.store.collectionsSortOrder = value;
+ };
+
+ return (
+
+
Sort Collections
+
+
Choose a sorting criteria for your collections
+
+
+
Sort By
+
handleSortType(SortingOptions.NAME)} label="Name" />
+ handleSortType(SortingOptions.CREATION_DATE)} label="Creation Date" />
+ handleSortType(SortingOptions.MODIFIED_DATE)} label="Modified Date" />
+
+
+
+
Order
+
handleSortOrder("asc")} label="Ascending" />
+ handleSortOrder("desc")} label="Descending" />
+
+
+ );
+}
diff --git a/src/equicordplugins/gifCollections/style.css b/src/equicordplugins/gifCollections/style.css
index a729e604d..313036e52 100644
--- a/src/equicordplugins/gifCollections/style.css
+++ b/src/equicordplugins/gifCollections/style.css
@@ -1,41 +1,41 @@
-.collections-sort-container {
+.vc-gif-collections-sort-container {
padding: 12px;
background-color: var(--background-secondary-alt);
border-radius: 8px;
margin-bottom: 16px;
}
-.collections-sort-title {
+.vc-gif-collections-sort-title {
font-size: 16px;
font-weight: 600;
color: var(--text-strong);
margin-bottom: 8px;
}
-.collections-sort-divider {
+.vc-gif-collections-sort-divider {
margin: 10px 0;
- border-top: 1px solid var(--input-border-default, var(--input-border));
+ border-top: 1px solid var(--border-subtle);
}
-.collections-sort-description,
-.collections-sort-section-title {
+.vc-gif-collections-sort-description,
+.vc-gif-collections-sort-section-title {
font-size: 14px;
font-weight: 500;
color: var(--text-default);
margin-bottom: 8px;
}
-.collections-sort-section {
+.vc-gif-collections-sort-section {
margin-top: 12px;
}
-.collections-sort-option {
+.vc-gif-collections-sort-option {
display: flex;
align-items: center;
margin-bottom: 8px;
}
-.collections-sort-label {
+.vc-gif-collections-sort-label {
font-size: 14px;
font-weight: 400;
color: var(--text-default);
@@ -44,43 +44,41 @@
align-items: center;
}
-.collections-sort-input {
+.vc-gif-collections-sort-input {
margin-right: 10px;
cursor: pointer;
accent-color: var(--brand-experiment);
}
-.collections-sort-label:hover {
+.vc-gif-collections-sort-label:hover {
color: var(--text-strong);
}
-.custom-modal {
- background-color: var(--background-primary);
+.vc-gif-collections-modal {
+ background-color: var(--modal-background) !important;
border-radius: 8px;
- box-shadow: 0 8px 16px rgb(0 0 0 / 24%), 0 16px 32px rgb(0 0 0 / 24%);
}
-.custom-modal-header {
+.vc-gif-collections-modal-header {
padding: 16px;
background-color: var(--background-secondary-alt);
- border-bottom: 1px solid var(--background-base-low);
+ border-bottom: 1px solid var(--border-subtle);
}
-.custom-modal-title {
+.vc-gif-collections-modal-title {
font-size: 16px;
font-weight: 600;
color: var(--text-strong);
}
-.custom-modal-content {
+.vc-gif-collections-modal-content {
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
-.gif-info,
-.collection-info {
+.vc-gif-collections-info-row {
display: flex;
justify-content: space-between;
align-items: center;
@@ -88,17 +86,17 @@
padding: 8px 0;
}
-.collection-info:last-child {
+.vc-gif-collections-info-row:last-child {
border-bottom: none;
}
-.collection-info-title {
+.vc-gif-collections-info-title {
font-size: 14px;
font-weight: 500;
color: var(--text-default);
}
-.collection-info-text {
+.vc-gif-collections-info-text {
font-size: 14px;
color: var(--text-muted);
max-width: 70%;
@@ -107,82 +105,54 @@
text-overflow: ellipsis;
}
-.custom-modal-footer {
+.vc-gif-collections-modal-footer {
padding: 16px;
background-color: var(--background-secondary-alt);
display: flex;
justify-content: flex-end;
}
-.custom-modal-footer button {
- margin-left: auto;
-}
-
-.custom-modal-button {
- background-color: var(--brand-experiment);
- color: white;
- border: none;
- padding: 8px 16px;
- border-radius: 4px;
- font-size: 14px;
- cursor: pointer;
- transition: background-color 0.2s;
-}
-
-.custom-modal-button:hover {
- background-color: var(--brand-experiment-560);
-}
-
-
-.rename-collection-text {
+.vc-gif-collections-rename-text {
font-size: 14px;
font-weight: 500;
color: var(--text-default);
margin: 16px 0 2px;
}
-.rename-collection-input {
+.vc-gif-collections-rename-input {
outline: 0 solid transparent;
transition: outline 0.2s;
border-radius: 4px;
}
-.input-warning {
+.vc-gif-collections-input-warning {
outline: 1px solid var(--text-feedback-critical);
}
-.warning-text {
+.vc-gif-collections-warning-text {
margin-top: 16px;
font-size: 14px;
color: var(--text-feedback-critical);
}
-.collection-buttons {
+.vc-gif-collections-buttons {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 8px;
max-height: 200px;
}
-/* e */
-.search-bar {
- width: 100%;
- margin-bottom: 12px;
- padding: 8px;
- border: 1px solid var(--border-faint);
- border-radius: 4px;
- background-color: var(--background-secondary-alt);
- color: var(--text-default);
- outline: none;
-}
-
-.collection-button {
+.vc-gif-collections-button {
text-align: center;
padding: 8px;
- background-color: var(--background-base-low);
+ background-color: var(--background-secondary);
color: var(--text-default);
border: none;
border-radius: 4px;
cursor: pointer;
- transition: background-color 0.2s;
-}
\ No newline at end of file
+ transition: background-color 0.15s;
+}
+
+.vc-gif-collections-button:hover {
+ background-color: var(--background-modifier-hover);
+}
diff --git a/src/equicordplugins/gifCollections/types.ts b/src/equicordplugins/gifCollections/types.ts
index 8b1e8ef27..5d94f6d2e 100644
--- a/src/equicordplugins/gifCollections/types.ts
+++ b/src/equicordplugins/gifCollections/types.ts
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
-export enum Format { NONE = 0, IMAGE = 1, VIDEO = 2 }
+export enum Format { IMAGE = 1, VIDEO = 2 }
export interface Category {
type: "Trending" | "Category";
@@ -28,11 +28,26 @@ export interface Gif {
attachmentId?: string;
}
-export interface Props {
- favorites: { [src: string]: any; };
- trendingCategories: Category[];
-}
-
type WithRequired = T & { [P in K]-?: T[P] };
export type Collection = WithRequired;
+
+export interface GifPickerInstance {
+ props: {
+ query: string;
+ resultItems: { id: string; format: Format; src: string; url: string; width: number; height: number; }[];
+ trendingCategories: Category[];
+ favorites: Category[];
+ item: { name?: string; id?: string; src?: string; url?: string; height?: number; width?: number; };
+ };
+ forceUpdate: () => void;
+}
+
+export interface GifItem {
+ name?: string;
+ id?: string;
+ src?: string;
+ url?: string;
+ height?: number;
+ width?: number;
+}
diff --git a/src/equicordplugins/gifCollections/utils/cleanUrl.ts b/src/equicordplugins/gifCollections/utils/cleanUrl.ts
index 415598487..edba2eac2 100644
--- a/src/equicordplugins/gifCollections/utils/cleanUrl.ts
+++ b/src/equicordplugins/gifCollections/utils/cleanUrl.ts
@@ -4,8 +4,12 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
-export const cleanUrl = (url: string) => {
- const urlObject = new URL(url);
- urlObject.search = "";
- return urlObject.href;
-};
+export function cleanUrl(url: string): string {
+ try {
+ const urlObject = new URL(url);
+ urlObject.search = "";
+ return urlObject.href;
+ } catch {
+ return url;
+ }
+}
diff --git a/src/equicordplugins/gifCollections/utils/collectionManager.ts b/src/equicordplugins/gifCollections/utils/collectionManager.ts
index d7617dab8..eafcc839b 100644
--- a/src/equicordplugins/gifCollections/utils/collectionManager.ts
+++ b/src/equicordplugins/gifCollections/utils/collectionManager.ts
@@ -5,192 +5,142 @@
*/
import { DataStore } from "@api/index";
-import { settings } from "@equicordplugins/gifCollections/index";
-import { Collection, Gif } from "@equicordplugins/gifCollections/types";
import { Toasts } from "@webpack/common";
+import { settings } from "../settings";
+import { Collection, Gif } from "../types";
import { getFormat } from "./getFormat";
+import { logger } from "./misc";
export const DATA_COLLECTION_NAME = "gif-collections-collections";
-// this is here bec async + react class component dont play nice and stutters happen. IF theres a better way of doing it pls let me know
export let cache_collections: Collection[] = [];
export const getCollections = async (): Promise => (await DataStore.get(DATA_COLLECTION_NAME)) ?? [];
-export const getCollection = async (name: string): Promise => {
- const collections = await getCollections();
- return collections.find(c => c.name === name);
-};
-
-export const getCachedCollection = (name: string): Collection | undefined => cache_collections.find(c => c.name === name);
-
-export const createCollection = async (name: string, gifs: Gif[]): Promise => {
- const collections = await getCollections();
- const duplicateCollection = collections.find(c => c.name === `${settings.store.collectionPrefix}${name}`);
- if (duplicateCollection)
- return Toasts.show({
- message: "That collection already exists",
- type: Toasts.Type.FAILURE,
- id: Toasts.genId(),
- options: {
- duration: 3000,
- position: Toasts.Position.BOTTOM
- }
- });
-
- // gifs shouldnt be empty because to create a collection you need to right click an image / gif and then create it yk. but cant hurt to have a null-conditional check RIGHT?
- const latestGifSrc = gifs[gifs.length - 1]?.src ?? settings.store.defaultEmptyCollectionImage;
- const collection: Collection = {
- name: `${settings.store.collectionPrefix}${name}`,
- src: latestGifSrc,
- format: getFormat(latestGifSrc),
- type: "Category",
- gifs,
- createdAt: Date.now(),
- lastUpdated: Date.now()
- };
-
- await DataStore.set(DATA_COLLECTION_NAME, [...collections, collection]);
- return await refreshCacheCollection();
-};
-
-export const addToCollection = async (name: string, gif: Gif): Promise => {
- const collections = await getCollections();
- const collectionIndex = collections.findIndex(c => c.name === name);
- if (collectionIndex === -1) return console.warn("collection not found");
-
- if (settings.store.preventDuplicates) {
- const isDuplicate = collections[collectionIndex].gifs.some(g => g.url === gif.url);
- if (isDuplicate) {
- return Toasts.show({
- message: "This GIF is already in the collection",
- type: Toasts.Type.FAILURE,
- id: Toasts.genId(),
- options: {
- duration: 3000,
- position: Toasts.Position.BOTTOM
- }
- });
- }
- }
-
- collections[collectionIndex].gifs.push(gif);
- collections[collectionIndex].src = gif.src;
- collections[collectionIndex].format = getFormat(gif.src);
- collections[collectionIndex].lastUpdated = Date.now();
-
- gif.addedAt = Date.now();
-
+async function saveCollections(collections: Collection[]) {
+ cache_collections = collections;
await DataStore.set(DATA_COLLECTION_NAME, collections);
- return await refreshCacheCollection();
-};
-
-export const renameCollection = async (oldName: string, newName: string): Promise => {
- const collections = await getCollections();
- const collectionIndex = collections.findIndex(c => c.name === oldName);
- if (collectionIndex === -1) return console.warn("collection not found");
-
- collections[collectionIndex].name = `${settings.store.collectionPrefix}${newName}`;
- collections[collectionIndex].lastUpdated = Date.now();
-
- await DataStore.set(DATA_COLLECTION_NAME, collections);
- return await refreshCacheCollection();
-};
-
-export const removeFromCollection = async (id: string): Promise => {
- const collections = await getCollections();
- const collectionIndex = collections.findIndex(c => c.gifs.some(g => g.id === id));
- if (collectionIndex === -1) return console.warn("collection not found");
-
- collections[collectionIndex].gifs = collections[collectionIndex].gifs.filter(g => g.id !== id);
-
- const collection = collections[collectionIndex];
- const latestGifSrc = collection.gifs.length ? collection.gifs[collection.gifs.length - 1].src : settings.store.defaultEmptyCollectionImage;
- collections[collectionIndex].src = latestGifSrc;
- collections[collectionIndex].format = getFormat(latestGifSrc);
- collections[collectionIndex].lastUpdated = Date.now();
-
- await DataStore.set(DATA_COLLECTION_NAME, collections);
- return await refreshCacheCollection();
-};
-
-export const deleteCollection = async (name: string): Promise => {
- const collections = await getCollections();
- const col = collections.filter(c => c.name !== name);
- await DataStore.set(DATA_COLLECTION_NAME, col);
- await refreshCacheCollection();
-};
+}
export const refreshCacheCollection = async (): Promise => {
cache_collections = await getCollections();
};
-export const fixPrefix = async (newPrefix: string): Promise => {
- const normalizedPrefix = newPrefix.replace(/:+$/, "") + ":";
+export async function createCollection(name: string, gifs: Gif[]): Promise {
+ const collections = [...cache_collections];
+ const fullName = `${settings.store.collectionPrefix}${name}`;
- const collections = await getCollections();
+ if (collections.some(c => c.name === fullName)) {
+ Toasts.show({ message: "That collection already exists", type: Toasts.Type.FAILURE, id: Toasts.genId(), options: { duration: 3000, position: Toasts.Position.BOTTOM } });
+ return;
+ }
- collections.forEach(c => {
- const nameParts = c.name.split(":");
- c.name = `${normalizedPrefix}${nameParts[nameParts.length - 1]}`;
+ const latestGifSrc = gifs[gifs.length - 1]?.src ?? settings.store.defaultEmptyCollectionImage;
+ collections.push({
+ name: fullName,
+ src: latestGifSrc,
+ format: getFormat(latestGifSrc),
+ type: "Category",
+ gifs,
+ createdAt: Date.now(),
+ lastUpdated: Date.now(),
});
- await DataStore.set(DATA_COLLECTION_NAME, collections);
- await refreshCacheCollection();
-};
+ await saveCollections(collections);
+}
-export const getItemCollectionNameFromId = (id: string): string | undefined => {
- const collections = cache_collections;
+export async function addToCollection(name: string, gif: Gif): Promise {
+ const collections = [...cache_collections];
+ const collection = collections.find(c => c.name === name);
+ if (!collection) return void logger.warn("Collection not found");
+
+ if (settings.store.preventDuplicates && collection.gifs.some(g => g.url === gif.url)) {
+ Toasts.show({ message: "This GIF is already in the collection", type: Toasts.Type.FAILURE, id: Toasts.genId(), options: { duration: 3000, position: Toasts.Position.BOTTOM } });
+ return;
+ }
+
+ collection.gifs = [...collection.gifs, { ...gif, addedAt: Date.now() }];
+ collection.src = gif.src;
+ collection.format = getFormat(gif.src);
+ collection.lastUpdated = Date.now();
+
+ await saveCollections(collections);
+}
+
+export async function renameCollection(oldName: string, newName: string): Promise {
+ const collections = [...cache_collections];
+ const collection = collections.find(c => c.name === oldName);
+ if (!collection) return void logger.warn("Collection not found");
+
+ collection.name = `${settings.store.collectionPrefix}${newName}`;
+ collection.lastUpdated = Date.now();
+
+ await saveCollections(collections);
+}
+
+export async function removeFromCollection(id: string): Promise {
+ const collections = [...cache_collections];
const collection = collections.find(c => c.gifs.some(g => g.id === id));
- return collection?.name;
-};
+ if (!collection) return void logger.warn("Collection not found");
-export const getGifById = (id: string): Gif | undefined => {
- const collections = cache_collections;
- const gif = collections.flatMap(c => c.gifs).find(g => g.id === id);
- return gif;
-};
+ collection.gifs = collection.gifs.filter(g => g.id !== id);
+ const latestGifSrc = collection.gifs.length ? collection.gifs[collection.gifs.length - 1].src : settings.store.defaultEmptyCollectionImage;
+ collection.src = latestGifSrc;
+ collection.format = getFormat(latestGifSrc);
+ collection.lastUpdated = Date.now();
-export const moveGifToCollection = async (gifId: string, fromCollection: string, toCollection: string): Promise => {
- const collections = await getCollections();
- const fromCollectionIndex = collections.findIndex(c => c.name === fromCollection);
- const toCollectionIndex = collections.findIndex(c => c.name === toCollection);
- if (fromCollectionIndex === -1 || toCollectionIndex === -1) return console.warn("collection not found");
+ await saveCollections(collections);
+}
- const gifIndex = collections[fromCollectionIndex].gifs.findIndex(g => g.id === gifId);
- if (gifIndex === -1) return console.warn("gif not found");
+export async function deleteCollection(name: string): Promise {
+ await saveCollections(cache_collections.filter(c => c.name !== name));
+}
- const gif = collections[fromCollectionIndex].gifs[gifIndex];
- gif.addedAt = Date.now();
- collections[fromCollectionIndex].gifs.splice(gifIndex, 1);
- collections[toCollectionIndex].gifs.push(gif);
+export async function moveGifToCollection(gifId: string, fromName: string, toName: string): Promise {
+ const collections = [...cache_collections];
+ const from = collections.find(c => c.name === fromName);
+ const to = collections.find(c => c.name === toName);
+ if (!from || !to) return void logger.warn("Collection not found");
- const fromCollectionLatestGifSrc = collections[fromCollectionIndex].gifs.length ? collections[fromCollectionIndex].gifs[collections[fromCollectionIndex].gifs.length - 1].src : settings.store.defaultEmptyCollectionImage;
- collections[fromCollectionIndex].src = fromCollectionLatestGifSrc;
- collections[fromCollectionIndex].format = getFormat(fromCollectionLatestGifSrc);
- collections[fromCollectionIndex].lastUpdated = Date.now();
+ const gifIndex = from.gifs.findIndex(g => g.id === gifId);
+ if (gifIndex === -1) return void logger.warn("Gif not found");
- const toCollectionLatestGifSrc = collections[toCollectionIndex].gifs.length ? collections[toCollectionIndex].gifs[collections[toCollectionIndex].gifs.length - 1].src : settings.store.defaultEmptyCollectionImage;
- collections[toCollectionIndex].src = toCollectionLatestGifSrc;
- collections[toCollectionIndex].format = getFormat(toCollectionLatestGifSrc);
- collections[toCollectionIndex].lastUpdated = Date.now();
+ const gif = { ...from.gifs[gifIndex], addedAt: Date.now() };
+ from.gifs = from.gifs.filter((_, i) => i !== gifIndex);
+ to.gifs = [...to.gifs, gif];
- await DataStore.set(DATA_COLLECTION_NAME, collections);
- return await refreshCacheCollection();
-};
+ const updateCollectionMeta = (col: Collection) => {
+ const latest = col.gifs.length ? col.gifs[col.gifs.length - 1].src : settings.store.defaultEmptyCollectionImage;
+ col.src = latest;
+ col.format = getFormat(latest);
+ col.lastUpdated = Date.now();
+ };
-export const updateGif = async (gifId: string, updatedGif: Gif): Promise => {
- const collections = await getCollections();
- const collectionIndex = collections.findIndex(c => c.gifs.some(g => g.id === gifId));
- if (collectionIndex === -1) return console.warn("collection not found");
+ updateCollectionMeta(from);
+ updateCollectionMeta(to);
- const gifIndex = collections[collectionIndex].gifs.findIndex(g => g.id === gifId);
- if (gifIndex === -1) return console.warn("gif not found");
+ await saveCollections(collections);
+}
- collections[collectionIndex].gifs[gifIndex] = updatedGif;
- collections[collectionIndex].lastUpdated = Date.now();
+export async function updateGif(gifId: string, updatedGif: Gif): Promise {
+ const collections = [...cache_collections];
+ const collection = collections.find(c => c.gifs.some(g => g.id === gifId));
+ if (!collection) return void logger.warn("Collection not found");
- await DataStore.set(DATA_COLLECTION_NAME, collections);
- return await refreshCacheCollection();
-};
+ const gifIndex = collection.gifs.findIndex(g => g.id === gifId);
+ if (gifIndex === -1) return void logger.warn("Gif not found");
+
+ collection.gifs = collection.gifs.map((g, i) => i === gifIndex ? updatedGif : g);
+ collection.lastUpdated = Date.now();
+
+ await saveCollections(collections);
+}
+
+export function getItemCollectionNameFromId(id: string): string | undefined {
+ return cache_collections.find(c => c.gifs.some(g => g.id === id))?.name;
+}
+
+export function getGifById(id: string): Gif | undefined {
+ return cache_collections.flatMap(c => c.gifs).find(g => g.id === id);
+}
diff --git a/src/equicordplugins/gifCollections/utils/getFormat.ts b/src/equicordplugins/gifCollections/utils/getFormat.ts
index 3ab90b8ec..238a2d781 100644
--- a/src/equicordplugins/gifCollections/utils/getFormat.ts
+++ b/src/equicordplugins/gifCollections/utils/getFormat.ts
@@ -4,8 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
-import { Format } from "@equicordplugins/gifCollections/types";
-
+import { Format } from "../types";
import { getUrlExtension } from "./getUrlExtension";
const videoExtensions = ["mp4", "ogg", "webm", "avi", "wmv", "flv", "mov", "mkv", "m4v"];
diff --git a/src/equicordplugins/gifCollections/utils/getGif.ts b/src/equicordplugins/gifCollections/utils/getGif.ts
index caf41cc68..f1f3a2a08 100644
--- a/src/equicordplugins/gifCollections/utils/getGif.ts
+++ b/src/equicordplugins/gifCollections/utils/getGif.ts
@@ -4,61 +4,58 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
-import { settings } from "@equicordplugins/gifCollections/index";
-import { Gif } from "@equicordplugins/gifCollections/types";
import { Message } from "@vencord/discord-types";
import { MessageStore, SnowflakeUtils } from "@webpack/common";
+import { settings } from "../settings";
+import { Gif } from "../types";
import { cleanUrl } from "./cleanUrl";
import { isAudio } from "./isAudio";
import { uuidv4 } from "./uuidv4";
-export function getGifByTarget(url: string, target?: HTMLDivElement | null): Gif | null {
+function isValidSnowflake(snowflake: string): boolean {
+ return !Number.isNaN(SnowflakeUtils.extractTimestamp(snowflake));
+}
+
+function getGifByTarget(url: string, target?: HTMLElement | null): Gif | null {
const liElement = target?.closest("li");
- if (!target || !liElement || !liElement.id) return null;
+ if (!target || !liElement?.id) return null;
const [channelId, messageId] = liElement.id.split("-").slice(2);
- // the isValidSnowFlake part may not be nessesery cuse either way (valid or not) message will be undefined if it doenst find a message /shrug
- if (!channelId || !messageId || !isValidSnowFlake(channelId) || !isValidSnowFlake(messageId)) return null;
+ if (!channelId || !messageId || !isValidSnowflake(channelId) || !isValidSnowflake(messageId)) return null;
const message = MessageStore.getMessage(channelId, messageId);
- if (!message || !message.embeds.length && !message.attachments.length) return null;
+ if (!message || (!message.embeds.length && !message.attachments.length)) return null;
return getGifByMessageAndUrl(url, message);
}
-export function getGifByMessageAndTarget(target: HTMLDivElement, message: Message) {
- const url = target.closest('[class*="imageWrapper"]')?.querySelector("video")?.src ?? target.closest('[class*="imageWrapper"]')?.querySelector("img")?.src;
-
+function getGifByMessageAndTarget(target: HTMLElement, message: Message): Gif | null {
+ const wrapper = target.closest("[class*=\"imageWrapper\"]");
+ const url = wrapper?.querySelector("video")?.src ?? wrapper?.querySelector("img")?.src;
if (!url) return null;
return getGifByMessageAndUrl(url, message);
}
-export function getGifByMessageAndUrl(url: string, message: Message): Gif | null {
- if (!message.embeds.length && !message.attachments.length || isAudio(url))
- return null;
+function getGifByMessageAndUrl(url: string, message: Message): Gif | null {
+ if ((!message.embeds.length && !message.attachments.length) || isAudio(url)) return null;
const cleanedUrl = cleanUrl(url);
- // find embed with matching url or image/thumbnail url
const embed = message.embeds.find(e => {
- const hasMatchingUrl = e.url && cleanUrl(e.url) === cleanedUrl;
- const hasMatchingImage = e.image && cleanUrl(e.image.url) === cleanedUrl;
- const hasMatchingImageProxy = e.image?.proxyURL === cleanedUrl;
- const hasMatchingVideoProxy = e.video?.proxyURL === cleanedUrl;
- const hasMatchingThumbnailProxy = e.thumbnail?.proxyURL === cleanedUrl;
-
- return (
- hasMatchingUrl ||
- hasMatchingImage ||
- hasMatchingImageProxy ||
- hasMatchingVideoProxy ||
- hasMatchingThumbnailProxy
- );
+ const urls = [
+ e.url && cleanUrl(e.url),
+ e.image && cleanUrl(e.image.url),
+ e.image?.proxyURL,
+ e.video?.proxyURL,
+ e.thumbnail?.proxyURL,
+ ];
+ return urls.some(u => u === cleanedUrl);
});
+
if (embed) {
- if (embed.image)
+ if (embed.image) {
return {
id: uuidv4(settings.store.itemPrefix),
height: embed.image.height,
@@ -66,62 +63,52 @@ export function getGifByMessageAndUrl(url: string, message: Message): Gif | null
src: embed.image.proxyURL!,
url: embed.image.url,
};
- // Tennor
- if (embed.video && embed.video.proxyURL) return {
- id: uuidv4(settings.store.itemPrefix),
- height: embed.video.height,
- width: embed.video.width,
- src: embed.video.proxyURL,
- url: embed.provider?.name === "Tenor" ? embed.url ?? embed.video.url : embed.video.url,
- };
-
- // Youtube thumbnails and other stuff idk
- if (embed.thumbnail && embed.thumbnail.proxyURL) return {
- id: uuidv4(settings.store.itemPrefix),
- height: embed.thumbnail.height,
- width: embed.thumbnail.width,
- src: embed.thumbnail.proxyURL,
- url: embed.thumbnail.url,
- };
+ }
+ if (embed.video?.proxyURL) {
+ return {
+ id: uuidv4(settings.store.itemPrefix),
+ height: embed.video.height,
+ width: embed.video.width,
+ src: embed.video.proxyURL,
+ url: embed.provider?.name === "Tenor" ? embed.url ?? embed.video.url : embed.video.url,
+ };
+ }
+ if (embed.thumbnail?.proxyURL) {
+ return {
+ id: uuidv4(settings.store.itemPrefix),
+ height: embed.thumbnail.height,
+ width: embed.thumbnail.width,
+ src: embed.thumbnail.proxyURL,
+ url: embed.thumbnail.url,
+ };
+ }
}
const attachment = message.attachments.find(a => cleanUrl(a.url) === cleanedUrl || a.proxy_url === cleanedUrl);
- if (attachment) return {
- id: uuidv4(settings.store.itemPrefix),
- height: attachment.height ?? 50,
- width: attachment.width ?? 50,
- src: attachment.proxy_url,
- url: attachment.url,
- channelId: message.channel_id,
- messageId: message.id,
- attachmentId: attachment.id
- };
+ if (attachment) {
+ return {
+ id: uuidv4(settings.store.itemPrefix),
+ height: attachment.height ?? 50,
+ width: attachment.width ?? 50,
+ src: attachment.proxy_url,
+ url: attachment.url,
+ channelId: message.channel_id,
+ messageId: message.id,
+ attachmentId: attachment.id,
+ };
+ }
return null;
}
-export const getGif = (message: Message | null, url: string | null, target: HTMLDivElement | null) => {
- if (message && url) {
- const gif = getGifByMessageAndUrl(url, message);
- if (!gif) return null;
-
- return gif;
- }
- if (message && target && !url) {
- const gif = getGifByMessageAndTarget(target, message);
- if (!gif) return null;
-
- return gif;
- }
+export function getGif(message: Message | null, url: string | null, target: HTMLElement | null): Gif | null {
+ if (message && url) return getGifByMessageAndUrl(url, message);
+ if (message && target && !url) return getGifByMessageAndTarget(target, message);
if (url && target && !message) {
- // youtube thumbnail url is message link for some reason eh
- const gif = getGifByTarget(url.startsWith("https://discord.com/") ? target.parentElement?.querySelector("img")?.src ?? url : url, target);
- if (!gif) return null;
-
- return gif;
+ const resolvedUrl = url.startsWith("https://discord.com/")
+ ? target.parentElement?.querySelector("img")?.src ?? url
+ : url;
+ return getGifByTarget(resolvedUrl, target);
}
-};
-
-function isValidSnowFlake(snowflake: string) {
- return !Number.isNaN(SnowflakeUtils.extractTimestamp(snowflake));
+ return null;
}
diff --git a/src/equicordplugins/gifCollections/utils/getUrlExtension.ts b/src/equicordplugins/gifCollections/utils/getUrlExtension.ts
index 05ca07165..5f33fec5a 100644
--- a/src/equicordplugins/gifCollections/utils/getUrlExtension.ts
+++ b/src/equicordplugins/gifCollections/utils/getUrlExtension.ts
@@ -5,7 +5,6 @@
*/
export function getUrlExtension(url: string) {
- // tennor stuff is like //media.tenor/blah/blah
if (!url.startsWith("https:")) url = "https:" + url;
return new URL(url).pathname.split(".").pop();
}
diff --git a/src/equicordplugins/gifCollections/utils/misc.ts b/src/equicordplugins/gifCollections/utils/misc.ts
new file mode 100644
index 000000000..685ed08b0
--- /dev/null
+++ b/src/equicordplugins/gifCollections/utils/misc.ts
@@ -0,0 +1,15 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { classNameFactory } from "@utils/css";
+import { Logger } from "@utils/Logger";
+
+export const cl = classNameFactory("vc-gif-collections-");
+export const logger = new Logger("GifCollections");
+
+export function stripPrefix(name: string): string {
+ return name.includes(":") ? name.replace(/.+?:/, "") : name;
+}
diff --git a/src/equicordplugins/gifCollections/utils/refreshUrl.ts b/src/equicordplugins/gifCollections/utils/refreshUrl.ts
index c07922ca5..76d25cdbf 100644
--- a/src/equicordplugins/gifCollections/utils/refreshUrl.ts
+++ b/src/equicordplugins/gifCollections/utils/refreshUrl.ts
@@ -6,12 +6,15 @@
import { RestAPI } from "@webpack/common";
+import { logger } from "./misc";
+
export function isCdnUrlExpired(url: string): boolean {
try {
const ex = new URL(url).searchParams.get("ex");
if (!ex) return false;
return parseInt(ex, 16) * 1000 < Date.now();
- } catch {
+ } catch (e) {
+ logger.warn("Failed to parse CDN URL expiry", e);
return false;
}
}
@@ -28,7 +31,8 @@ export async function batchRefreshAttachmentUrls(urls: string[]): Promise {
- URL.revokeObjectURL(a.href);
- document.body.removeChild(a);
- });
+ if (IS_DISCORD_DESKTOP) {
+ DiscordNative.fileManager.saveWithDialog(data, "gif-collections.json");
} else {
- DiscordNative.fileManager.saveWithDialog(data, filename);
+ saveFile(new File([data], "gif-collections.json", { type: "application/json" }));
}
-
}
export async function exportCollections() {
@@ -40,53 +28,23 @@ export async function exportCollections() {
}
export async function importCollections(data: string) {
- try {
- var parsed = JSON.parse(data);
- } catch (err) {
- console.log(data);
- throw new Error("Failed to parse JSON: " + String(err));
- }
+ const parsed = JSON.parse(data);
if ("collections" in parsed) {
await DataStore.set(DATA_COLLECTION_NAME, parsed.collections);
await refreshCacheCollection();
- } else
- throw new Error("Invalid Collections");
+ } else {
+ throw new Error("Invalid collections format");
+ }
}
export async function uploadGifCollections(showToast = true): Promise {
- if (IS_WEB || IS_EQUIBOP || IS_VESKTOP) {
- const input = document.createElement("input");
- input.type = "file";
- input.style.display = "none";
- input.accept = "application/json";
- input.onchange = async () => {
- const file = input.files?.[0];
- if (!file) return;
-
- const reader = new FileReader();
- reader.onload = async () => {
- try {
- await importCollections(reader.result as string);
- if (showToast) toastSuccess();
- } catch (err) {
- console.error(err);
- // new Logger("SettingsSync").error(err);
- if (showToast) toastFailure(err);
- }
- };
- reader.readAsText(file);
- };
-
- document.body.appendChild(input);
- input.click();
- setImmediate(() => document.body.removeChild(input));
- } else {
+ if (IS_DISCORD_DESKTOP) {
const [file] = await DiscordNative.fileManager.openFiles({
filters: [
{ name: "Gif Collections", extensions: ["json"] },
- { name: "all", extensions: ["*"] }
- ]
+ { name: "all", extensions: ["*"] },
+ ],
});
if (file) {
@@ -94,22 +52,32 @@ export async function uploadGifCollections(showToast = true): Promise {
await importCollections(new TextDecoder().decode(file.data));
if (showToast) toastSuccess();
} catch (err) {
- console.error(err);
- // new Logger("SettingsSync").error(err);
+ logger.error("Failed to import collections", err);
if (showToast) toastFailure(err);
}
}
+ } else {
+ const file = await chooseFile("application/json");
+ if (!file) return;
+
+ try {
+ await importCollections(await file.text());
+ if (showToast) toastSuccess();
+ } catch (err) {
+ logger.error("Failed to import collections", err);
+ if (showToast) toastFailure(err);
+ }
}
}
const toastSuccess = () => Toasts.show({
type: Toasts.Type.SUCCESS,
- message: "Settings successfully imported.",
- id: Toasts.genId()
+ message: "Collections imported successfully.",
+ id: Toasts.genId(),
});
-const toastFailure = (err: any) => Toasts.show({
+const toastFailure = (err: unknown) => Toasts.show({
type: Toasts.Type.FAILURE,
- message: `Failed to import settings: ${String(err)}`,
- id: Toasts.genId()
+ message: `Failed to import collections: ${String(err)}`,
+ id: Toasts.genId(),
});
diff --git a/src/equicordplugins/gifCollections/utils/uuidv4.ts b/src/equicordplugins/gifCollections/utils/uuidv4.ts
index 5ffe08916..0dae663d3 100644
--- a/src/equicordplugins/gifCollections/utils/uuidv4.ts
+++ b/src/equicordplugins/gifCollections/utils/uuidv4.ts
@@ -5,15 +5,5 @@
*/
export function uuidv4(prefix: string) {
- let d = new Date().getTime();
- d += performance.now();
- return `${prefix}xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`.replace(/[xy]/g, c => {
- const r = (d + Math.random() * 16) % 16 | 0;
- d = Math.floor(d / 16);
- if (c === "x") {
- return r.toString(16);
- } else {
- return ((r & 0x3) | 0x8).toString(16);
- }
- });
+ return `${prefix}${crypto.randomUUID()}`;
}