From 26a8961d9e4ec45fbaa0a1bf5807bd65557ca8d5 Mon Sep 17 00:00:00 2001 From: leko Date: Sat, 19 Oct 2024 13:42:14 +0800 Subject: [PATCH] feat(plugin): MoreStickers (#66) --- package.json | 2 + pnpm-lock.yaml | 36 ++ src/components/CheckedTextInput.tsx | 5 +- .../components/PickerCategoriesSidebar.tsx | 114 ++++ .../moreStickers/components/PickerContent.tsx | 402 ++++++++++++++ .../moreStickers/components/PickerHeader.tsx | 70 +++ .../moreStickers/components/categoryImage.tsx | 60 +++ .../components/categoryScroller.tsx | 37 ++ .../components/categoryWrapper.tsx | 27 + .../moreStickers/components/header.tsx | 27 + .../moreStickers/components/iconContainer.tsx | 31 ++ .../moreStickers/components/icons.tsx | 49 ++ .../moreStickers/components/recent.ts | 61 +++ .../moreStickers/components/settings.tsx | 397 ++++++++++++++ .../components/stickerCategory.tsx | 43 ++ .../moreStickers/components/wrapper.tsx | 30 ++ src/equicordplugins/moreStickers/index.tsx | 225 ++++++++ .../moreStickers/lineEmojis.ts | 150 ++++++ .../moreStickers/lineStickers.ts | 149 +++++ src/equicordplugins/moreStickers/stickers.ts | 120 +++++ src/equicordplugins/moreStickers/style.css | 508 ++++++++++++++++++ src/equicordplugins/moreStickers/testdata.ts | 89 +++ src/equicordplugins/moreStickers/types.ts | 91 ++++ src/equicordplugins/moreStickers/upload.ts | 210 ++++++++ src/equicordplugins/moreStickers/utils.tsx | 78 +++ src/utils/constants.ts | 6 +- 26 files changed, 3015 insertions(+), 2 deletions(-) create mode 100644 src/equicordplugins/moreStickers/components/PickerCategoriesSidebar.tsx create mode 100644 src/equicordplugins/moreStickers/components/PickerContent.tsx create mode 100644 src/equicordplugins/moreStickers/components/PickerHeader.tsx create mode 100644 src/equicordplugins/moreStickers/components/categoryImage.tsx create mode 100644 src/equicordplugins/moreStickers/components/categoryScroller.tsx create mode 100644 src/equicordplugins/moreStickers/components/categoryWrapper.tsx create mode 100644 src/equicordplugins/moreStickers/components/header.tsx create mode 100644 src/equicordplugins/moreStickers/components/iconContainer.tsx create mode 100644 src/equicordplugins/moreStickers/components/icons.tsx create mode 100644 src/equicordplugins/moreStickers/components/recent.ts create mode 100644 src/equicordplugins/moreStickers/components/settings.tsx create mode 100644 src/equicordplugins/moreStickers/components/stickerCategory.tsx create mode 100644 src/equicordplugins/moreStickers/components/wrapper.tsx create mode 100644 src/equicordplugins/moreStickers/index.tsx create mode 100644 src/equicordplugins/moreStickers/lineEmojis.ts create mode 100644 src/equicordplugins/moreStickers/lineStickers.ts create mode 100644 src/equicordplugins/moreStickers/stickers.ts create mode 100644 src/equicordplugins/moreStickers/style.css create mode 100644 src/equicordplugins/moreStickers/testdata.ts create mode 100644 src/equicordplugins/moreStickers/types.ts create mode 100644 src/equicordplugins/moreStickers/upload.ts create mode 100644 src/equicordplugins/moreStickers/utils.tsx diff --git a/package.json b/package.json index 77396d5b..bff89bba 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,8 @@ "testTsc": "tsc --noEmit" }, "dependencies": { + "@ffmpeg/ffmpeg": "^0.12.10", + "@ffmpeg/util": "^0.12.1", "@sapphi-red/web-noise-suppressor": "0.3.5", "@types/less": "^3.0.6", "@types/stylus": "^0.48.42", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5f2c3b19..f3e444a5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,6 +16,12 @@ importers: .: dependencies: + '@ffmpeg/ffmpeg': + specifier: ^0.12.10 + version: 0.12.10 + '@ffmpeg/util': + specifier: ^0.12.1 + version: 0.12.1 '@sapphi-red/web-noise-suppressor': specifier: 0.3.5 version: 0.3.5 @@ -419,6 +425,21 @@ packages: '@eslint/object-schema@2.1.4': resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ffmpeg/ffmpeg@0.12.10': + resolution: {integrity: sha512-lVtk8PW8e+NUzGZhPTWj2P1J4/NyuCrbDD3O9IGpSeLYtUZKBqZO8CNj1WYGghep/MXoM8e1qVY1GztTkf8YYQ==} + engines: {node: '>=18.x'} + + '@ffmpeg/types@0.12.2': + resolution: {integrity: sha512-NJtxwPoLb60/z1Klv0ueshguWQ/7mNm106qdHkB4HL49LXszjhjCCiL+ldHJGQ9ai2Igx0s4F24ghigy//ERdA==} + engines: {node: '>=16.x'} + + '@ffmpeg/util@0.12.1': + resolution: {integrity: sha512-10jjfAKWaDyb8+nAkijcsi9wgz/y26LOc1NKJradNMyCIl6usQcBbhkjX5qhALrSBcOy6TOeksunTYa+a03qNQ==} + engines: {node: '>=18.x'} + + '@humanwhocodes/config-array@0.11.10': + resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==} + engines: {node: '>=10.10.0'} '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} @@ -2564,6 +2585,21 @@ snapshots: '@eslint/js@9.8.0': {} '@eslint/object-schema@2.1.4': {} + '@ffmpeg/ffmpeg@0.12.10': + dependencies: + '@ffmpeg/types': 0.12.2 + + '@ffmpeg/types@0.12.2': {} + + '@ffmpeg/util@0.12.1': {} + + '@humanwhocodes/config-array@0.11.10': + dependencies: + '@humanwhocodes/object-schema': 1.2.1 + debug: 4.3.4 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color '@humanwhocodes/module-importer@1.0.1': {} diff --git a/src/components/CheckedTextInput.tsx b/src/components/CheckedTextInput.tsx index cf4aa119..3fc42226 100644 --- a/src/components/CheckedTextInput.tsx +++ b/src/components/CheckedTextInput.tsx @@ -34,13 +34,15 @@ interface TextInputProps { * Otherwise, return a string containing the reason for this input being invalid */ validate(v: string): true | string; + + placeholder?: string; } /** * A very simple wrapper around Discord's TextInput that validates input and shows * the user an error message and only calls your onChange when the input is valid */ -export function CheckedTextInput({ value: initialValue, onChange, validate }: TextInputProps) { +export function CheckedTextInput({ value: initialValue, onChange, validate, placeholder }: TextInputProps) { const [value, setValue] = React.useState(initialValue); const [error, setError] = React.useState(); @@ -62,6 +64,7 @@ export function CheckedTextInput({ value: initialValue, onChange, validate }: Te value={value} onChange={handleChange} error={error} + placeholder={placeholder} /> ); diff --git a/src/equicordplugins/moreStickers/components/PickerCategoriesSidebar.tsx b/src/equicordplugins/moreStickers/components/PickerCategoriesSidebar.tsx new file mode 100644 index 00000000..6618c4d6 --- /dev/null +++ b/src/equicordplugins/moreStickers/components/PickerCategoriesSidebar.tsx @@ -0,0 +1,114 @@ +/* + * 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 . +*/ + +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(RecentPack); + const [hovering, setHovering] = React.useState(false); + + return ( + + + { + if (activePack === RecentPack) return; + + onPackSelect(RecentPack); + setActivePack(RecentPack); + }} + > + + + { + ...packMetas.map(pack => { + return ( + { + if (activePack?.id === pack.id) return; + + onPackSelect(pack); + setActivePack(pack); + }} + isActive={activePack?.id === pack.id} + > + + + ); + }) + } + +
+ +
+
+ ); +}; diff --git a/src/equicordplugins/moreStickers/components/PickerContent.tsx b/src/equicordplugins/moreStickers/components/PickerContent.tsx new file mode 100644 index 00000000..c7d81181 --- /dev/null +++ b/src/equicordplugins/moreStickers/components/PickerContent.tsx @@ -0,0 +1,402 @@ +/* + * 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 . +*/ + +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>; + 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
FFmpegStateContext is undefined
; + } + + const ffmpegState = React.useContext(FFmpegStateContext); + + return ( +
onHover(sticker)} + onClick={e => { + if (!channelId) return; + + sendSticker({ channelId, sticker, ctrlKey: e.ctrlKey, shiftKey: e.shiftKey, ffmpegState }); + addRecentSticker(sticker); + onSend(sticker, e.ctrlKey); + }} + > +
+ {sticker.title} + +
+
+ ); +} + +function PickerContentRow({ rowIndex, grid1, grid2, grid3, channelId }: PickerContentRow) { + return ( +
+ + {grid2 && } + {grid3 && } +
+ ); +} + + +function HeaderCollapseIcon({ isExpanded }: { isExpanded: boolean; }) { + return ( + + + + ); +} + +export function PickerContentHeader({ + image, + title, + children, + isSelected = false, + afterScroll = () => { }, + beforeScroll = () => { } +}: PickerContentHeader) { + + const [isExpand, setIsExpand] = React.useState(true); + const headerElem = React.useRef(null); + React.useEffect(() => { + if (isSelected && headerElem.current) { + beforeScroll(); + + headerElem.current.scrollIntoView({ + behavior: "smooth", + block: "start", + }); + + afterScroll(); + } + }, [isSelected]); + + return ( + +
+
{ + setIsExpand(e => !e); + }} + > +
+
+ {typeof image === "string" ? + + {title} + + + : image} +
+
+ + {title} + + +
+
+ {isExpand ? children : null} +
+ ); +} + +export function PickerContent({ stickerPacks, selectedStickerPackId, setSelectedStickerPackId, channelId, closePopout, query }: PickerContent) { + const [currentSticker, setCurrentSticker] = ( + React.useState(( + stickerPacks.length && stickerPacks[0].stickers.length) ? + stickerPacks[0].stickers[0] : + null + ) + ); + + const [currentStickerPack, setCurrentStickerPack] = React.useState(stickerPacks.length ? stickerPacks[0] : null); + const [recentStickers, setRecentStickers] = React.useState([]); + + const stickerPacksElemRef = React.useRef(null); + const scrollerRef = React.useRef(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) => ( + { !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 ( +
+
+
+
+
+ + } + title={RECENT_STICKERS_TITLE} + isSelected={RECENT_STICKERS_ID === selectedStickerPackId} + beforeScroll={() => { + scrollerRef.current?.scrollTo({ + top: 0, + }); + }} + afterScroll={() => { setSelectedStickerPackId(null); }} + > + { + ...stickersToRows( + queryFilter(recentStickers) + ) + } + + { + stickerPacks.map(sp => { + const rows = stickersToRows(queryFilter(sp.stickers)); + return ( + { + scrollerRef.current?.scrollTo({ + top: 0, + }); + }} + afterScroll={() => { setSelectedStickerPackId(null); }} + > + {...rows} + + ); + }) + } +
+
+
+
+
+ +
+
{currentSticker?.title ?? ""}
+
+ {currentStickerPack?.title ? "from " : ""} + {currentStickerPack?.title ?? ""} +
+
+ +
+
+
+ ); +} diff --git a/src/equicordplugins/moreStickers/components/PickerHeader.tsx b/src/equicordplugins/moreStickers/components/PickerHeader.tsx new file mode 100644 index 00000000..ce8e1ede --- /dev/null +++ b/src/equicordplugins/moreStickers/components/PickerHeader.tsx @@ -0,0 +1,70 @@ +/* + * 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 . +*/ + +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(); + + const setQueryDebounced = (value: string, immediate = false) => { + setQuery(value); + if (immediate) onQueryChange(value); + else debounceQueryChange(onQueryChange, value); + }; + + return ( +
+
+
+
+ setQueryDebounced(value)} + /> +
+
+ + { + (query && query.length > 0) ? + setQueryDebounced("", true)} /> : + + } + +
+
+
+
+ ); +}; diff --git a/src/equicordplugins/moreStickers/components/categoryImage.tsx b/src/equicordplugins/moreStickers/components/categoryImage.tsx new file mode 100644 index 00000000..00a996b3 --- /dev/null +++ b/src/equicordplugins/moreStickers/components/categoryImage.tsx @@ -0,0 +1,60 @@ +/* + * 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 . +*/ + +import { cl } from "../utils"; + +export interface CategoryImageProps { + src: string; + alt?: string; + isActive?: boolean; +} + +export function CategoryImage({ src, alt, isActive }: CategoryImageProps) { + return ( +
+ + + {alt} + + +
+ ); +} diff --git a/src/equicordplugins/moreStickers/components/categoryScroller.tsx b/src/equicordplugins/moreStickers/components/categoryScroller.tsx new file mode 100644 index 00000000..c7d77601 --- /dev/null +++ b/src/equicordplugins/moreStickers/components/categoryScroller.tsx @@ -0,0 +1,37 @@ +/* + * 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 . +*/ + +import { cl } from "../utils"; + +export function CategoryScroller(props: { children: React.ReactNode, categoryLength: number; }) { + const children = Array.isArray(props.children) ? props.children : [props.children]; + + return ( +
+
{ + children.map(child => ( +
+ {child} +
+ )) + }
+
+ +
+ ); +} diff --git a/src/equicordplugins/moreStickers/components/categoryWrapper.tsx b/src/equicordplugins/moreStickers/components/categoryWrapper.tsx new file mode 100644 index 00000000..fd6a2559 --- /dev/null +++ b/src/equicordplugins/moreStickers/components/categoryWrapper.tsx @@ -0,0 +1,27 @@ +/* + * 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 . +*/ + +import { cl } from "../utils"; + +export function CategoryWrapper(props: { children: JSX.Element | JSX.Element[]; }) { + return ( +
+ {props.children} +
+ ); +} diff --git a/src/equicordplugins/moreStickers/components/header.tsx b/src/equicordplugins/moreStickers/components/header.tsx new file mode 100644 index 00000000..f9257834 --- /dev/null +++ b/src/equicordplugins/moreStickers/components/header.tsx @@ -0,0 +1,27 @@ +/* + * 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 . +*/ + +import { cl } from "../utils"; + +export function Header(props: { children: JSX.Element | JSX.Element[]; }) { + return ( +
+ {props.children} +
+ ); +} diff --git a/src/equicordplugins/moreStickers/components/iconContainer.tsx b/src/equicordplugins/moreStickers/components/iconContainer.tsx new file mode 100644 index 00000000..3bd8e6d6 --- /dev/null +++ b/src/equicordplugins/moreStickers/components/iconContainer.tsx @@ -0,0 +1,31 @@ +/* + * 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 . +*/ + +export function IconContainer(props: { children: JSX.Element | JSX.Element[]; }) { + return ( +
+ {props.children} +
+ ); +} diff --git a/src/equicordplugins/moreStickers/components/icons.tsx b/src/equicordplugins/moreStickers/components/icons.tsx new file mode 100644 index 00000000..18e3f031 --- /dev/null +++ b/src/equicordplugins/moreStickers/components/icons.tsx @@ -0,0 +1,49 @@ +/* + * 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 . +*/ + +export function SearchIcon({ width, height, color }: { width: number, height: number, color: string; }) { + return ( + + + + ); +}; + +export function CancelIcon({ width, height, className, onClick }: { width: number, height: number, className: string, onClick: () => void; }) { + return ( + + + + ); +}; + +export function RecentlyUsedIcon({ width, height, color }: { width: number, height: number, color: string; }) { + return ( + + + + ); +}; + +export function CogIcon({ width, height }: { width: number, height: number; }) { + return ( + + + + ); +}; diff --git a/src/equicordplugins/moreStickers/components/recent.ts b/src/equicordplugins/moreStickers/components/recent.ts new file mode 100644 index 00000000..012c4287 --- /dev/null +++ b/src/equicordplugins/moreStickers/components/recent.ts @@ -0,0 +1,61 @@ +/* + * 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 . +*/ + +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 { + return (await DataStore.get(KEY)) ?? []; +} + +export async function setRecentStickers(stickers: Sticker[]): Promise { + const unlock = await mutex.lock(); + try { + await DataStore.set(KEY, stickers); + } finally { + unlock(); + } +} + +export async function addRecentSticker(sticker: Sticker): Promise { + 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 { + const stickers = await getRecentStickers(); + await setRecentStickers(stickers.filter(s => s.stickerPackId !== packId)); +} diff --git a/src/equicordplugins/moreStickers/components/settings.tsx b/src/equicordplugins/moreStickers/components/settings.tsx new file mode 100644 index 00000000..4773c659 --- /dev/null +++ b/src/equicordplugins/moreStickers/components/settings.tsx @@ -0,0 +1,397 @@ +/* + * 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 . +*/ + +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 ( +
setHoveredStickerPackId(meta.id)} + onMouseLeave={() => setHoveredStickerPackId(null)} + > +
+ + + {meta.title} +
+ ); +}; + +export const Settings = () => { + const [stickerPackMetas, setstickerPackMetas] = React.useState([]); + const [addStickerUrl, setAddStickerUrl] = React.useState(""); + const [addStickerHtml, setAddStickerHtml] = React.useState(""); + const [tab, setTab] = React.useState(SettingsTabsKey.ADD_STICKER_PACK_URL); + const [hoveredStickerPackId, setHoveredStickerPackId] = React.useState(null); + + async function refreshStickerPackMetas() { + setstickerPackMetas(await getStickerPackMetas()); + } + React.useEffect(() => { + refreshStickerPackMetas(); + }, []); + + return ( +
+ + { + Object.values(SettingsTabsKey).map(k => ( + + {k} + + )) + } + + + {tab === SettingsTabsKey.ADD_STICKER_PACK_URL && +
+ Add Sticker Pack from URL + +

+ Currently LINE stickers supported only.
+ 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. +

+
+ + + { + try { + getLineStickerPackIdFromUrl(v); + return true; + } catch (e: any) { } + try { + getLineEmojiPackIdFromUrl(v); + return true; + } catch (e: any) { } + + return "Invalid URL"; + }} + placeholder="Sticker Pack URL" + /> + + + +
+ } + {tab === SettingsTabsKey.ADD_STICKER_PACK_HTML && +
+ Add Sticker Pack from HTML + +

+ When encountering errors while adding a sticker pack, you can try to add it using the HTML source code of the sticker pack page.
+ This applies to stickers which are region locked / OS locked / etc.
+ The region LINE recognized may vary from the region you are in due to the CORS proxy we're using. +

+
+ + +