gifcollections refactor
This commit is contained in:
parent
a328c00fa3
commit
91a7567301
15 changed files with 938 additions and 1131 deletions
207
src/equicordplugins/gifCollections/components/contextMenus.tsx
Normal file
207
src/equicordplugins/gifCollections/components/contextMenus.tsx
Normal file
|
|
@ -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 (
|
||||
<Menu.MenuItem label="Add To Collection" key="add-to-collection" id="add-to-collection">
|
||||
{cache_collections.length > 0 && cache_collections.map(col => (
|
||||
<Menu.MenuItem
|
||||
key={col.name}
|
||||
id={col.name}
|
||||
label={stripPrefix(col.name)}
|
||||
action={() => addToCollection(col.name, gif)}
|
||||
/>
|
||||
))}
|
||||
{cache_collections.length > 0 && <Menu.MenuSeparator key="separator" />}
|
||||
<Menu.MenuItem
|
||||
key="create-collection"
|
||||
id="create-collection"
|
||||
label="Create Collection"
|
||||
action={() => openCreateCollectionModal(gif)}
|
||||
/>
|
||||
</Menu.MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
<Menu.MenuItem
|
||||
label="Copy Image Link"
|
||||
key="copy-image-link"
|
||||
id="copy-image-link"
|
||||
action={() => {
|
||||
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 (
|
||||
<Menu.Menu
|
||||
navId="gif-collection-id"
|
||||
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
|
||||
aria-label={type === "collection" ? "Delete Collection" : "Remove"}
|
||||
>
|
||||
{type === "collection" && (
|
||||
<>
|
||||
<Menu.MenuItem
|
||||
key="collection-information"
|
||||
id="collection-information"
|
||||
label="Collection Information"
|
||||
action={() => {
|
||||
const collection = cache_collections.find(c => c.name === nameOrId);
|
||||
if (collection) openCollectionInfoModal(collection);
|
||||
}}
|
||||
/>
|
||||
<Menu.MenuSeparator />
|
||||
<Menu.MenuItem
|
||||
key="rename-collection"
|
||||
id="rename-collection"
|
||||
label="Rename"
|
||||
action={() => openRenameCollectionModal(nameOrId)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{type === "gif" && (
|
||||
<>
|
||||
<Menu.MenuItem
|
||||
key="gif-information"
|
||||
id="gif-information"
|
||||
label="Information"
|
||||
action={() => {
|
||||
const gif = getGifById(nameOrId);
|
||||
if (gif) openGifInfoModal(gif);
|
||||
}}
|
||||
/>
|
||||
<Menu.MenuSeparator />
|
||||
<Menu.MenuItem
|
||||
key="copy-url"
|
||||
id="copy-url"
|
||||
label="Copy URL"
|
||||
action={() => {
|
||||
const gif = getGifById(nameOrId);
|
||||
if (!gif) return;
|
||||
copyToClipboard(gif.url);
|
||||
showToast("URL copied to clipboard", Toasts.Type.SUCCESS);
|
||||
}}
|
||||
/>
|
||||
<Menu.MenuItem
|
||||
key="move-to-collection"
|
||||
id="move-to-collection"
|
||||
label="Move To Collection"
|
||||
action={() => openMoveToCollectionModal(nameOrId)}
|
||||
/>
|
||||
<Menu.MenuSeparator />
|
||||
</>
|
||||
)}
|
||||
<Menu.MenuItem
|
||||
key="delete-collection"
|
||||
id="delete-collection"
|
||||
label={type === "collection" ? "Delete Collection" : "Remove"}
|
||||
action={() => {
|
||||
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,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Menu.Menu>
|
||||
);
|
||||
}
|
||||
|
||||
export function GifPickerContextMenu({ gif }: { gif: Gif; }) {
|
||||
return (
|
||||
<Menu.Menu
|
||||
navId="gif-collection-id"
|
||||
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
|
||||
aria-label="Gif Collections"
|
||||
>
|
||||
{settings.store.showCopyImageLink && (
|
||||
<Menu.MenuItem
|
||||
label="Copy Image Link"
|
||||
key="copy-image-link"
|
||||
id="copy-image-link"
|
||||
action={() => {
|
||||
copyToClipboard(gif.url);
|
||||
showToast("Image link copied to clipboard", Toasts.Type.SUCCESS);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{AddToCollectionMenu(gif)}
|
||||
</Menu.Menu>
|
||||
);
|
||||
}
|
||||
|
||||
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, () =>
|
||||
<RemoveItemContextMenu type="collection" nameOrId={item.name!} instance={instance} />
|
||||
);
|
||||
}
|
||||
if (item?.id?.startsWith(GIF_ITEM_PREFIX)) {
|
||||
return ContextMenuApi.openContextMenu(e, () =>
|
||||
<RemoveItemContextMenu type="gif" nameOrId={item.id!} instance={instance} />
|
||||
);
|
||||
}
|
||||
const { src, url, height, width } = item;
|
||||
if (src && url && height != null && width != null && !item.id?.startsWith(GIF_ITEM_PREFIX)) {
|
||||
return ContextMenuApi.openContextMenu(e, () =>
|
||||
<GifPickerContextMenu gif={{ id: uuidv4(GIF_ITEM_PREFIX), src, url, height, width }} />
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
172
src/equicordplugins/gifCollections/components/modals.tsx
Normal file
172
src/equicordplugins/gifCollections/components/modals.tsx
Normal file
|
|
@ -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 => (
|
||||
<InfoModal props={props} title="Collection Information" rows={[
|
||||
{ label: "Name", value: stripPrefix(collection.name) },
|
||||
{ label: "Gifs", value: String(collection.gifs.length) },
|
||||
{ label: "Created At", value: collection.createdAt ? new Date(collection.createdAt).toLocaleString() : "Unknown" },
|
||||
{ label: "Last Updated", value: collection.lastUpdated ? new Date(collection.lastUpdated).toLocaleString() : "Unknown" },
|
||||
]} />
|
||||
));
|
||||
}
|
||||
|
||||
export function openGifInfoModal(gif: Gif) {
|
||||
openModal(props => (
|
||||
<InfoModal props={props} title="Information" rows={[
|
||||
{ label: "Added At", value: gif.addedAt ? new Date(gif.addedAt).toLocaleString() : "Unknown" },
|
||||
{ label: "Width", value: String(gif.width) },
|
||||
{ label: "Height", value: String(gif.height) },
|
||||
]} />
|
||||
));
|
||||
}
|
||||
|
||||
export function openMoveToCollectionModal(gifId: string) {
|
||||
openModal(props => <MoveToCollectionModal props={props} gifId={gifId} />);
|
||||
}
|
||||
|
||||
export function openCreateCollectionModal(gif: Gif) {
|
||||
openModal(props => <CreateCollectionModal props={props} gif={gif} />);
|
||||
}
|
||||
|
||||
export function openRenameCollectionModal(name: string) {
|
||||
openModal(props => <RenameCollectionModal props={props} name={name} />);
|
||||
}
|
||||
|
||||
function InfoModal({ props, title, rows }: { props: ModalProps; title: string; rows: { label: string; value: string; }[]; }) {
|
||||
return (
|
||||
<ModalRoot {...props} size={ModalSize.SMALL} className={cl("modal")}>
|
||||
<ModalHeader separator={false} className={cl("modal-header")}>
|
||||
<Paragraph className={cl("modal-title")}>{title}</Paragraph>
|
||||
</ModalHeader>
|
||||
<ModalContent className={cl("modal-content")}>
|
||||
<section>
|
||||
{rows.map(row => (
|
||||
<Flex key={row.label} className={cl("info-row")}>
|
||||
<Heading className={cl("info-title")}>{row.label}</Heading>
|
||||
<Paragraph className={cl("info-text")}>{row.value}</Paragraph>
|
||||
</Flex>
|
||||
))}
|
||||
</section>
|
||||
</ModalContent>
|
||||
<ModalFooter className={cl("modal-footer")}>
|
||||
<Button onClick={props.onClose}>Close</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
);
|
||||
}
|
||||
|
||||
function MoveToCollectionModal({ props, gifId }: { props: ModalProps; gifId: string; }) {
|
||||
return (
|
||||
<ModalRoot {...props} size={ModalSize.SMALL} className={cl("modal")}>
|
||||
<ModalHeader separator={false} className={cl("modal-header")}>
|
||||
<Paragraph className={cl("modal-title")}>Move To Collection</Paragraph>
|
||||
</ModalHeader>
|
||||
<ModalContent className={cl("modal-content")}>
|
||||
<Heading>Select a collection to move the item to</Heading>
|
||||
<div className={cl("buttons")}>
|
||||
{cache_collections
|
||||
.filter(col => col.name !== getItemCollectionNameFromId(gifId))
|
||||
.map(col => (
|
||||
<Button
|
||||
key={col.name}
|
||||
className={cl("button")}
|
||||
onClick={async () => {
|
||||
const fromCollection = getItemCollectionNameFromId(gifId);
|
||||
if (!fromCollection) return;
|
||||
await moveGifToCollection(gifId, fromCollection, col.name);
|
||||
FluxDispatcher.dispatch({ type: "GIF_PICKER_QUERY", query: "" });
|
||||
FluxDispatcher.dispatch({ type: "GIF_PICKER_QUERY", query: fromCollection });
|
||||
props.onClose();
|
||||
}}
|
||||
>
|
||||
{stripPrefix(col.name)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</ModalContent>
|
||||
<ModalFooter className={cl("modal-footer")}>
|
||||
<Button onClick={props.onClose}>Close</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<ModalRoot {...props}>
|
||||
<form onSubmit={onSubmit}>
|
||||
<ModalHeader>
|
||||
<Paragraph>Create Collection</Paragraph>
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Heading className={cl("rename-text")}>Collection Name</Heading>
|
||||
<TextInput onChange={setName} />
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Button type="submit" color={Button.Colors.GREEN} disabled={!name.length} onClick={onSubmit}>
|
||||
Create
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</ModalRoot>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<ModalRoot {...props}>
|
||||
<form onSubmit={onSubmit}>
|
||||
<ModalHeader>
|
||||
<Paragraph>Rename Collection</Paragraph>
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Paragraph className={cl("rename-text")}>New Collection Name</Paragraph>
|
||||
<TextInput value={newName} className={classes(cl("rename-input"), tooLong ? cl("input-warning") : "")} onChange={setNewName} />
|
||||
{tooLong && <Paragraph className={cl("warning-text")}>Name can't be longer than 24 characters</Paragraph>}
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Button type="submit" color={Button.Colors.GREEN} disabled={!newName.length || tooLong} onClick={onSubmit}>
|
||||
Rename
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</ModalRoot>
|
||||
);
|
||||
}
|
||||
|
|
@ -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(
|
||||
<Menu.MenuItem
|
||||
label="Copy Image Link"
|
||||
key="copy-image-link"
|
||||
id="copy-image-link"
|
||||
action={() => {
|
||||
copyToClipboard(gif.url);
|
||||
showToast("Image link copied to clipboard", Toasts.Type.SUCCESS);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
group.push(
|
||||
<Menu.MenuItem
|
||||
label="Add To Collection"
|
||||
key="add-to-collection"
|
||||
id="add-to-collection"
|
||||
>
|
||||
{collections.length > 0 && collections.map(col => (
|
||||
<Menu.MenuItem
|
||||
key={col.name}
|
||||
id={col.name}
|
||||
label={col.name.replace(/.+?:/, "")}
|
||||
action={() => addToCollection(col.name, gif)}
|
||||
/>
|
||||
))}
|
||||
|
||||
{collections.length > 0 && <Menu.MenuSeparator key="separator" />}
|
||||
|
||||
<Menu.MenuItem
|
||||
key="create-collection"
|
||||
id="create-collection"
|
||||
label="Create Collection"
|
||||
action={() => {
|
||||
openModal(modalProps => (
|
||||
<CreateCollectionModal onClose={modalProps.onClose} gif={gif} modalProps={modalProps} />
|
||||
));
|
||||
}}
|
||||
/>
|
||||
</Menu.MenuItem>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
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 (
|
||||
<div className="collections-sort-container">
|
||||
<Heading className="collections-sort-title">Sort Collections</Heading>
|
||||
<Divider className="collections-sort-divider" />
|
||||
<Paragraph className="collections-sort-description">
|
||||
Choose a sorting criteria for your collections
|
||||
</Paragraph>
|
||||
<Divider className="collections-sort-divider" />
|
||||
<div className="collections-sort-section">
|
||||
<Paragraph className="collections-sort-section-title">Sort By</Paragraph>
|
||||
<div className="collections-sort-option">
|
||||
<label className="collections-sort-label">
|
||||
<input
|
||||
type="radio"
|
||||
name="sortType"
|
||||
value={SortingOptions.NAME}
|
||||
checked={sortType === SortingOptions.NAME}
|
||||
onChange={() => handleSortTypeChange(SortingOptions.NAME)}
|
||||
className="collections-sort-input"
|
||||
/>
|
||||
Name
|
||||
</label>
|
||||
</div>
|
||||
<div className="collections-sort-option">
|
||||
<label className="collections-sort-label">
|
||||
<input
|
||||
type="radio"
|
||||
name="sortType"
|
||||
value={SortingOptions.CREATION_DATE}
|
||||
checked={sortType === SortingOptions.CREATION_DATE}
|
||||
onChange={() => handleSortTypeChange(SortingOptions.CREATION_DATE)}
|
||||
className="collections-sort-input"
|
||||
/>
|
||||
Creation Date
|
||||
</label>
|
||||
</div>
|
||||
<div className="collections-sort-option">
|
||||
<label className="collections-sort-label">
|
||||
<input
|
||||
type="radio"
|
||||
name="sortType"
|
||||
value={SortingOptions.MODIFIED_DATE}
|
||||
checked={sortType === SortingOptions.MODIFIED_DATE}
|
||||
onChange={() => handleSortTypeChange(SortingOptions.MODIFIED_DATE)}
|
||||
className="collections-sort-input"
|
||||
/>
|
||||
Modified Date
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<Divider className="collections-sort-divider" />
|
||||
<div className="collections-sort-section">
|
||||
<Paragraph className="collections-sort-section-title">Order</Paragraph>
|
||||
<div className="collections-sort-option">
|
||||
<label className="collections-sort-label">
|
||||
<input
|
||||
type="radio"
|
||||
name="sortOrder"
|
||||
value="asc"
|
||||
checked={sortOrder === "asc"}
|
||||
onChange={() => handleSortOrderChange("asc")}
|
||||
className="collections-sort-input"
|
||||
/>
|
||||
Ascending
|
||||
</label>
|
||||
</div>
|
||||
<div className="collections-sort-option">
|
||||
<label className="collections-sort-label">
|
||||
<input
|
||||
type="radio"
|
||||
name="sortOrder"
|
||||
value="desc"
|
||||
checked={sortOrder === "desc"}
|
||||
onChange={() => handleSortOrderChange("desc")}
|
||||
className="collections-sort-input"
|
||||
/>
|
||||
Descending
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
importGifs: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "Import Collections",
|
||||
component: () =>
|
||||
<Button onClick={async () =>
|
||||
(await getCollections()).length ? Alerts.show({
|
||||
title: "Are you sure?",
|
||||
body: "Importing collections will overwrite your current collections.",
|
||||
confirmText: "Import",
|
||||
confirmColor: Button.Colors.RED,
|
||||
cancelText: "Nevermind",
|
||||
onConfirm: async () => uploadGifCollections()
|
||||
}) : uploadGifCollections()}>
|
||||
Import Collections
|
||||
</Button>,
|
||||
},
|
||||
exportGifs: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "Export Collections",
|
||||
component: () =>
|
||||
<Button onClick={downloadCollections}>
|
||||
Export Collections
|
||||
</Button>
|
||||
},
|
||||
resetCollections: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "Reset Collections",
|
||||
component: () =>
|
||||
<Button onClick={() =>
|
||||
Alerts.show({
|
||||
title: "Are you sure?",
|
||||
body: "Resetting collections will remove all your collections.",
|
||||
confirmText: "Reset",
|
||||
confirmColor: Button.Colors.RED,
|
||||
cancelText: "Nevermind",
|
||||
onConfirm: async () => {
|
||||
await DataStore.set(DATA_COLLECTION_NAME, []);
|
||||
refreshCacheCollection();
|
||||
}
|
||||
})}>
|
||||
Reset Collections
|
||||
</Button>
|
||||
}
|
||||
});
|
||||
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<string>(
|
||||
expiredGifs.flatMap(g => [g.src, g.url].filter((u): u is string => !!u && isCdnUrlExpired(u)))
|
||||
)];
|
||||
const refreshInBatches = async () => {
|
||||
const fullMap: Record<string, string> = {};
|
||||
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<void>(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<string>(
|
||||
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<string, string> = {};
|
||||
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, () =>
|
||||
<RemoveItemContextMenu type="collection" nameOrId={item.name} instance={instance} />
|
||||
);
|
||||
}
|
||||
if (item?.id?.startsWith(GIF_ITEM_PREFIX)) {
|
||||
return ContextMenuApi.openContextMenu(e, () =>
|
||||
<RemoveItemContextMenu type="gif" nameOrId={item.id} instance={instance} />
|
||||
);
|
||||
}
|
||||
const { src, url, height, width } = item;
|
||||
if (src && url && height != null && width != null && !item.id?.startsWith(GIF_ITEM_PREFIX)) {
|
||||
return ContextMenuApi.openContextMenu(e, () =>
|
||||
<Menu.Menu
|
||||
navId="gif-collection-id"
|
||||
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
|
||||
aria-label="Gif Collections"
|
||||
>
|
||||
{MenuThingy({ gif: { ...item, id: uuidv4(GIF_ITEM_PREFIX) } })}
|
||||
</Menu.Menu>
|
||||
);
|
||||
}
|
||||
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 }) => (
|
||||
<Menu.Menu
|
||||
navId="gif-collection-id"
|
||||
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
|
||||
aria-label={type === "collection" ? "Delete Collection" : "Remove"}
|
||||
>
|
||||
{type === "collection" && (
|
||||
<>
|
||||
<Menu.MenuItem
|
||||
key="collection-information"
|
||||
id="collection-information"
|
||||
label="Collection Information"
|
||||
action={() => {
|
||||
const collection = cache_collections.find(c => c.name === nameOrId);
|
||||
if (!collection) return;
|
||||
openModal(modalProps => (
|
||||
<ModalRoot
|
||||
{...modalProps}
|
||||
size={ModalSize.SMALL}
|
||||
transitionState={modalProps.transitionState}
|
||||
className="custom-modal"
|
||||
>
|
||||
<ModalHeader separator={false} className="custom-modal-header">
|
||||
<Paragraph className="custom-modal-title">Collection Information</Paragraph>
|
||||
</ModalHeader>
|
||||
<ModalContent className="custom-modal-content">
|
||||
<section>
|
||||
<Flex className="collection-info">
|
||||
<Heading className="collection-info-title">Name</Heading>
|
||||
<Paragraph className="collection-info-text">{collection.name.replace(/.+?:/, "")}</Paragraph>
|
||||
</Flex>
|
||||
<Flex className="collection-info">
|
||||
<Heading className="collection-info-title">Gifs</Heading>
|
||||
<Paragraph className="collection-info-text">{collection.gifs.length}</Paragraph>
|
||||
</Flex>
|
||||
<Flex className="collection-info">
|
||||
<Heading className="collection-info-title">Created At</Heading>
|
||||
<Paragraph className="collection-info-text">{collection.createdAt ? new Date(collection.createdAt).toLocaleString() : "Unknown"}</Paragraph>
|
||||
</Flex>
|
||||
<Flex className="collection-info">
|
||||
<Heading className="collection-info-title">Last Updated</Heading>
|
||||
<Paragraph className="collection-info-text">{collection.lastUpdated ? new Date(collection.lastUpdated).toLocaleString() : "Unknown"}</Paragraph>
|
||||
</Flex>
|
||||
</section>
|
||||
</ModalContent>
|
||||
<ModalFooter className="custom-modal-footer">
|
||||
<Button onClick={modalProps.onClose} className="custom-modal-button">Close</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
));
|
||||
}}
|
||||
/>
|
||||
<Menu.MenuSeparator />
|
||||
<Menu.MenuItem
|
||||
key="rename-collection"
|
||||
id="rename-collection"
|
||||
label="Rename"
|
||||
action={() => openModal(modalProps => (
|
||||
<RenameCollectionModal
|
||||
onClose={modalProps.onClose}
|
||||
name={nameOrId}
|
||||
modalProps={modalProps}
|
||||
/>
|
||||
))}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{type === "gif" && (
|
||||
<>
|
||||
<Menu.MenuItem
|
||||
key="gif-information"
|
||||
id="gif-information"
|
||||
label="Information"
|
||||
action={() => {
|
||||
const gifInfo = getGifById(nameOrId);
|
||||
if (!gifInfo) return;
|
||||
openModal(modalProps => (
|
||||
<ModalRoot
|
||||
{...modalProps}
|
||||
size={ModalSize.SMALL}
|
||||
transitionState={modalProps.transitionState}
|
||||
className="custom-modal"
|
||||
>
|
||||
<ModalHeader separator={false} className="custom-modal-header">
|
||||
<Paragraph className="custom-modal-title">Information</Paragraph>
|
||||
</ModalHeader>
|
||||
<ModalContent className="custom-modal-content">
|
||||
<section>
|
||||
<Flex className="gif-info">
|
||||
<Heading className="gif-info-title">Added At</Heading>
|
||||
<Paragraph className="gif-info-text">{gifInfo.addedAt ? new Date(gifInfo.addedAt).toLocaleString() : "Unknown"}</Paragraph>
|
||||
</Flex>
|
||||
<Flex className="gif-info">
|
||||
<Heading className="gif-info-title">Width</Heading>
|
||||
<Paragraph className="gif-info-text">{gifInfo.width}</Paragraph>
|
||||
</Flex>
|
||||
<Flex className="gif-info">
|
||||
<Heading className="gif-info-title">Height</Heading>
|
||||
<Paragraph className="gif-info-text">{gifInfo.height}</Paragraph>
|
||||
</Flex>
|
||||
</section>
|
||||
</ModalContent>
|
||||
<ModalFooter className="custom-modal-footer">
|
||||
<Button onClick={modalProps.onClose} className="custom-modal-button">Close</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
));
|
||||
}}
|
||||
/>
|
||||
<Menu.MenuSeparator />
|
||||
<Menu.MenuItem
|
||||
key="copy-url"
|
||||
id="copy-url"
|
||||
label="Copy URL"
|
||||
action={() => {
|
||||
const gifInfo = getGifById(nameOrId);
|
||||
if (!gifInfo) return;
|
||||
copyToClipboard(gifInfo.url);
|
||||
showToast("URL copied to clipboard", Toasts.Type.SUCCESS);
|
||||
}}
|
||||
/>
|
||||
<Menu.MenuItem
|
||||
key="move-to-collection"
|
||||
id="move-to-collection"
|
||||
label="Move To Collection"
|
||||
action={() => {
|
||||
openModal(modalProps => (
|
||||
<ModalRoot
|
||||
{...modalProps}
|
||||
size={ModalSize.SMALL}
|
||||
transitionState={modalProps.transitionState}
|
||||
className="custom-modal"
|
||||
>
|
||||
<ModalHeader separator={false} className="custom-modal-header">
|
||||
<Paragraph className="custom-modal-title">Move To Collection</Paragraph>
|
||||
</ModalHeader>
|
||||
<ModalContent className="custom-modal-content">
|
||||
<Heading className="custom-modal-text">
|
||||
Select a collection to move the item to
|
||||
</Heading>
|
||||
<div className="collection-buttons">
|
||||
{cache_collections
|
||||
.filter(col => col.name !== getItemCollectionNameFromId(nameOrId))
|
||||
.map(col => (
|
||||
<Button
|
||||
key={col.name}
|
||||
id={col.name}
|
||||
onClick={async () => {
|
||||
const fromCollection = getItemCollectionNameFromId(nameOrId);
|
||||
if (!fromCollection) return;
|
||||
await moveGifToCollection(nameOrId, fromCollection, col.name);
|
||||
FluxDispatcher.dispatch({
|
||||
type: "GIF_PICKER_QUERY",
|
||||
query: `${fromCollection} `
|
||||
});
|
||||
FluxDispatcher.dispatch({
|
||||
type: "GIF_PICKER_QUERY",
|
||||
query: `${fromCollection}`
|
||||
});
|
||||
modalProps.onClose();
|
||||
}}
|
||||
className="collection-button"
|
||||
>
|
||||
{col.name.replace(/.+?:/, "")}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</ModalContent>
|
||||
<ModalFooter className="custom-modal-footer">
|
||||
<Button onClick={modalProps.onClose} className="custom-modal-button">Close</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
));
|
||||
}}
|
||||
/>
|
||||
<Menu.MenuSeparator />
|
||||
</>
|
||||
)}
|
||||
<Menu.MenuItem
|
||||
key="delete-collection"
|
||||
id="delete-collection"
|
||||
label={type === "collection" ? "Delete Collection" : "Remove"}
|
||||
action={async () => {
|
||||
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}`
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Menu.Menu>
|
||||
);
|
||||
|
||||
const MenuThingy = ({ gif }) => {
|
||||
const collections = cache_collections;
|
||||
const menuItems: React.ReactNode[] = [];
|
||||
|
||||
if (settings.store.showCopyImageLink) {
|
||||
menuItems.push(
|
||||
<Menu.MenuItem
|
||||
label="Copy Image Link"
|
||||
key="copy-image-link"
|
||||
id="copy-image-link"
|
||||
action={() => {
|
||||
copyToClipboard(gif.url);
|
||||
showToast("Image link copied to clipboard", Toasts.Type.SUCCESS);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
menuItems.push(
|
||||
<Menu.MenuItem label="Add To Collection" key="add-to-collection" id="add-to-collection">
|
||||
{collections.map(col => (
|
||||
<Menu.MenuItem
|
||||
key={col.name}
|
||||
id={col.name}
|
||||
label={col.name.replace(/.+?:/, "")}
|
||||
action={() => addToCollection(col.name, gif)}
|
||||
/>
|
||||
))}
|
||||
{collections.length > 0 && <Menu.MenuSeparator />}
|
||||
<Menu.MenuItem
|
||||
key="create-collection"
|
||||
id="create-collection"
|
||||
label="Create Collection"
|
||||
action={() => openModal(modalProps => (
|
||||
<CreateCollectionModal onClose={modalProps.onClose} gif={gif} modalProps={modalProps} />
|
||||
))}
|
||||
/>
|
||||
</Menu.MenuItem>
|
||||
);
|
||||
|
||||
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 (
|
||||
<ModalRoot {...modalProps}>
|
||||
<form onSubmit={onSubmit}>
|
||||
<ModalHeader>
|
||||
<Paragraph>Create Collection</Paragraph>
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Heading style={{ marginTop: "10px" }}>Collection Name</Heading>
|
||||
<TextInput onChange={e => setName(e)} />
|
||||
</ModalContent>
|
||||
<div style={{ marginTop: "1rem" }}>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
color={Button.Colors.GREEN}
|
||||
disabled={!name.length}
|
||||
onClick={onSubmit}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</div>
|
||||
</form>
|
||||
</ModalRoot>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<ModalRoot {...modalProps}>
|
||||
<form onSubmit={onSubmit}>
|
||||
<ModalHeader>
|
||||
<Paragraph>Rename Collection</Paragraph>
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Paragraph className="rename-collection-text">New Collection Name</Paragraph>
|
||||
<TextInput value={newName} className={`rename-collection-input ${newName.length >= 25 ? "input-warning" : ""}`} onChange={e => setNewName(e)} />
|
||||
{newName.length >= 25 && <Paragraph className="warning-text">Name can't be longer than 24 characters</Paragraph>}
|
||||
</ModalContent>
|
||||
<div style={{ marginTop: "1rem" }}>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
color={Button.Colors.GREEN}
|
||||
disabled={!newName.length || newName.length >= 25}
|
||||
onClick={onSubmit}
|
||||
>
|
||||
Rename
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</div>
|
||||
</form>
|
||||
</ModalRoot>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
171
src/equicordplugins/gifCollections/settings.tsx
Normal file
171
src/equicordplugins/gifCollections/settings.tsx
Normal file
|
|
@ -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: () =>
|
||||
<Button onClick={async () =>
|
||||
(await getCollections()).length ? Alerts.show({
|
||||
title: "Are you sure?",
|
||||
body: "Importing collections will overwrite your current collections.",
|
||||
confirmText: "Import",
|
||||
confirmColor: Button.Colors.RED,
|
||||
cancelText: "Nevermind",
|
||||
onConfirm: () => uploadGifCollections(),
|
||||
}) : uploadGifCollections()}>
|
||||
Import Collections
|
||||
</Button>,
|
||||
},
|
||||
exportGifs: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "Export collections.",
|
||||
component: () =>
|
||||
<Button onClick={downloadCollections}>
|
||||
Export Collections
|
||||
</Button>,
|
||||
},
|
||||
resetCollections: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "Reset collections.",
|
||||
component: () =>
|
||||
<Button onClick={() =>
|
||||
Alerts.show({
|
||||
title: "Are you sure?",
|
||||
body: "Resetting collections will remove all your collections.",
|
||||
confirmText: "Reset",
|
||||
confirmColor: Button.Colors.RED,
|
||||
cancelText: "Nevermind",
|
||||
onConfirm: async () => {
|
||||
await DataStore.set(DATA_COLLECTION_NAME, []);
|
||||
refreshCacheCollection();
|
||||
},
|
||||
})}>
|
||||
Reset Collections
|
||||
</Button>,
|
||||
},
|
||||
});
|
||||
|
||||
function RadioOption({ name, value, checked, onChange, label }: { name: string; value: string | number; checked: boolean; onChange: () => void; label: string; }) {
|
||||
return (
|
||||
<div className={cl("sort-option")}>
|
||||
<label className={cl("sort-label")}>
|
||||
<input type="radio" name={name} value={value} checked={checked} onChange={onChange} className={cl("sort-input")} />
|
||||
{label}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={cl("sort-container")}>
|
||||
<Heading className={cl("sort-title")}>Sort Collections</Heading>
|
||||
<Divider className={cl("sort-divider")} />
|
||||
<Paragraph className={cl("sort-description")}>Choose a sorting criteria for your collections</Paragraph>
|
||||
<Divider className={cl("sort-divider")} />
|
||||
<div className={cl("sort-section")}>
|
||||
<Paragraph className={cl("sort-section-title")}>Sort By</Paragraph>
|
||||
<RadioOption name="sortType" value={SortingOptions.NAME} checked={sortType === SortingOptions.NAME} onChange={() => handleSortType(SortingOptions.NAME)} label="Name" />
|
||||
<RadioOption name="sortType" value={SortingOptions.CREATION_DATE} checked={sortType === SortingOptions.CREATION_DATE} onChange={() => handleSortType(SortingOptions.CREATION_DATE)} label="Creation Date" />
|
||||
<RadioOption name="sortType" value={SortingOptions.MODIFIED_DATE} checked={sortType === SortingOptions.MODIFIED_DATE} onChange={() => handleSortType(SortingOptions.MODIFIED_DATE)} label="Modified Date" />
|
||||
</div>
|
||||
<Divider className={cl("sort-divider")} />
|
||||
<div className={cl("sort-section")}>
|
||||
<Paragraph className={cl("sort-section-title")}>Order</Paragraph>
|
||||
<RadioOption name="sortOrder" value="asc" checked={sortOrder === "asc"} onChange={() => handleSortOrder("asc")} label="Ascending" />
|
||||
<RadioOption name="sortOrder" value="desc" checked={sortOrder === "desc"} onChange={() => handleSortOrder("desc")} label="Descending" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
|
||||
.vc-gif-collections-button:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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, K extends keyof T> = T & { [P in K]-?: T[P] };
|
||||
|
||||
export type Collection = WithRequired<Category, "gifs">;
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Collection[]> => (await DataStore.get<Collection[]>(DATA_COLLECTION_NAME)) ?? [];
|
||||
|
||||
export const getCollection = async (name: string): Promise<Collection | undefined> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
cache_collections = await getCollections();
|
||||
};
|
||||
|
||||
export const fixPrefix = async (newPrefix: string): Promise<void> => {
|
||||
const normalizedPrefix = newPrefix.replace(/:+$/, "") + ":";
|
||||
export async function createCollection(name: string, gifs: Gif[]): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> => {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> => {
|
||||
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<void> {
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
15
src/equicordplugins/gifCollections/utils/misc.ts
Normal file
15
src/equicordplugins/gifCollections/utils/misc.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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<Record
|
|||
map[original] = refreshed;
|
||||
}
|
||||
return map;
|
||||
} catch {
|
||||
} catch (e) {
|
||||
logger.warn("Failed to refresh attachment URLs", e);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,33 +5,21 @@
|
|||
*/
|
||||
|
||||
import { DataStore } from "@api/index";
|
||||
import { chooseFile, saveFile } from "@utils/web";
|
||||
import { Toasts } from "@webpack/common";
|
||||
|
||||
import { DATA_COLLECTION_NAME, getCollections, refreshCacheCollection } from "./collectionManager";
|
||||
|
||||
// 99% of this is coppied from src/utils/settingsSync.ts
|
||||
import { logger } from "./misc";
|
||||
|
||||
export async function downloadCollections() {
|
||||
const filename = "gif-collections.json";
|
||||
const exportData = await exportCollections();
|
||||
const data = new TextEncoder().encode(exportData);
|
||||
|
||||
if (IS_WEB || IS_EQUIBOP || IS_VESKTOP) {
|
||||
const file = new File([data], filename, { type: "application/json" });
|
||||
const a = document.createElement("a");
|
||||
a.href = URL.createObjectURL(file);
|
||||
a.download = filename;
|
||||
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
setImmediate(() => {
|
||||
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<void> {
|
||||
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<void> {
|
|||
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(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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()}`;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue