Moar Fixes

This commit is contained in:
thororen 2024-10-19 01:49:08 -04:00
parent 69237bfe11
commit 8b246e5b9a
14 changed files with 4 additions and 1301 deletions

View file

@ -85,6 +85,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
- MessageLoggerEnhanced by Aria
- MessageTranslate by Samwich
- ModalFade by Kyuuhachi
- MoreStickers by Leko & Arjix
- NewPluginsManager by Sqaaakoi
- NoAppsAllowed by kvba
- NoBulletPoints by Samwich

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,402 +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 { React } from "@webpack/common";
import { Sticker, StickerPack } from "../types";
import { sendSticker } from "../upload";
import { RecentlyUsedIcon } from "./icons";
import { addRecentSticker, getRecentStickers, RECENT_STICKERS_ID, RECENT_STICKERS_TITLE } from "./recent";
import { clPicker, FFmpegStateContext } from "../utils";
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;
}
function PickerContentRowGrid({
rowIndex,
colIndex,
sticker,
onHover,
channelId,
onSend = () => { },
isHovered = false
}: PickerContentRowGrid) {
if (FFmpegStateContext === undefined) {
return <div>FFmpegStateContext is undefined</div>;
}
const ffmpegState = React.useContext(FFmpegStateContext);
return (
<div
role="gridcell"
aria-rowindex={rowIndex}
aria-colindex={colIndex}
id={clPicker(`content-row-grid-${rowIndex}-${colIndex}`)}
onMouseEnter={() => onHover(sticker)}
onClick={e => {
if (!channelId) return;
sendSticker({ channelId, sticker, ctrlKey: e.ctrlKey, shiftKey: e.shiftKey, ffmpegState });
addRecentSticker(sticker);
onSend(sticker, e.ctrlKey);
}}
>
<div
className={clPicker("content-row-grid-sticker")}
>
<span className={clPicker("content-row-grid-hidden-visually")}>{sticker.title}</span>
<div aria-hidden="true">
<div className={
[
clPicker("content-row-grid-inspected-indicator"),
`${isHovered ? "inspected" : ""}`
].join(" ")
}></div>
<div className={clPicker("content-row-grid-sticker-node")}>
<div className={clPicker("content-row-grid-asset-wrapper")} style={{
height: "96px",
width: "96px"
}}>
<img
alt={sticker.title}
src={sticker.image}
draggable="false"
datatype="sticker"
data-id={sticker.id}
className={clPicker("content-row-grid-img")}
loading="lazy"
/>
</div>
</div>
</div>
</div>
</div>
);
}
function PickerContentRow({ rowIndex, grid1, grid2, grid3, channelId }: PickerContentRow) {
return (
<div className={clPicker("content-row")}
role="row"
aria-rowindex={rowIndex}
>
<PickerContentRowGrid {...grid1} rowIndex={rowIndex} colIndex={1} channelId={channelId} />
{grid2 && <PickerContentRowGrid {...grid2} rowIndex={rowIndex} colIndex={2} channelId={channelId} />}
{grid3 && <PickerContentRowGrid {...grid3} rowIndex={rowIndex} colIndex={3} channelId={channelId} />}
</div>
);
}
function HeaderCollapseIcon({ isExpanded }: { isExpanded: boolean; }) {
return (
<svg
className={clPicker("content-header-collapse-icon")}
width={16} height={16} viewBox="0 0 24 24"
style={{
transform: `rotate(${isExpanded ? "0" : "-90deg"})`
}}
>
<path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M16.59 8.59004L12 13.17L7.41 8.59004L6 10L12 16L18 10L16.59 8.59004Z"></path>
</svg>
);
}
export function PickerContentHeader({
image,
title,
children,
isSelected = false,
afterScroll = () => { },
beforeScroll = () => { }
}: PickerContentHeader) {
const [isExpand, setIsExpand] = React.useState(true);
const headerElem = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (isSelected && headerElem.current) {
beforeScroll();
headerElem.current.scrollIntoView({
behavior: "smooth",
block: "start",
});
afterScroll();
}
}, [isSelected]);
return (
<span>
<div className={clPicker("content-header-wrapper")}>
<div className={clPicker("content-header-header")} ref={headerElem}
aria-expanded={isExpand}
aria-label={`Category, ${title}`}
role="button"
tabIndex={0}
onClick={() => {
setIsExpand(e => !e);
}}
>
<div className={clPicker("content-header-header-icon")}>
<div>
{typeof image === "string" ? <svg
className={clPicker("content-header-svg")}
width={16} height={16} viewBox="0 0 16 16"
>
<foreignObject
x={0} y={0} width={16} height={16}
overflow="visible" mask="url(#svg-mask-squircle)"
>
<img
alt={title}
src={image}
className={clPicker("content-header-guild-icon")}
loading="lazy"
></img>
</foreignObject>
</svg>
: image}
</div>
</div>
<span
className={clPicker("content-header-header-label")}
>
{title}
</span>
<HeaderCollapseIcon isExpanded={isExpand} />
</div>
</div>
{isExpand ? children : null}
</span>
);
}
export function PickerContent({ stickerPacks, selectedStickerPackId, setSelectedStickerPackId, channelId, closePopout, query }: PickerContent) {
const [currentSticker, setCurrentSticker] = (
React.useState<Sticker | null>((
stickerPacks.length && stickerPacks[0].stickers.length) ?
stickerPacks[0].stickers[0] :
null
)
);
const [currentStickerPack, setCurrentStickerPack] = React.useState<StickerPack | null>(stickerPacks.length ? stickerPacks[0] : null);
const [recentStickers, setRecentStickers] = React.useState<Sticker[]>([]);
const stickerPacksElemRef = React.useRef<HTMLDivElement>(null);
const scrollerRef = React.useRef<HTMLDivElement>(null);
function queryFilter(stickers: Sticker[]): Sticker[] {
if (!query) return stickers;
return stickers.filter(sticker => sticker.title.toLowerCase().includes(query.toLowerCase()));
}
async function fetchRecentStickers() {
const recentStickers = await getRecentStickers();
setRecentStickers(recentStickers);
}
React.useEffect(() => {
fetchRecentStickers();
}, []);
React.useEffect(() => {
if (currentStickerPack?.id !== currentSticker?.stickerPackId) {
setCurrentStickerPack(stickerPacks.find(p => p.id === currentSticker?.stickerPackId) ?? currentStickerPack);
}
}, [currentSticker]);
const stickersToRows = (stickers: Sticker[]): JSX.Element[] => stickers
.reduce((acc, sticker, i) => {
if (i % 3 === 0) {
acc.push([]);
}
acc[acc.length - 1].push(sticker);
return acc;
}, [] as Sticker[][])
.map((stickers, i) => (
<PickerContentRow
rowIndex={i}
channelId={channelId}
grid1={{
rowIndex: i,
colIndex: 1,
sticker: stickers[0],
onHover: setCurrentSticker,
onSend: (_, s) => { !s && closePopout(); },
isHovered: currentSticker?.id === stickers[0].id
}}
grid2={
stickers.length > 1 ? {
rowIndex: i,
colIndex: 2,
sticker: stickers[1],
onHover: setCurrentSticker,
onSend: (_, s) => { !s && closePopout(); },
isHovered: currentSticker?.id === stickers[1].id
} : undefined
}
grid3={
stickers.length > 2 ? {
rowIndex: i,
colIndex: 3,
sticker: stickers[2],
onHover: setCurrentSticker,
onSend: (_, s) => { !s && closePopout(); },
isHovered: currentSticker?.id === stickers[2].id
} : undefined
}
/>
));
return (
<div className={clPicker("content-list-wrapper")}>
<div className={clPicker("content-wrapper")}>
<div className={clPicker("content-scroller")} ref={scrollerRef}>
<div className={clPicker("content-list-items")} role="none presentation">
<div ref={stickerPacksElemRef}>
<PickerContentHeader
image={
<RecentlyUsedIcon width={16} height={16} color="currentColor" />
}
title={RECENT_STICKERS_TITLE}
isSelected={RECENT_STICKERS_ID === selectedStickerPackId}
beforeScroll={() => {
scrollerRef.current?.scrollTo({
top: 0,
});
}}
afterScroll={() => { setSelectedStickerPackId(null); }}
>
{
...stickersToRows(
queryFilter(recentStickers)
)
}
</PickerContentHeader>
{
stickerPacks.map(sp => {
const rows = stickersToRows(queryFilter(sp.stickers));
return (
<PickerContentHeader
image={sp.logo.image}
title={sp.title}
isSelected={sp.id === selectedStickerPackId}
beforeScroll={() => {
scrollerRef.current?.scrollTo({
top: 0,
});
}}
afterScroll={() => { setSelectedStickerPackId(null); }}
>
{...rows}
</PickerContentHeader>
);
})
}
</div>
</div>
<div style={{
height: `${stickerPacksElemRef.current?.clientHeight ?? 0}px`
}}></div>
</div>
<div
className={clPicker("content-inspector")}
style={{
visibility: !currentSticker ? "hidden" : "visible",
...(!currentSticker ? {
height: "0"
} : {})
}}
>
<div className={clPicker("content-inspector-graphic-primary")} aria-hidden="true">
<div>
<div className={clPicker("content-row-grid-asset-wrapper")} style={{
height: "28px",
width: "28px"
}}>
<img
alt={currentSticker?.title ?? ""}
src={currentSticker?.image}
draggable="false"
datatype="sticker"
data-id={currentSticker?.id ?? ""}
className={clPicker("content-inspector-img")}
/>
</div>
</div>
</div>
<div className={clPicker("content-inspector-text-wrapper")}>
<div className={clPicker("content-inspector-title-primary")} data-text-variant="text-md/semibold">{currentSticker?.title ?? ""}</div>
<div className={clPicker("content-inspector-title-secondary")} data-text-variant="text-md/semibold">
{currentStickerPack?.title ? "from " : ""}
<strong>{currentStickerPack?.title ?? ""}</strong>
</div>
</div>
<div className={clPicker("content-inspector-graphic-secondary")} aria-hidden="true">
<div>
<svg width={32} height={32} viewBox="0 0 32 32">
<foreignObject x={0} y={0} width={32} height={32} overflow="visible" mask="url(#svg-mask-squircle)">
<img
alt={currentStickerPack?.title ?? ""}
src={currentStickerPack?.logo?.image}
></img>
</foreignObject>
</svg>
</div>
</div>
</div>
</div>
</div>
);
}

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

@ -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

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

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

@ -25,7 +25,7 @@ export default definePlugin({
options: {
settings: {
type: OptionType.COMPONENT,
description: "Why is this here? Who is going to read this on a custom component? It isn't even rendered? What is its purpose?",
description: "Packs",
component: Settings
}
},
@ -103,7 +103,8 @@ export default definePlugin({
onClick={onClick}
style={{ backgroundColor: "transparent" }}
>
{/* Icon taken from: https://github.com/Pitu/Magane/blob/0ebb09acf9901933ebebe19fbd473ec08cf917b3/src/Button.svelte#L29 */}
{
/* Icon taken from: https://github.com/Pitu/Magane/blob/0ebb09acf9901933ebebe19fbd473ec08cf917b3/src/Button.svelte#L29 */}
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"