mirror of
https://github.com/Equicord/Equicord.git
synced 2025-06-26 06:38:24 -04:00
Fixes
This commit is contained in:
parent
93a96bc120
commit
d18e1b2415
43 changed files with 6 additions and 9034 deletions
|
@ -1,111 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { DataStore } from "@api/index";
|
||||
import { Toasts } from "@webpack/common";
|
||||
|
||||
import { settings } from "./index";
|
||||
import { Collection, Gif } from "./types";
|
||||
import { getFormat } from "./utils/getFormat";
|
||||
|
||||
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 === `gc:${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 = {
|
||||
name: `gc:${name}`,
|
||||
src: latestGifSrc,
|
||||
format: getFormat(latestGifSrc),
|
||||
type: "Category",
|
||||
gifs
|
||||
};
|
||||
|
||||
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");
|
||||
|
||||
collections[collectionIndex].gifs.push(gif);
|
||||
collections[collectionIndex].src = gif.src;
|
||||
collections[collectionIndex].format = getFormat(gif.src);
|
||||
|
||||
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");
|
||||
|
||||
// Remove The Gif
|
||||
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);
|
||||
|
||||
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();
|
||||
};
|
|
@ -1,21 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// cant change them now eh. My friend uses this plugin. LOVE YOU FREEZER
|
||||
export const GIF_ITEM_PREFIX = "gc-moment:";
|
||||
export const GIF_COLLECTION_PREFIX = "gc:";
|
|
@ -1,352 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// Plugin idea by brainfreeze (668137937333911553) 😎
|
||||
|
||||
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Alerts, Button, ContextMenuApi, FluxDispatcher, Forms, Menu, React, TextInput, useCallback, useState } from "@webpack/common";
|
||||
|
||||
import * as CollectionManager from "./CollectionManager";
|
||||
import { GIF_COLLECTION_PREFIX, GIF_ITEM_PREFIX } from "./constants";
|
||||
import { Category, Collection, Gif, Props } from "./types";
|
||||
import { getFormat } from "./utils/getFormat";
|
||||
import { getGif } from "./utils/getGif";
|
||||
import { downloadCollections, uploadGifCollections } from "./utils/settingsUtils";
|
||||
import { uuidv4 } from "./utils/uuidv4";
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
defaultEmptyCollectionImage: {
|
||||
description: "The image / gif that will be shown when a collection has no images / gifs",
|
||||
type: OptionType.STRING,
|
||||
default: "https://i.imgur.com/TFatP8r.png"
|
||||
},
|
||||
importGifs: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "Import Collections",
|
||||
component: () =>
|
||||
<Button onClick={async () =>
|
||||
// if they have collections show the warning
|
||||
(await CollectionManager.getCollections()).length ? Alerts.show({
|
||||
title: "Are you sure?",
|
||||
body: "Importing collections will overwrite your current collections.",
|
||||
confirmText: "Import",
|
||||
// wow this works?
|
||||
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>
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
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")) {
|
||||
group.push(
|
||||
// if i do it the normal way i get a invalid context menu thingy error -> Menu API only allows Items and groups of Items as children.
|
||||
MenuThingy({ gif })
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export default definePlugin({
|
||||
name: "Gif Collection",
|
||||
// need better description eh
|
||||
description: "Allows you to have collections of gifs",
|
||||
authors: [Devs.Aria],
|
||||
patches: [
|
||||
{
|
||||
find: "renderCategoryExtras",
|
||||
replacement: [
|
||||
// This patch adds the collections to the gif part yk
|
||||
{
|
||||
match: /(\i\.render=function\(\){)(.{1,50}getItemGrid)/,
|
||||
replace: "$1;$self.insertCollections(this);$2"
|
||||
},
|
||||
// Hides the gc: from the name gc:monkeh -> monkeh
|
||||
{
|
||||
match: /(\i\.renderCategoryExtras=function\((?<props>\i)\){)var (?<varName>\i)=\i\.name,/,
|
||||
replace: "$1var $<varName>=$self.hidePrefix($<props>),"
|
||||
},
|
||||
// Replaces this.props.resultItems with the collection.gifs
|
||||
{
|
||||
match: /(\i\.renderContent=function\(\){)(.{1,50}resultItems)/,
|
||||
replace: "$1;$self.renderContent(this);$2"
|
||||
},
|
||||
// Delete context menu for collection
|
||||
{
|
||||
match: /(\i\.render=function\(\){.{1,100}renderExtras.{1,200}onClick:this\.handleClick,)/,
|
||||
replace: "$1onContextMenu: (e) => $self.collectionContextMenu(e, this),"
|
||||
},
|
||||
]
|
||||
},
|
||||
/*
|
||||
problem:
|
||||
when you click your collection in the gifs picker discord enters the collection name into the search bar
|
||||
which causes discord to fetch the gifs from their api. This causes a tiny flash when the gifs have fetched successfully
|
||||
solution:
|
||||
if query starts with gc: and collection is not null then return early and prevent the fetch
|
||||
*/
|
||||
{
|
||||
find: "type:\"GIF_PICKER_QUERY\"",
|
||||
replacement: {
|
||||
match: /(function \i\((?<query>\i),\i\){.{1,200}dispatch\({type:"GIF_PICKER_QUERY".{1,20};)/,
|
||||
replace:
|
||||
"$&if($self.shouldStopFetch($<query>)) return;"
|
||||
}
|
||||
},
|
||||
],
|
||||
|
||||
settings,
|
||||
|
||||
|
||||
start() {
|
||||
CollectionManager.refreshCacheCollection();
|
||||
|
||||
addContextMenuPatch("message", addCollectionContextMenuPatch);
|
||||
},
|
||||
|
||||
stop() {
|
||||
removeContextMenuPatch("message", addCollectionContextMenuPatch);
|
||||
},
|
||||
CollectionManager,
|
||||
|
||||
oldTrendingCat: null as Category[] | null,
|
||||
sillyInstance: null as any,
|
||||
sillyContentInstance: null as any,
|
||||
|
||||
get collections(): Collection[] {
|
||||
CollectionManager.refreshCacheCollection();
|
||||
return CollectionManager.cache_collections;
|
||||
},
|
||||
|
||||
renderContent(instance) {
|
||||
if (instance.props.query.startsWith(GIF_COLLECTION_PREFIX)) {
|
||||
this.sillyContentInstance = instance;
|
||||
const collection = this.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();
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
hidePrefix(props: Category) {
|
||||
return props.name.split(":").length > 1 ? props.name.replace(/.+?:/, "") : props.name;
|
||||
},
|
||||
|
||||
insertCollections(instance: { props: Props; }) {
|
||||
try {
|
||||
this.sillyInstance = instance;
|
||||
if (instance.props.trendingCategories.length && instance.props.trendingCategories[0].type === "Trending")
|
||||
this.oldTrendingCat = instance.props.trendingCategories;
|
||||
|
||||
|
||||
if (this.oldTrendingCat != null)
|
||||
instance.props.trendingCategories = this.collections.reverse().concat(this.oldTrendingCat as Collection[]);
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
|
||||
shouldStopFetch(query: string) {
|
||||
if (query.startsWith(GIF_COLLECTION_PREFIX)) {
|
||||
const collection = this.collections.find(c => c.name === query);
|
||||
if (collection != null) return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
collectionContextMenu(e: React.UIEvent, instance) {
|
||||
const { item } = instance.props;
|
||||
if (item?.name?.startsWith(GIF_COLLECTION_PREFIX))
|
||||
return ContextMenuApi.openContextMenu(e, () =>
|
||||
<RemoveItemContextMenu
|
||||
type="collection"
|
||||
onConfirm={() => { this.sillyInstance && this.sillyInstance.forceUpdate(); }}
|
||||
nameOrId={instance.props.item.name} />
|
||||
);
|
||||
if (item?.id?.startsWith(GIF_ITEM_PREFIX))
|
||||
return ContextMenuApi.openContextMenu(e, () =>
|
||||
<RemoveItemContextMenu
|
||||
type="gif"
|
||||
onConfirm={() => { this.sillyContentInstance && this.sillyContentInstance.forceUpdate(); }}
|
||||
nameOrId={instance.props.item.id}
|
||||
/>);
|
||||
|
||||
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"
|
||||
>
|
||||
|
||||
{/* if i do it the normal way i get a invalid context menu thingy error -> Menu API only allows Items and groups of Items as children.*/}
|
||||
{MenuThingy({ gif: { ...item, id: uuidv4() } })}
|
||||
|
||||
|
||||
</Menu.Menu>
|
||||
);
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
// stolen from spotify controls
|
||||
const RemoveItemContextMenu = ({ type, nameOrId, onConfirm }: { type: "gif" | "collection", nameOrId: string, onConfirm: () => void; }) => (
|
||||
<Menu.Menu
|
||||
navId="gif-collection-id"
|
||||
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
|
||||
aria-label={type === "collection" ? "Delete Collection" : "Remove"}
|
||||
>
|
||||
<Menu.MenuItem
|
||||
key="delete-collection"
|
||||
id="delete-collection"
|
||||
label={type === "collection" ? "Delete Collection" : "Remove"}
|
||||
action={() =>
|
||||
// Stolen from Review components
|
||||
type === "collection" ? Alerts.show({
|
||||
title: "Are you sure?",
|
||||
body: "Do you really want to delete this collection?",
|
||||
confirmText: "Delete",
|
||||
confirmColor: Button.Colors.RED,
|
||||
cancelText: "Nevermind",
|
||||
onConfirm: async () => {
|
||||
await CollectionManager.deleteCollection(nameOrId);
|
||||
onConfirm();
|
||||
}
|
||||
}) : CollectionManager.removeFromCollection(nameOrId).then(() => onConfirm())}
|
||||
>
|
||||
|
||||
</Menu.MenuItem>
|
||||
</Menu.Menu>
|
||||
);
|
||||
|
||||
|
||||
|
||||
const MenuThingy: React.FC<{ gif: Gif; }> = ({ gif }) => {
|
||||
CollectionManager.refreshCacheCollection();
|
||||
const collections = CollectionManager.cache_collections;
|
||||
|
||||
return (
|
||||
<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={() => CollectionManager.addToCollection(col.name, gif)}
|
||||
/>
|
||||
))}
|
||||
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
interface CreateCollectionModalProps {
|
||||
gif: Gif;
|
||||
onClose: () => void,
|
||||
modalProps: ModalProps;
|
||||
}
|
||||
|
||||
function CreateCollectionModal({ gif, onClose, modalProps }: CreateCollectionModalProps) {
|
||||
|
||||
const [name, setName] = useState("");
|
||||
|
||||
const onSubmit = useCallback((e: React.FormEvent<HTMLFormElement> | React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
if (!name.length) return;
|
||||
CollectionManager.createCollection(name, [gif]);
|
||||
onClose();
|
||||
}, [name]);
|
||||
|
||||
return (
|
||||
<ModalRoot {...modalProps}>
|
||||
<form onSubmit={onSubmit}>
|
||||
<ModalHeader>
|
||||
<Forms.FormText>Create Collection</Forms.FormText>
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Collection Name</Forms.FormTitle>
|
||||
<TextInput onChange={(e: string) => 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>
|
||||
);
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export enum Format { NONE = 0, IMAGE = 1, VIDEO = 2 }
|
||||
|
||||
export interface Category {
|
||||
type: "Trending" | "Category";
|
||||
name: string;
|
||||
src: string;
|
||||
format: Format;
|
||||
gifs?: Gif[];
|
||||
}
|
||||
|
||||
export interface Gif {
|
||||
id: string,
|
||||
src: string;
|
||||
url: string;
|
||||
height: number,
|
||||
width: number;
|
||||
}
|
||||
|
||||
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">;
|
|
@ -1,23 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export const cleanUrl = (url: string) => {
|
||||
const urlObject = new URL(url);
|
||||
urlObject.search = "";
|
||||
return urlObject.href;
|
||||
};
|
|
@ -1,27 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Format } from "../types";
|
||||
import { getUrlExtension } from "./getUrlExtension";
|
||||
|
||||
const videoExtensions = ["mp4", "ogg", "webm", "avi", "wmv", "flv", "mov", "mkv", "m4v"];
|
||||
|
||||
export function getFormat(url: string) {
|
||||
const extension = getUrlExtension(url);
|
||||
return url.startsWith("https://media.tenor") || extension == null || videoExtensions.includes(extension) ? Format.VIDEO : Format.IMAGE;
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { MessageStore, SnowflakeUtils } from "@webpack/common";
|
||||
import { Message } from "discord-types/general";
|
||||
|
||||
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 {
|
||||
const liElement = target?.closest("li");
|
||||
if (!target || !liElement || !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;
|
||||
|
||||
const message = MessageStore.getMessage(channelId, messageId);
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
url = cleanUrl(url);
|
||||
|
||||
// find embed with matching url or image/thumbnail url
|
||||
const embed = message.embeds.find(e => e.url === url || e.image?.url === url || e.image?.proxyURL === url || e.video?.proxyURL === url || e.thumbnail?.proxyURL === url); // no point in checking thumbnail/video url because no way of getting it eh. discord renders the img/video element with proxy urls
|
||||
if (embed) {
|
||||
if (embed.image)
|
||||
return {
|
||||
id: uuidv4(),
|
||||
height: embed.image.height,
|
||||
width: embed.image.width,
|
||||
src: embed.image.proxyURL,
|
||||
url: embed.image.url,
|
||||
};
|
||||
// Tennor
|
||||
if (embed.video && embed.video.proxyURL) return {
|
||||
id: uuidv4(),
|
||||
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(),
|
||||
height: embed.thumbnail.height,
|
||||
width: embed.thumbnail.width,
|
||||
src: embed.thumbnail.proxyURL,
|
||||
url: embed.thumbnail.url,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
const attachment = message.attachments.find(a => a.url === url || a.proxy_url === url);
|
||||
if (attachment) return {
|
||||
id: uuidv4(),
|
||||
height: attachment.height ?? 50,
|
||||
width: attachment.width ?? 50,
|
||||
src: attachment.proxy_url,
|
||||
url: attachment.url
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
function isValidSnowFlake(snowflake: string) {
|
||||
return !Number.isNaN(SnowflakeUtils.extractTimestamp(snowflake));
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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();
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { getUrlExtension } from "./getUrlExtension";
|
||||
|
||||
const audioExtensions = ["mp3", "wav", "ogg", "aac", "m4a", "wma", "flac"];
|
||||
|
||||
|
||||
export function isAudio(url: string) {
|
||||
const extension = getUrlExtension(url);
|
||||
return extension && audioExtensions.includes(extension);
|
||||
}
|
|
@ -1,131 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { DataStore } from "@api/index";
|
||||
import { Toasts } from "@webpack/common";
|
||||
|
||||
import { DATA_COLLECTION_NAME, getCollections, refreshCacheCollection } from "../CollectionManager";
|
||||
|
||||
// 99% of this is coppied from src\utils\settingsSync.ts
|
||||
|
||||
export async function downloadCollections() {
|
||||
const filename = "gif-collections.json";
|
||||
const exportData = await exportCollections();
|
||||
const data = new TextEncoder().encode(exportData);
|
||||
|
||||
if (IS_WEB) {
|
||||
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);
|
||||
});
|
||||
} else {
|
||||
DiscordNative.fileManager.saveWithDialog(data, filename);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export async function exportCollections() {
|
||||
const collections = await getCollections();
|
||||
return JSON.stringify({ collections }, null, 4);
|
||||
}
|
||||
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
if ("collections" in parsed) {
|
||||
await DataStore.set(DATA_COLLECTION_NAME, parsed.collections);
|
||||
await refreshCacheCollection();
|
||||
} else
|
||||
throw new Error("Invalid Collections");
|
||||
}
|
||||
|
||||
|
||||
export async function uploadGifCollections(showToast = true): Promise<void> {
|
||||
if (IS_WEB) {
|
||||
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 {
|
||||
const [file] = await DiscordNative.fileManager.openFiles({
|
||||
filters: [
|
||||
{ name: "Gif Collections", extensions: ["json"] },
|
||||
{ name: "all", extensions: ["*"] }
|
||||
]
|
||||
});
|
||||
|
||||
if (file) {
|
||||
try {
|
||||
await importCollections(new TextDecoder().decode(file.data));
|
||||
if (showToast) toastSuccess();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
// new Logger("SettingsSync").error(err);
|
||||
if (showToast) toastFailure(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const toastSuccess = () => Toasts.show({
|
||||
type: Toasts.Type.SUCCESS,
|
||||
message: "Settings successfully imported.",
|
||||
id: Toasts.genId()
|
||||
});
|
||||
|
||||
const toastFailure = (err: any) => Toasts.show({
|
||||
type: Toasts.Type.FAILURE,
|
||||
message: `Failed to import settings: ${String(err)}`,
|
||||
id: Toasts.genId()
|
||||
});
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { GIF_ITEM_PREFIX } from "../constants";
|
||||
|
||||
export function uuidv4() {
|
||||
let d = new Date().getTime();
|
||||
d += performance.now();
|
||||
return `${GIF_ITEM_PREFIX}xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`.replace(/[xy]/g, c => {
|
||||
const r = (d + Math.random() * 16) % 16 | 0;
|
||||
d = Math.floor(d / 16);
|
||||
return (c === "x" ? r : (r & 0x3 | 0x8)).toString(16);
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue