mirror of
https://github.com/Equicord/Equicord.git
synced 2025-02-25 09:38:52 -05:00
Moved Some Stuff Around
This commit is contained in:
parent
7ff4d9e818
commit
166159dc0a
23 changed files with 402 additions and 597 deletions
|
@ -398,7 +398,7 @@ function resetSettings(plugin: Plugin, warningModalProps?: ModalProps, pluginMod
|
||||||
|
|
||||||
export function openWarningModal(plugin: Plugin, pluginModalProps: ModalProps, onRestartNeeded?: (pluginName: string) => void) {
|
export function openWarningModal(plugin: Plugin, pluginModalProps: ModalProps, onRestartNeeded?: (pluginName: string) => void) {
|
||||||
if (Settings.ignoreResetWarning) return resetSettings(plugin, pluginModalProps, pluginModalProps, onRestartNeeded);
|
if (Settings.ignoreResetWarning) return resetSettings(plugin, pluginModalProps, pluginModalProps, onRestartNeeded);
|
||||||
|
|
||||||
openModal(warningModalProps => (
|
openModal(warningModalProps => (
|
||||||
<ModalRoot
|
<ModalRoot
|
||||||
{...warningModalProps}
|
{...warningModalProps}
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
88
src/equicordplugins/moreStickers/components/categories.tsx
Normal file
88
src/equicordplugins/moreStickers/components/categories.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
10
src/equicordplugins/moreStickers/components/index.ts
Normal file
10
src/equicordplugins/moreStickers/components/index.ts
Normal 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";
|
|
@ -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));
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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));
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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[];
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -88,4 +158,4 @@ export interface StickerPack extends StickerPackMeta {
|
||||||
export interface FFmpegState {
|
export interface FFmpegState {
|
||||||
ffmpeg?: FFmpeg;
|
ffmpeg?: FFmpeg;
|
||||||
isLoaded: boolean;
|
isLoaded: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
Loading…
Add table
Reference in a new issue