Moved Some Stuff Around

This commit is contained in:
thororen 2024-10-19 01:19:15 -04:00
parent 7ff4d9e818
commit 166159dc0a
23 changed files with 402 additions and 597 deletions

View file

@ -1,114 +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 { ModalContent, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { React, Text } from "@webpack/common";
import { CategoryImage } from "./categoryImage";
import { CategoryScroller } from "./categoryScroller";
import { CategoryWrapper } from "./categoryWrapper";
import { CogIcon, RecentlyUsedIcon } from "./icons";
import { RECENT_STICKERS_ID, RECENT_STICKERS_TITLE } from "./recent";
import { Settings } from "./settings";
import { StickerCategory } from "./stickerCategory";
import { cl, clPicker } from "../utils";
export interface StickerCategory {
id: string;
name: string;
iconUrl?: string;
}
export interface SidebarProps {
packMetas: StickerCategory[];
onPackSelect: (category: StickerCategory) => void;
}
export const RecentPack = {
id: RECENT_STICKERS_ID,
name: RECENT_STICKERS_TITLE,
} as StickerCategory;
export const PickerSidebar = ({ packMetas, onPackSelect }: SidebarProps) => {
const [activePack, setActivePack] = React.useState<StickerCategory>(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>
);
};

View file

@ -1,70 +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 { debounce } from "@shared/debounce";
import { React, TextInput } from "@webpack/common";
import { Header } from "./header";
import { IconContainer } from "./iconContainer";
import { CancelIcon, SearchIcon } from "./icons";
import { clPicker } from "../utils";
export interface PickerHeaderProps {
onQueryChange: (query: string) => void;
}
const debounceQueryChange = debounce((cb: Function, ...args: any) => cb(...args), 150);
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

@ -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,60 +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 { cl } from "../utils";
export interface CategoryImageProps {
src: string;
alt?: string;
isActive?: boolean;
}
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>
);
}

View file

@ -1,37 +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 { cl } from "../utils";
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>
);
}

View file

@ -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 { cl } from "../utils";
export function CategoryWrapper(props: { children: JSX.Element | JSX.Element[]; }) {
return (
<div className={cl("category-wrapper")}>
{props.children}
</div>
);
}

View file

@ -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 { cl } from "../utils";
export function Header(props: { children: JSX.Element | JSX.Element[]; }) {
return (
<div className={cl("header")}>
{props.children}
</div>
);
}

View file

@ -1,31 +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 IconContainer(props: { children: JSX.Element | JSX.Element[]; }) {
return (
<div style={{
width: "20px",
height: "20px",
boxSizing: "border-box",
position: "relative",
cursor: "text"
}}>
{props.children}
</div>
);
}

View file

@ -16,13 +16,28 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * 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 +45,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 +53,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 +61,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

@ -16,21 +16,24 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as DataStore from "@api/DataStore";
import { CheckedTextInput } from "@components/CheckedTextInput"; import { CheckedTextInput } from "@components/CheckedTextInput";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
import { Button, Forms, React, TabBar, Text, TextArea, Toasts } from "@webpack/common"; import { Button, Forms, React, TabBar, Text, TextArea, Toasts } from "@webpack/common";
import { convert as convertLineSP, getIdFromUrl as getLineStickerPackIdFromUrl, getStickerPackById as getLineStickerPackById, parseHtml as getLineSPFromHtml, isLineStickerPackHtml } from "../lineStickers"; import { convert as convertLineEP, getIdFromUrl as getLineEmojiPackIdFromUrl, getStickerPackById as getLineEmojiPackById, isLineEmojiPackHtml, parseHtml as getLineEPFromHtml } from "../lineEmojis";
import { convert as convertLineEP, getIdFromUrl as getLineEmojiPackIdFromUrl, getStickerPackById as getLineEmojiPackById, parseHtml as getLineEPFromHtml, isLineEmojiPackHtml } 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 { deleteStickerPack, getStickerPackMetas, saveStickerPack } from "../stickers";
import { StickerPack, StickerPackMeta } from "../types"; import { SettingsTabsKey, Sticker, StickerPack, StickerPackMeta } from "../types";
import { cl, clPicker } from "../utils"; import { cl, clPicker, Mutex } from "../utils";
enum SettingsTabsKey { const mutex = new Mutex();
ADD_STICKER_PACK_URL = "Add from URL",
ADD_STICKER_PACK_HTML = "Add from HTML", // The ID of recent sticker and recent sticker pack
ADD_STICKER_PACK_FILE = "Add from File", export const RECENT_STICKERS_ID = "recent";
} export const RECENT_STICKERS_TITLE = "Recently Used";
const KEY = "Vencord-MoreStickers-RecentStickers";
const noDrag = { const noDrag = {
onMouseDown: e => { e.preventDefault(); return false; }, onMouseDown: e => { e.preventDefault(); return false; },
@ -189,7 +192,7 @@ export const Settings = () => {
errorMessage = e.message; errorMessage = e.message;
} }
break; break;
}; }
case "LineEmojiPack": { case "LineEmojiPack": {
try { try {
const id = getLineEmojiPackIdFromUrl(addStickerUrl); const id = getLineEmojiPackIdFromUrl(addStickerUrl);
@ -395,3 +398,56 @@ export const Settings = () => {
</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

@ -1,64 +1,95 @@
/* /*
* 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 { React } from "@webpack/common"; import { debounce } from "@shared/debounce";
import { ModalContent, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { React, Text, TextInput } from "@webpack/common";
import { Sticker, StickerPack } from "../types"; import { PickerContent, PickerContentHeader, PickerContentRow, PickerContentRowGrid, PickerHeaderProps, SidebarProps, Sticker, StickerCategoryType, StickerPack } from "../types";
import { sendSticker } from "../upload"; import { sendSticker } from "../upload";
import { RecentlyUsedIcon } from "./icons";
import { addRecentSticker, getRecentStickers, RECENT_STICKERS_ID, RECENT_STICKERS_TITLE } from "./recent";
import { clPicker, FFmpegStateContext } from "../utils"; 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";
export interface PickerContent { const debounceQueryChange = debounce((cb: Function, ...args: any) => cb(...args), 150);
stickerPacks: StickerPack[];
selectedStickerPackId?: string | null;
setSelectedStickerPackId: React.Dispatch<React.SetStateAction<string | null>>;
channelId: string;
closePopout: () => void;
query?: string;
}
export interface PickerContentHeader { export const RecentPack = {
image: string | React.ReactNode; id: RECENT_STICKERS_ID,
title: string; name: RECENT_STICKERS_TITLE,
children?: React.ReactNode; } as StickerCategoryType;
isSelected?: boolean;
afterScroll?: () => void;
beforeScroll?: () => void;
}
export interface PickerContentRow { export const PickerSidebar = ({ packMetas, onPackSelect }: SidebarProps) => {
rowIndex: number; const [activePack, setActivePack] = React.useState<StickerCategoryType>(RecentPack);
grid1: PickerContentRowGrid; const [hovering, setHovering] = React.useState(false);
grid2?: PickerContentRowGrid;
grid3?: PickerContentRowGrid;
channelId: string;
}
export interface PickerContentRowGrid { return (
rowIndex: number; <CategoryWrapper>
colIndex: number; <CategoryScroller categoryLength={packMetas.length}>
sticker: Sticker; <StickerCategory
onHover: (sticker: Sticker | null) => void; style={{ padding: "4px", boxSizing: "border-box", width: "32px" }}
isHovered?: boolean; isActive={activePack === RecentPack}
channelId?: string; onClick={() => {
onSend?: (sticker?: Sticker, shouldClose?: boolean) => void; 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({ function PickerContentRowGrid({
rowIndex, rowIndex,
@ -400,3 +431,43 @@ export function PickerContent({ stickerPacks, selectedStickerPackId, setSelected
</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,61 +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 * as DataStore from "@api/DataStore";
import { Sticker } from "../types";
import { 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";
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

@ -1,43 +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 { cl } from "../utils";
export interface StickerCategoryProps {
children: React.ReactNode;
onClick?: () => void;
isActive: boolean;
style?: React.CSSProperties;
}
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,30 +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 Wrapper(props: { children: JSX.Element | JSX.Element[]; }) {
return (
<div style={{
position: "relative",
display: "grid",
gridTemplateColumns: "48px auto",
gridTemplateRows: "auto 1fr auto",
}}>
{props.children}
</div>
);
}

View file

@ -18,20 +18,15 @@
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 +52,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 +66,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

@ -113,7 +113,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

@ -18,7 +18,7 @@
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

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * 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 +44,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

@ -16,7 +16,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * 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 +68,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 +128,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

@ -16,11 +16,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * 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 +104,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