Merge branch 'prtesting' into dev

This commit is contained in:
thororen 2024-10-19 01:43:14 -04:00
commit 69237bfe11
15 changed files with 1165 additions and 184 deletions

View file

@ -0,0 +1,88 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { React } from "@webpack/common";
import { CategoryImageProps, StickerCategoryProps } from "../types";
import { cl } from "../utils";
export function CategoryImage({ src, alt, isActive }: CategoryImageProps) {
return (
<div>
<svg width={32} height={32} style={{
display: "block",
contain: "paint",
overflow: "hidden",
overflowClipMargin: "content-box",
}}>
<foreignObject
className={
cl("foreign-object") + (
isActive ?
` ${cl("foreign-object-active")}`
: ""
)
}
x={0} y={0}
width={32}
height={32}
overflow="visible"
>
<img
src={src}
alt={alt}
width={32}
height={32}
/>
</foreignObject>
</svg>
</div>
);
}
export function CategoryScroller(props: { children: React.ReactNode, categoryLength: number; }) {
const children = Array.isArray(props.children) ? props.children : [props.children];
return (
<div className={cl("category-scroller")}>
<div>{
children.map(child => (
<div role="listitem">
{child}
</div>
))
}</div>
<div style={{ height: `${Math.round(41.75 * (props.categoryLength + 1))}px` }}></div>
<div aria-hidden="true"></div>
</div>
);
}
export function CategoryWrapper(props: { children: JSX.Element | JSX.Element[]; }) {
return (
<div className={cl("category-wrapper")}>
{props.children}
</div>
);
}
export function StickerCategory(props: StickerCategoryProps) {
return (
<div
style={props.style}
className={
cl("sticker-category") +
(props.isActive ? ` ${cl("sticker-category-active")}` : "")
}
tabIndex={0}
role="button"
onClick={props.onClick}
>
{props.children}
</div>
);
}

View file

@ -1,28 +1,31 @@
/* /*
* Vencord, a modification for Discord's desktop app * Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors * Copyright (c) 2024 Vendicated and contributors
* * SPDX-License-Identifier: GPL-3.0-or-later
* 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 IconContainer(props: { children: JSX.Element | JSX.Element[]; }) {
return (
<div style={{
width: "20px",
height: "20px",
boxSizing: "border-box",
position: "relative",
cursor: "text"
}}>
{props.children}
</div>
);
}
export function SearchIcon({ width, height, color }: { width: number, height: number, color: string; }) { export function SearchIcon({ width, height, color }: { width: number, height: number, color: string; }) {
return ( return (
<svg role="img" width={width} height={height} viewBox="0 0 24 24"> <svg role="img" width={width} height={height} viewBox="0 0 24 24">
<path fill={color} d="M21.707 20.293L16.314 14.9C17.403 13.504 18 11.799 18 10C18 7.863 17.167 5.854 15.656 4.344C14.146 2.832 12.137 2 10 2C7.863 2 5.854 2.832 4.344 4.344C2.833 5.854 2 7.863 2 10C2 12.137 2.833 14.146 4.344 15.656C5.854 17.168 7.863 18 10 18C11.799 18 13.504 17.404 14.9 16.314L20.293 21.706L21.707 20.293ZM10 16C8.397 16 6.891 15.376 5.758 14.243C4.624 13.11 4 11.603 4 10C4 8.398 4.624 6.891 5.758 5.758C6.891 4.624 8.397 4 10 4C11.603 4 13.109 4.624 14.242 5.758C15.376 6.891 16 8.398 16 10C16 11.603 15.376 13.11 14.242 14.243C13.109 15.376 11.603 16 10 16Z"></path> <path fill={color} d="M21.707 20.293L16.314 14.9C17.403 13.504 18 11.799 18 10C18 7.863 17.167 5.854 15.656 4.344C14.146 2.832 12.137 2 10 2C7.863 2 5.854 2.832 4.344 4.344C2.833 5.854 2 7.863 2 10C2 12.137 2.833 14.146 4.344 15.656C5.854 17.168 7.863 18 10 18C11.799 18 13.504 17.404 14.9 16.314L20.293 21.706L21.707 20.293ZM10 16C8.397 16 6.891 15.376 5.758 14.243C4.624 13.11 4 11.603 4 10C4 8.398 4.624 6.891 5.758 5.758C6.891 4.624 8.397 4 10 4C11.603 4 13.109 4.624 14.242 5.758C15.376 6.891 16 8.398 16 10C16 11.603 15.376 13.11 14.242 14.243C13.109 15.376 11.603 16 10 16Z"></path>
</svg> </svg>
); );
}; }
export function CancelIcon({ width, height, className, onClick }: { width: number, height: number, className: string, onClick: () => void; }) { export function CancelIcon({ width, height, className, onClick }: { width: number, height: number, className: string, onClick: () => void; }) {
return ( return (
@ -30,7 +33,7 @@ export function CancelIcon({ width, height, className, onClick }: { width: numbe
<path fill="currentColor" d="M18.4 4L12 10.4L5.6 4L4 5.6L10.4 12L4 18.4L5.6 20L12 13.6L18.4 20L20 18.4L13.6 12L20 5.6L18.4 4Z"></path> <path fill="currentColor" d="M18.4 4L12 10.4L5.6 4L4 5.6L10.4 12L4 18.4L5.6 20L12 13.6L18.4 20L20 18.4L13.6 12L20 5.6L18.4 4Z"></path>
</svg> </svg>
); );
}; }
export function RecentlyUsedIcon({ width, height, color }: { width: number, height: number, color: string; }) { export function RecentlyUsedIcon({ width, height, color }: { width: number, height: number, color: string; }) {
return ( return (
@ -38,7 +41,7 @@ export function RecentlyUsedIcon({ width, height, color }: { width: number, heig
<path d="M12 2C6.4764 2 2 6.4764 2 12C2 17.5236 6.4764 22 12 22C17.5236 22 22 17.5236 22 12C22 6.4764 17.5236 2 12 2ZM12 5.6C12.4422 5.6 12.8 5.95781 12.8 6.4V11.5376L16.5625 13.7126C16.9453 13.9329 17.0703 14.4173 16.85 14.8001C16.6297 15.183 16.1453 15.3079 15.7625 15.0876L11.6873 12.7376C11.656 12.7251 11.6279 12.7048 11.5998 12.6876C11.3607 12.5486 11.1998 12.2954 11.1998 12.0001V6.4001C11.1998 5.9579 11.5578 5.6 12 5.6Z" fill={color}></path> <path d="M12 2C6.4764 2 2 6.4764 2 12C2 17.5236 6.4764 22 12 22C17.5236 22 22 17.5236 22 12C22 6.4764 17.5236 2 12 2ZM12 5.6C12.4422 5.6 12.8 5.95781 12.8 6.4V11.5376L16.5625 13.7126C16.9453 13.9329 17.0703 14.4173 16.85 14.8001C16.6297 15.183 16.1453 15.3079 15.7625 15.0876L11.6873 12.7376C11.656 12.7251 11.6279 12.7048 11.5998 12.6876C11.3607 12.5486 11.1998 12.2954 11.1998 12.0001V6.4001C11.1998 5.9579 11.5578 5.6 12 5.6Z" fill={color}></path>
</svg> </svg>
); );
}; }
export function CogIcon({ width, height }: { width: number, height: number; }) { export function CogIcon({ width, height }: { width: number, height: number; }) {
return ( return (
@ -46,4 +49,4 @@ export function CogIcon({ width, height }: { width: number, height: number; }) {
<path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M19.738 10H22V14H19.739C19.498 14.931 19.1 15.798 18.565 16.564L20 18L18 20L16.565 18.564C15.797 19.099 14.932 19.498 14 19.738V22H10V19.738C9.069 19.498 8.203 19.099 7.436 18.564L6 20L4 18L5.436 16.564C4.901 15.799 4.502 14.932 4.262 14H2V10H4.262C4.502 9.068 4.9 8.202 5.436 7.436L4 6L6 4L7.436 5.436C8.202 4.9 9.068 4.502 10 4.262V2H14V4.261C14.932 4.502 15.797 4.9 16.565 5.435L18 3.999L20 5.999L18.564 7.436C19.099 8.202 19.498 9.069 19.738 10ZM12 16C14.2091 16 16 14.2091 16 12C16 9.79086 14.2091 8 12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16Z"></path> <path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M19.738 10H22V14H19.739C19.498 14.931 19.1 15.798 18.565 16.564L20 18L18 20L16.565 18.564C15.797 19.099 14.932 19.498 14 19.738V22H10V19.738C9.069 19.498 8.203 19.099 7.436 18.564L6 20L4 18L5.436 16.564C4.901 15.799 4.502 14.932 4.262 14H2V10H4.262C4.502 9.068 4.9 8.202 5.436 7.436L4 6L6 4L7.436 5.436C8.202 4.9 9.068 4.502 10 4.262V2H14V4.261C14.932 4.502 15.797 4.9 16.565 5.435L18 3.999L20 5.999L18.564 7.436C19.099 8.202 19.498 9.069 19.738 10ZM12 16C14.2091 16 16 14.2091 16 12C16 9.79086 14.2091 8 12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16Z"></path>
</svg> </svg>
); );
}; }

View file

@ -0,0 +1,10 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export * from "./categories";
export * from "./icons";
export * from "./misc";
export * from "./picker";

View file

@ -0,0 +1,441 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import * as DataStore from "@api/DataStore";
import { CheckedTextInput } from "@components/CheckedTextInput";
import { Flex } from "@components/Flex";
import { Button, Forms, React, TabBar, Text, TextArea, Toasts } from "@webpack/common";
import { convert as convertLineEP, getIdFromUrl as getLineEmojiPackIdFromUrl, getStickerPackById as getLineEmojiPackById, isLineEmojiPackHtml, parseHtml as getLineEPFromHtml } from "../lineEmojis";
import { convert as convertLineSP, getIdFromUrl as getLineStickerPackIdFromUrl, getStickerPackById as getLineStickerPackById, isLineStickerPackHtml, parseHtml as getLineSPFromHtml } from "../lineStickers";
import { deleteStickerPack, getStickerPackMetas, saveStickerPack } from "../stickers";
import { SettingsTabsKey, Sticker, StickerPack, StickerPackMeta } from "../types";
import { cl, clPicker, Mutex } from "../utils";
const mutex = new Mutex();
// The ID of recent sticker and recent sticker pack
export const RECENT_STICKERS_ID = "recent";
export const RECENT_STICKERS_TITLE = "Recently Used";
const KEY = "Vencord-MoreStickers-RecentStickers";
const noDrag = {
onMouseDown: e => { e.preventDefault(); return false; },
onDragStart: e => { e.preventDefault(); return false; }
};
const StickerPackMetadata = ({ meta, hoveredStickerPackId, setHoveredStickerPackId, refreshStickerPackMetas }:
{ meta: StickerPackMeta, [key: string]: any; }
) => {
return (
<div className="sticker-pack"
onMouseEnter={() => setHoveredStickerPackId(meta.id)}
onMouseLeave={() => setHoveredStickerPackId(null)}
>
<div className={
[
clPicker("content-row-grid-inspected-indicator"),
hoveredStickerPackId === meta.id ? "inspected" : ""
].join(" ")
} style={{
top: "unset",
left: "unset",
height: "96px",
width: "96px",
}}></div>
<img src={meta.logo.image} width="96" {...noDrag} />
<button
className={hoveredStickerPackId === meta.id ? "show" : ""}
onClick={async () => {
try {
await deleteStickerPack(meta.id);
Toasts.show({
message: "Sticker Pack deleted",
type: Toasts.Type.SUCCESS,
id: Toasts.genId(),
options: {
duration: 1000
}
});
await refreshStickerPackMetas();
} catch (e: any) {
Toasts.show({
message: e.message,
type: Toasts.Type.FAILURE,
id: Toasts.genId(),
options: {
duration: 1000
}
});
}
}}
>
<svg width="24" height="24" viewBox="0 0 24 24" style={{ fill: "var(--status-danger)" }}>
<title>Delete</title>
<path d="M15 3.999V2H9V3.999H3V5.999H21V3.999H15Z" />
<path d="M5 6.99902V18.999C5 20.101 5.897 20.999 7 20.999H17C18.103 20.999 19 20.101 19 18.999V6.99902H5ZM11 17H9V11H11V17ZM15 17H13V11H15V17Z" />
</svg>
</button>
<Text className={cl("pack-title")} tag="span">{meta.title}</Text>
</div>
);
};
export const Settings = () => {
const [stickerPackMetas, setstickerPackMetas] = React.useState<StickerPackMeta[]>([]);
const [addStickerUrl, setAddStickerUrl] = React.useState<string>("");
const [addStickerHtml, setAddStickerHtml] = React.useState<string>("");
const [tab, setTab] = React.useState<SettingsTabsKey>(SettingsTabsKey.ADD_STICKER_PACK_URL);
const [hoveredStickerPackId, setHoveredStickerPackId] = React.useState<string | null>(null);
async function refreshStickerPackMetas() {
setstickerPackMetas(await getStickerPackMetas());
}
React.useEffect(() => {
refreshStickerPackMetas();
}, []);
return (
<div className={cl("settings")}>
<TabBar
type="top"
look="brand"
selectedItem={tab}
onItemSelect={setTab}
className="tab-bar"
>
{
Object.values(SettingsTabsKey).map(k => (
<TabBar.Item key={k} id={k} className="tab-bar-item">
{k}
</TabBar.Item>
))
}
</TabBar>
{tab === SettingsTabsKey.ADD_STICKER_PACK_URL &&
<div className="section">
<Forms.FormTitle tag="h5">Add Sticker Pack from URL</Forms.FormTitle>
<Forms.FormText>
<p>
Currently LINE stickers supported only. <br />
Telegram stickers support is planned, but due to the lack of a public API, it is most likely to be provided by sticker pack files instead of adding by URL.
</p>
</Forms.FormText>
<Flex flexDirection="row" style={{
alignItems: "center",
justifyContent: "center"
}} >
<span style={{
flexGrow: 1
}}>
<CheckedTextInput
value={addStickerUrl}
onChange={setAddStickerUrl}
validate={(v: string) => {
try {
getLineStickerPackIdFromUrl(v);
return true;
} catch (e: any) { }
try {
getLineEmojiPackIdFromUrl(v);
return true;
} catch (e: any) { }
return "Invalid URL";
}}
placeholder="Sticker Pack URL"
/>
</span>
<Button
size={Button.Sizes.SMALL}
onClick={async e => {
e.preventDefault();
let type: string = "";
try {
getLineStickerPackIdFromUrl(addStickerUrl);
type = "LineStickerPack";
} catch (e: any) { }
try {
getLineEmojiPackIdFromUrl(addStickerUrl);
type = "LineEmojiPack";
} catch (e: any) { }
let errorMessage = "";
switch (type) {
case "LineStickerPack": {
try {
const id = getLineStickerPackIdFromUrl(addStickerUrl);
const lineSP = await getLineStickerPackById(id);
const stickerPack = convertLineSP(lineSP);
await saveStickerPack(stickerPack);
} catch (e: any) {
console.error(e);
errorMessage = e.message;
}
break;
}
case "LineEmojiPack": {
try {
const id = getLineEmojiPackIdFromUrl(addStickerUrl);
const lineEP = await getLineEmojiPackById(id);
const stickerPack = convertLineEP(lineEP);
await saveStickerPack(stickerPack);
} catch (e: any) {
console.error(e);
errorMessage = e.message;
}
break;
}
}
setAddStickerUrl("");
refreshStickerPackMetas();
if (errorMessage) {
Toasts.show({
message: errorMessage,
type: Toasts.Type.FAILURE,
id: Toasts.genId(),
options: {
duration: 1000
}
});
} else {
Toasts.show({
message: "Sticker Pack added",
type: Toasts.Type.SUCCESS,
id: Toasts.genId(),
options: {
duration: 1000
}
});
}
}}
>Insert</Button>
</Flex>
</div>
}
{tab === SettingsTabsKey.ADD_STICKER_PACK_HTML &&
<div className="section">
<Forms.FormTitle tag="h5">Add Sticker Pack from HTML</Forms.FormTitle>
<Forms.FormText>
<p>
When encountering errors while adding a sticker pack, you can try to add it using the HTML source code of the sticker pack page.<br />
This applies to stickers which are region locked / OS locked / etc.<br />
The region LINE recognized may vary from the region you are in due to the CORS proxy we're using.
</p>
</Forms.FormText>
<Flex flexDirection="row" style={{
alignItems: "center",
justifyContent: "center"
}} >
<span style={{
flexGrow: 1
}}>
<TextArea
value={addStickerHtml}
onChange={setAddStickerHtml}
placeholder="Paste HTML here"
rows={1}
/>
</span>
<Button
size={Button.Sizes.SMALL}
onClick={async e => {
e.preventDefault();
let errorMessage = "";
if (isLineEmojiPackHtml(addStickerHtml)) {
try {
const lineSP = getLineSPFromHtml(addStickerHtml);
const stickerPack = convertLineSP(lineSP);
await saveStickerPack(stickerPack);
} catch (e: any) {
console.error(e);
errorMessage = e.message;
}
} else if (isLineStickerPackHtml(addStickerHtml)) {
try {
const lineEP = getLineEPFromHtml(addStickerHtml);
const stickerPack = convertLineEP(lineEP);
await saveStickerPack(stickerPack);
} catch (e: any) {
console.error(e);
errorMessage = e.message;
}
}
setAddStickerHtml("");
refreshStickerPackMetas();
if (errorMessage) {
Toasts.show({
message: errorMessage,
type: Toasts.Type.FAILURE,
id: Toasts.genId(),
options: {
duration: 1000
}
});
} else {
Toasts.show({
message: "Sticker Pack added",
type: Toasts.Type.SUCCESS,
id: Toasts.genId(),
options: {
duration: 1000
}
});
}
}}
>Insert from HTML</Button>
</Flex>
</div>
}
{
tab === SettingsTabsKey.ADD_STICKER_PACK_FILE &&
<div className="section">
<Forms.FormTitle tag="h5">Add Sticker Pack from File</Forms.FormTitle>
<Button
size={Button.Sizes.SMALL}
onClick={async e => {
const input = document.createElement("input");
input.type = "file";
input.accept = ".stickerpack,.stickerpacks,.json";
input.onchange = async e => {
try {
const file = input.files?.[0];
if (!file) return;
const fileText = await file.text();
const fileJson = JSON.parse(fileText);
let stickerPacks: StickerPack[] = [];
if (Array.isArray(fileJson)) {
stickerPacks = fileJson;
} else {
stickerPacks = [fileJson];
}
for (const stickerPack of stickerPacks) {
await saveStickerPack(stickerPack);
}
Toasts.show({
message: "Sticker Packs added",
type: Toasts.Type.SUCCESS,
id: Toasts.genId(),
options: {
duration: 1000
}
});
} catch (e: any) {
console.error(e);
Toasts.show({
message: e.message,
type: Toasts.Type.FAILURE,
id: Toasts.genId(),
options: {
duration: 1000
}
});
}
};
input.click();
}}
>
Open Sticker Pack File
</Button>
</div>
}
<Forms.FormDivider style={{
marginTop: "8px",
marginBottom: "8px"
}} />
<Forms.FormTitle tag="h5">Stickers Management</Forms.FormTitle>
<div className="section">
<div style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(96px, 1fr))",
gap: "8px"
}}>
{
stickerPackMetas.map(meta => (
<StickerPackMetadata
key={meta.id}
meta={meta}
hoveredStickerPackId={hoveredStickerPackId}
setHoveredStickerPackId={setHoveredStickerPackId}
refreshStickerPackMetas={refreshStickerPackMetas}
/>
))
}
</div>
</div>
</div>
);
};
export function Header(props: { children: JSX.Element | JSX.Element[]; }) {
return (
<div className={cl("header")}>
{props.children}
</div>
);
}
export function Wrapper(props: { children: JSX.Element | JSX.Element[]; }) {
return (
<div style={{
position: "relative",
display: "grid",
gridTemplateColumns: "48px auto",
gridTemplateRows: "auto 1fr auto",
}}>
{props.children}
</div>
);
}
export async function getRecentStickers(): Promise<Sticker[]> {
return (await DataStore.get(KEY)) ?? [];
}
export async function setRecentStickers(stickers: Sticker[]): Promise<void> {
const unlock = await mutex.lock();
try {
await DataStore.set(KEY, stickers);
} finally {
unlock();
}
}
export async function addRecentSticker(sticker: Sticker): Promise<void> {
const stickers = await getRecentStickers();
const index = stickers.findIndex(s => s.id === sticker.id);
if (index !== -1) {
stickers.splice(index, 1);
}
stickers.unshift(sticker);
while (stickers.length > 16) {
stickers.pop();
}
await setRecentStickers(stickers);
}
export async function removeRecentStickerByPackId(packId: string): Promise<void> {
const stickers = await getRecentStickers();
await setRecentStickers(stickers.filter(s => s.stickerPackId !== packId));
}

View file

@ -0,0 +1,473 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { debounce } from "@shared/debounce";
import { ModalContent, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { React, Text, TextInput } from "@webpack/common";
import { PickerContent, PickerContentHeader, PickerContentRow, PickerContentRowGrid, PickerHeaderProps, SidebarProps, Sticker, StickerCategoryType, StickerPack } from "../types";
import { sendSticker } from "../upload";
import { clPicker, FFmpegStateContext } from "../utils";
import { CategoryImage, CategoryScroller, CategoryWrapper, StickerCategory } from "./categories";
import { CancelIcon, CogIcon, IconContainer, RecentlyUsedIcon, SearchIcon } from "./icons";
import { addRecentSticker, getRecentStickers, Header, RECENT_STICKERS_ID, RECENT_STICKERS_TITLE, Settings } from "./misc";
const debounceQueryChange = debounce((cb: Function, ...args: any) => cb(...args), 150);
export const RecentPack = {
id: RECENT_STICKERS_ID,
name: RECENT_STICKERS_TITLE,
} as StickerCategoryType;
export const PickerSidebar = ({ packMetas, onPackSelect }: SidebarProps) => {
const [activePack, setActivePack] = React.useState<StickerCategoryType>(RecentPack);
const [hovering, setHovering] = React.useState(false);
return (
<CategoryWrapper>
<CategoryScroller categoryLength={packMetas.length}>
<StickerCategory
style={{ padding: "4px", boxSizing: "border-box", width: "32px" }}
isActive={activePack === RecentPack}
onClick={() => {
if (activePack === RecentPack) return;
onPackSelect(RecentPack);
setActivePack(RecentPack);
}}
>
<RecentlyUsedIcon width={24} height={24} color={
activePack === RecentPack ? " var(--interactive-active)" : "var(--interactive-normal)"
} />
</StickerCategory>
{
...packMetas.map(pack => {
return (
<StickerCategory
key={pack.id}
onClick={() => {
if (activePack?.id === pack.id) return;
onPackSelect(pack);
setActivePack(pack);
}}
isActive={activePack?.id === pack.id}
>
<CategoryImage src={pack.iconUrl!} alt={pack.name} isActive={activePack?.id === pack.id} />
</StickerCategory>
);
})
}
</CategoryScroller>
<div className={clPicker("settings-cog-container")}>
<button
className={clPicker("settings-cog") + (
hovering ? ` ${clPicker("settings-cog-active")}` : ""
)}
onClick={() => {
openModal(modalProps => {
return (
<ModalRoot size={ModalSize.LARGE} {...modalProps}>
<ModalHeader>
<Text tag="h2">Stickers+</Text>
</ModalHeader>
<ModalContent>
<Settings />
</ModalContent>
</ModalRoot>
);
});
}}
onMouseEnter={() => setHovering(true)}
onMouseLeave={() => setHovering(false)}
>
<CogIcon width={20} height={20} />
</button>
</div>
</CategoryWrapper>
);
};
function PickerContentRowGrid({
rowIndex,
colIndex,
sticker,
onHover,
channelId,
onSend = () => { },
isHovered = false
}: PickerContentRowGrid) {
if (FFmpegStateContext === undefined) {
return <div>FFmpegStateContext is undefined</div>;
}
const ffmpegState = React.useContext(FFmpegStateContext);
return (
<div
role="gridcell"
aria-rowindex={rowIndex}
aria-colindex={colIndex}
id={clPicker(`content-row-grid-${rowIndex}-${colIndex}`)}
onMouseEnter={() => onHover(sticker)}
onClick={e => {
if (!channelId) return;
sendSticker({ channelId, sticker, ctrlKey: e.ctrlKey, shiftKey: e.shiftKey, ffmpegState });
addRecentSticker(sticker);
onSend(sticker, e.ctrlKey);
}}
>
<div
className={clPicker("content-row-grid-sticker")}
>
<span className={clPicker("content-row-grid-hidden-visually")}>{sticker.title}</span>
<div aria-hidden="true">
<div className={
[
clPicker("content-row-grid-inspected-indicator"),
`${isHovered ? "inspected" : ""}`
].join(" ")
}></div>
<div className={clPicker("content-row-grid-sticker-node")}>
<div className={clPicker("content-row-grid-asset-wrapper")} style={{
height: "96px",
width: "96px"
}}>
<img
alt={sticker.title}
src={sticker.image}
draggable="false"
datatype="sticker"
data-id={sticker.id}
className={clPicker("content-row-grid-img")}
loading="lazy"
/>
</div>
</div>
</div>
</div>
</div>
);
}
function PickerContentRow({ rowIndex, grid1, grid2, grid3, channelId }: PickerContentRow) {
return (
<div className={clPicker("content-row")}
role="row"
aria-rowindex={rowIndex}
>
<PickerContentRowGrid {...grid1} rowIndex={rowIndex} colIndex={1} channelId={channelId} />
{grid2 && <PickerContentRowGrid {...grid2} rowIndex={rowIndex} colIndex={2} channelId={channelId} />}
{grid3 && <PickerContentRowGrid {...grid3} rowIndex={rowIndex} colIndex={3} channelId={channelId} />}
</div>
);
}
function HeaderCollapseIcon({ isExpanded }: { isExpanded: boolean; }) {
return (
<svg
className={clPicker("content-header-collapse-icon")}
width={16} height={16} viewBox="0 0 24 24"
style={{
transform: `rotate(${isExpanded ? "0" : "-90deg"})`
}}
>
<path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M16.59 8.59004L12 13.17L7.41 8.59004L6 10L12 16L18 10L16.59 8.59004Z"></path>
</svg>
);
}
export function PickerContentHeader({
image,
title,
children,
isSelected = false,
afterScroll = () => { },
beforeScroll = () => { }
}: PickerContentHeader) {
const [isExpand, setIsExpand] = React.useState(true);
const headerElem = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (isSelected && headerElem.current) {
beforeScroll();
headerElem.current.scrollIntoView({
behavior: "smooth",
block: "start",
});
afterScroll();
}
}, [isSelected]);
return (
<span>
<div className={clPicker("content-header-wrapper")}>
<div className={clPicker("content-header-header")} ref={headerElem}
aria-expanded={isExpand}
aria-label={`Category, ${title}`}
role="button"
tabIndex={0}
onClick={() => {
setIsExpand(e => !e);
}}
>
<div className={clPicker("content-header-header-icon")}>
<div>
{typeof image === "string" ? <svg
className={clPicker("content-header-svg")}
width={16} height={16} viewBox="0 0 16 16"
>
<foreignObject
x={0} y={0} width={16} height={16}
overflow="visible" mask="url(#svg-mask-squircle)"
>
<img
alt={title}
src={image}
className={clPicker("content-header-guild-icon")}
loading="lazy"
></img>
</foreignObject>
</svg>
: image}
</div>
</div>
<span
className={clPicker("content-header-header-label")}
>
{title}
</span>
<HeaderCollapseIcon isExpanded={isExpand} />
</div>
</div>
{isExpand ? children : null}
</span>
);
}
export function PickerContent({ stickerPacks, selectedStickerPackId, setSelectedStickerPackId, channelId, closePopout, query }: PickerContent) {
const [currentSticker, setCurrentSticker] = (
React.useState<Sticker | null>((
stickerPacks.length && stickerPacks[0].stickers.length) ?
stickerPacks[0].stickers[0] :
null
)
);
const [currentStickerPack, setCurrentStickerPack] = React.useState<StickerPack | null>(stickerPacks.length ? stickerPacks[0] : null);
const [recentStickers, setRecentStickers] = React.useState<Sticker[]>([]);
const stickerPacksElemRef = React.useRef<HTMLDivElement>(null);
const scrollerRef = React.useRef<HTMLDivElement>(null);
function queryFilter(stickers: Sticker[]): Sticker[] {
if (!query) return stickers;
return stickers.filter(sticker => sticker.title.toLowerCase().includes(query.toLowerCase()));
}
async function fetchRecentStickers() {
const recentStickers = await getRecentStickers();
setRecentStickers(recentStickers);
}
React.useEffect(() => {
fetchRecentStickers();
}, []);
React.useEffect(() => {
if (currentStickerPack?.id !== currentSticker?.stickerPackId) {
setCurrentStickerPack(stickerPacks.find(p => p.id === currentSticker?.stickerPackId) ?? currentStickerPack);
}
}, [currentSticker]);
const stickersToRows = (stickers: Sticker[]): JSX.Element[] => stickers
.reduce((acc, sticker, i) => {
if (i % 3 === 0) {
acc.push([]);
}
acc[acc.length - 1].push(sticker);
return acc;
}, [] as Sticker[][])
.map((stickers, i) => (
<PickerContentRow
rowIndex={i}
channelId={channelId}
grid1={{
rowIndex: i,
colIndex: 1,
sticker: stickers[0],
onHover: setCurrentSticker,
onSend: (_, s) => { !s && closePopout(); },
isHovered: currentSticker?.id === stickers[0].id
}}
grid2={
stickers.length > 1 ? {
rowIndex: i,
colIndex: 2,
sticker: stickers[1],
onHover: setCurrentSticker,
onSend: (_, s) => { !s && closePopout(); },
isHovered: currentSticker?.id === stickers[1].id
} : undefined
}
grid3={
stickers.length > 2 ? {
rowIndex: i,
colIndex: 3,
sticker: stickers[2],
onHover: setCurrentSticker,
onSend: (_, s) => { !s && closePopout(); },
isHovered: currentSticker?.id === stickers[2].id
} : undefined
}
/>
));
return (
<div className={clPicker("content-list-wrapper")}>
<div className={clPicker("content-wrapper")}>
<div className={clPicker("content-scroller")} ref={scrollerRef}>
<div className={clPicker("content-list-items")} role="none presentation">
<div ref={stickerPacksElemRef}>
<PickerContentHeader
image={
<RecentlyUsedIcon width={16} height={16} color="currentColor" />
}
title={RECENT_STICKERS_TITLE}
isSelected={RECENT_STICKERS_ID === selectedStickerPackId}
beforeScroll={() => {
scrollerRef.current?.scrollTo({
top: 0,
});
}}
afterScroll={() => { setSelectedStickerPackId(null); }}
>
{
...stickersToRows(
queryFilter(recentStickers)
)
}
</PickerContentHeader>
{
stickerPacks.map(sp => {
const rows = stickersToRows(queryFilter(sp.stickers));
return (
<PickerContentHeader
image={sp.logo.image}
title={sp.title}
isSelected={sp.id === selectedStickerPackId}
beforeScroll={() => {
scrollerRef.current?.scrollTo({
top: 0,
});
}}
afterScroll={() => { setSelectedStickerPackId(null); }}
>
{...rows}
</PickerContentHeader>
);
})
}
</div>
</div>
<div style={{
height: `${stickerPacksElemRef.current?.clientHeight ?? 0}px`
}}></div>
</div>
<div
className={clPicker("content-inspector")}
style={{
visibility: !currentSticker ? "hidden" : "visible",
...(!currentSticker ? {
height: "0"
} : {})
}}
>
<div className={clPicker("content-inspector-graphic-primary")} aria-hidden="true">
<div>
<div className={clPicker("content-row-grid-asset-wrapper")} style={{
height: "28px",
width: "28px"
}}>
<img
alt={currentSticker?.title ?? ""}
src={currentSticker?.image}
draggable="false"
datatype="sticker"
data-id={currentSticker?.id ?? ""}
className={clPicker("content-inspector-img")}
/>
</div>
</div>
</div>
<div className={clPicker("content-inspector-text-wrapper")}>
<div className={clPicker("content-inspector-title-primary")} data-text-variant="text-md/semibold">{currentSticker?.title ?? ""}</div>
<div className={clPicker("content-inspector-title-secondary")} data-text-variant="text-md/semibold">
{currentStickerPack?.title ? "from " : ""}
<strong>{currentStickerPack?.title ?? ""}</strong>
</div>
</div>
<div className={clPicker("content-inspector-graphic-secondary")} aria-hidden="true">
<div>
<svg width={32} height={32} viewBox="0 0 32 32">
<foreignObject x={0} y={0} width={32} height={32} overflow="visible" mask="url(#svg-mask-squircle)">
<img
alt={currentStickerPack?.title ?? ""}
src={currentStickerPack?.logo?.image}
></img>
</foreignObject>
</svg>
</div>
</div>
</div>
</div>
</div>
);
}
export const PickerHeader = ({ onQueryChange }: PickerHeaderProps) => {
const [query, setQuery] = React.useState<string | undefined>();
const setQueryDebounced = (value: string, immediate = false) => {
setQuery(value);
if (immediate) onQueryChange(value);
else debounceQueryChange(onQueryChange, value);
};
return (
<Header>
<div className={clPicker("container")}>
<div>
<div className={clPicker("search-box")}>
<TextInput
style={{ height: "30px" }}
placeholder="Search stickers"
autoFocus={true}
value={query}
onChange={(value: string) => setQueryDebounced(value)}
/>
</div>
<div className={clPicker("search-icon")}>
<IconContainer>
{
(query && query.length > 0) ?
<CancelIcon className={clPicker("clear-icon")} width={20} height={20} onClick={() => setQueryDebounced("", true)} /> :
<SearchIcon width={20} height={20} color="var(--text-muted)" />
}
</IconContainer>
</div>
</div>
</div>
</Header>
);
};

View file

@ -1,37 +1,20 @@
/* /*
* Vencord, a modification for Discord's desktop app * Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors * Copyright (c) 2024 Vendicated and contributors
* * SPDX-License-Identifier: GPL-3.0-or-later
* 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 "./style.css"; import "./style.css";
import { FFmpeg } from "@ffmpeg/ffmpeg";
import { Devs, EquicordDevs } from "@utils/constants"; import { Devs, EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { React } from "@webpack/common"; import { React } from "@webpack/common";
import { Channel } from "discord-types/general"; import { Channel } from "discord-types/general";
import { FFmpeg } from '@ffmpeg/ffmpeg'; import { PickerContent, PickerHeader, PickerSidebar, Settings, Wrapper } from "./components";
import { PickerSidebar } from "./components/PickerCategoriesSidebar";
import { PickerContent } from "./components/PickerContent";
import { PickerHeader } from "./components/PickerHeader";
import { Settings } from "./components/settings";
import { Wrapper } from "./components/wrapper";
import { getStickerPack, getStickerPackMetas } from "./stickers"; import { getStickerPack, getStickerPackMetas } from "./stickers";
import { StickerPack, StickerPackMeta, FFmpegState } from "./types"; import { StickerPack, StickerPackMeta } from "./types";
import { cl, FFmpegStateContext, loadFFmpeg } from "./utils"; import { cl, FFmpegStateContext, loadFFmpeg } from "./utils";
export default definePlugin({ export default definePlugin({
@ -57,7 +40,7 @@ export default definePlugin({
return `${head}${isMoreStickers}?$self.stickerButton:${button}${tail}`; return `${head}${isMoreStickers}?$self.stickerButton:${button}${tail}`;
} }
}, { }, {
match: /(\w=)(\w\.useCallback\(\(\)=>\{\(0,\w+\.\w+\)\([\w\.]*?\.STICKER,.*?);/, match: /(\w=)(\w\.useCallback\(\(\)=>\{\(0,\w+\.\w+\)\([\w.]*?\.STICKER,.*?);/,
replace: (_, decl, cb) => { replace: (_, decl, cb) => {
const newCb = cb.replace(/(?<=\(\)=>\{\(.*?\)\().+?\.STICKER/, "\"stickers+\""); const newCb = cb.replace(/(?<=\(\)=>\{\(.*?\)\().+?\.STICKER/, "\"stickers+\"");
return `${decl}arguments[0]?.stickersType?${newCb}:${cb};`; return `${decl}arguments[0]?.stickersType?${newCb}:${cb};`;
@ -71,7 +54,7 @@ export default definePlugin({
}] }]
}, },
{ {
find: '.gifts)', find: ".gifts)",
replacement: { replacement: {
match: /,\(null===\(\w=\w\.stickers\)\|\|void 0.*?(\w)\.push\((\(0,\w\.jsx\))\((.+?),{disabled:\w,type:(\w)},"sticker"\)\)/, match: /,\(null===\(\w=\w\.stickers\)\|\|void 0.*?(\w)\.push\((\(0,\w\.jsx\))\((.+?),{disabled:\w,type:(\w)},"sticker"\)\)/,
replace: (m, _, jsx, compo, type) => { replace: (m, _, jsx, compo, type) => {

View file

@ -1,19 +1,7 @@
/* /*
* Vencord, a modification for Discord's desktop app * Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors * Copyright (c) 2024 Vendicated and contributors
* * SPDX-License-Identifier: GPL-3.0-or-later
* 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 { LineEmoji, LineEmojiPack, Sticker, StickerPack } from "./types"; import { LineEmoji, LineEmojiPack, Sticker, StickerPack } from "./types";
@ -113,7 +101,7 @@ export function parseHtml(html: string): LineEmojiPack {
const { id } = mainImage; const { id } = mainImage;
const stickers = const stickers =
[...doc.querySelectorAll('.FnStickerPreviewItem')] [...doc.querySelectorAll(".FnStickerPreviewItem")]
.map(x => JSON.parse((x as HTMLElement).dataset.preview ?? "null")) .map(x => JSON.parse((x as HTMLElement).dataset.preview ?? "null"))
.filter(x => x !== null) .filter(x => x !== null)
.map(x => ({ ...x, stickerPackId: id })) as LineEmoji[]; .map(x => ({ ...x, stickerPackId: id })) as LineEmoji[];

View file

@ -1,19 +1,7 @@
/* /*
* Vencord, a modification for Discord's desktop app * Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors * Copyright (c) 2024 Vendicated and contributors
* * SPDX-License-Identifier: GPL-3.0-or-later
* 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 { LineSticker, LineStickerPack, Sticker, StickerPack } from "./types"; import { LineSticker, LineStickerPack, Sticker, StickerPack } from "./types";

View file

@ -1,24 +1,12 @@
/* /*
* Vencord, a modification for Discord's desktop app * Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors * Copyright (c) 2024 Vendicated and contributors
* * SPDX-License-Identifier: GPL-3.0-or-later
* 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 * as DataStore from "@api/DataStore"; import * as DataStore from "@api/DataStore";
import { removeRecentStickerByPackId } from "./components/recent"; import { removeRecentStickerByPackId } from "./components";
import { StickerPack, StickerPackMeta } from "./types"; import { StickerPack, StickerPackMeta } from "./types";
import { Mutex } from "./utils"; import { Mutex } from "./utils";
const mutex = new Mutex(); const mutex = new Mutex();

View file

@ -142,9 +142,9 @@
.vc-more-stickers-category-scroller::-webkit-scrollbar-track, .vc-more-stickers-category-scroller::-webkit-scrollbar-track,
.vc-more-stickers-picker-content-scroller::-webkit-scrollbar-track { .vc-more-stickers-picker-content-scroller::-webkit-scrollbar-track {
border: 2px solid var(--scrollbar-thin-track);
border-color: var(--scrollbar-thin-track); border-color: var(--scrollbar-thin-track);
background-color: var(--scrollbar-thin-track); background-color: var(--scrollbar-thin-track);
border: 2px solid var(--scrollbar-thin-track);
} }
.vc-more-stickers-header { .vc-more-stickers-header {
@ -316,8 +316,6 @@
position: absolute; position: absolute;
left: 0; left: 0;
top: 0; top: 0;
/* prettier-ignore */
mask: url("https://discord.com/assets/7d65d61e6cd25bf1847ec2d3e4a0144d.svg") 0 0/100% 100%; mask: url("https://discord.com/assets/7d65d61e6cd25bf1847ec2d3e4a0144d.svg") 0 0/100% 100%;
transition: background-color 0.08s ease-out, color 0.08s ease-out; transition: background-color 0.08s ease-out, color 0.08s ease-out;
z-index: -1; z-index: -1;
@ -337,7 +335,6 @@
} }
.vc-more-stickers-picker-content-row-grid-asset-wrapper:hover { .vc-more-stickers-picker-content-row-grid-asset-wrapper:hover {
/* prettier-ignore */
mask: url("https://discord.com/assets/7d65d61e6cd25bf1847ec2d3e4a0144d.svg") 0 0/100% 100%; mask: url("https://discord.com/assets/7d65d61e6cd25bf1847ec2d3e4a0144d.svg") 0 0/100% 100%;
} }

View file

@ -1,22 +1,10 @@
/* /*
* Vencord, a modification for Discord's desktop app * Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors * Copyright (c) 2024 Vendicated and contributors
* * SPDX-License-Identifier: GPL-3.0-or-later
* 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 { setRecentStickers } from "./components/recent"; import { setRecentStickers } from "./components";
import { import {
convert, convert,
getStickerPackById getStickerPackById
@ -44,11 +32,11 @@ export async function initTest() {
// Add test sticker packs // Add test sticker packs
console.log("Adding test sticker packs."); console.log("Adding test sticker packs.");
const lineStickerPackIds = [ const lineStickerPackIds = [
"22814489", // LV.47 野生喵喵怪 "22814489", // LV.47
"22567773", // LV.46 野生喵喵怪 "22567773", // LV.46
"22256215", // LV.45 野生喵喵怪 "22256215", // LV.45
"21936635", // LV.44 野生喵喵怪 "21936635", // LV.44
"21836565", // LV.43 野生喵喵怪 "21836565", // LV.43
]; ];
const ps: Promise<StickerPack | null>[] = []; const ps: Promise<StickerPack | null>[] = [];
for (const id of lineStickerPackIds) { for (const id of lineStickerPackIds) {

View file

@ -1,22 +1,16 @@
/* /*
* Vencord, a modification for Discord's desktop app * Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors * Copyright (c) 2024 Vendicated and contributors
* * SPDX-License-Identifier: GPL-3.0-or-later
* 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 type { FFmpeg } from '@ffmpeg/ffmpeg'; import type { FFmpeg } from "@ffmpeg/ffmpeg";
export interface CategoryImageProps {
src: string;
alt?: string;
isActive?: boolean;
}
export interface LineSticker { export interface LineSticker {
animationUrl: string, animationUrl: string,
@ -62,6 +56,57 @@ export interface LineEmojiPack {
stickers: LineEmoji[]; stickers: LineEmoji[];
} }
export interface PickerHeaderProps {
onQueryChange: (query: string) => void;
}
export interface PickerContent {
stickerPacks: StickerPack[];
selectedStickerPackId?: string | null;
setSelectedStickerPackId: React.Dispatch<React.SetStateAction<string | null>>;
channelId: string;
closePopout: () => void;
query?: string;
}
export interface PickerContentHeader {
image: string | React.ReactNode;
title: string;
children?: React.ReactNode;
isSelected?: boolean;
afterScroll?: () => void;
beforeScroll?: () => void;
}
export interface PickerContentRow {
rowIndex: number;
grid1: PickerContentRowGrid;
grid2?: PickerContentRowGrid;
grid3?: PickerContentRowGrid;
channelId: string;
}
export interface PickerContentRowGrid {
rowIndex: number;
colIndex: number;
sticker: Sticker;
onHover: (sticker: Sticker | null) => void;
isHovered?: boolean;
channelId?: string;
onSend?: (sticker?: Sticker, shouldClose?: boolean) => void;
}
export enum SettingsTabsKey {
ADD_STICKER_PACK_URL = "Add from URL",
ADD_STICKER_PACK_HTML = "Add from HTML",
ADD_STICKER_PACK_FILE = "Add from File",
}
export interface SidebarProps {
packMetas: StickerCategoryType[];
onPackSelect: (category: StickerCategoryType) => void;
}
export interface Sticker { export interface Sticker {
id: string; id: string;
image: string; image: string;
@ -71,6 +116,19 @@ export interface Sticker {
isAnimated?: boolean; isAnimated?: boolean;
} }
export interface StickerCategoryType {
id: string;
name: string;
iconUrl?: string;
}
export interface StickerCategoryProps {
children: React.ReactNode;
onClick?: () => void;
isActive: boolean;
style?: React.CSSProperties;
}
export interface StickerPackMeta { export interface StickerPackMeta {
id: string; id: string;
title: string; title: string;

View file

@ -1,26 +1,15 @@
/* /*
* Vencord, a modification for Discord's desktop app * Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors * Copyright (c) 2024 Vendicated and contributors
* * SPDX-License-Identifier: GPL-3.0-or-later
* 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 { FFmpeg } from "@ffmpeg/ffmpeg";
import { fetchFile } from "@ffmpeg/util";
import { findByPropsLazy, findLazy } from "@webpack"; import { findByPropsLazy, findLazy } from "@webpack";
import { ChannelStore } from "@webpack/common"; import { ChannelStore } from "@webpack/common";
import { FFmpegState, Sticker } from "./types"; import { FFmpegState, Sticker } from "./types";
import { FFmpeg } from '@ffmpeg/ffmpeg';
import { fetchFile, toBlobURL } from '@ffmpeg/util';
const MessageUpload = findByPropsLazy("instantBatchUpload"); const MessageUpload = findByPropsLazy("instantBatchUpload");
@ -103,7 +92,7 @@ async function toGIF(url: string, ffmpeg: FFmpeg): Promise<File> {
if (typeof data === "string") { if (typeof data === "string") {
throw new Error("Could not read file"); throw new Error("Could not read file");
} }
return new File([data.buffer], outputFilename, { type: 'image/gif' }); return new File([data.buffer], outputFilename, { type: "image/gif" });
} }
export async function sendSticker({ export async function sendSticker({

File diff suppressed because one or more lines are too long