diff --git a/src/plugins/kaomojiPicker/README.md b/src/plugins/kaomojiPicker/README.md new file mode 100644 index 00000000000..eb587918875 --- /dev/null +++ b/src/plugins/kaomojiPicker/README.md @@ -0,0 +1,8 @@ +# KaomojiPicker (๑>ᴗ<๑) + +Adds a Kaomoji Tab to bring back the removed Kaomoji experiments + +### Small tips +You can quickly open the Kaomoji Tab by pressing `Ctrl + E` followed by `Ctrl + F`. + +![kaomoji tab in the expression picker](https://github.com/user-attachments/assets/0051f55d-0bb3-4ef7-970d-5a4830354552) diff --git a/src/plugins/kaomojiPicker/cl.ts b/src/plugins/kaomojiPicker/cl.ts new file mode 100644 index 00000000000..a428f89018d --- /dev/null +++ b/src/plugins/kaomojiPicker/cl.ts @@ -0,0 +1,9 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2026 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { classNameFactory } from "@utils/css"; + +export const cl = classNameFactory("vc-kaomoji-"); diff --git a/src/plugins/kaomojiPicker/components/AddKaomojiModal.tsx b/src/plugins/kaomojiPicker/components/AddKaomojiModal.tsx new file mode 100644 index 00000000000..73f5c2b042f --- /dev/null +++ b/src/plugins/kaomojiPicker/components/AddKaomojiModal.tsx @@ -0,0 +1,178 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2026 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { TextButton } from "@components/Button"; +import { Flex } from "@components/Flex"; +import { HeadingSecondary } from "@components/Heading"; +import { BaseText } from "@components/index"; +import { cl } from "@plugins/kaomojiPicker/cl"; +import { getAllKaomoji, getCategories, Kaomoji, parseUserSetting } from "@plugins/kaomojiPicker/data/kaomoji"; +import { saveUserKaomoji, useKaomojiStore } from "@plugins/kaomojiPicker/store"; +import { RenderModalProps } from "@vencord/discord-types"; +import { Modal, openModal, SearchableSelect, TextArea, TextInput, useState } from "@webpack/common"; + +import { openCreateCategoryModal } from "./CreateCategoryModal"; + +export function openAddKaomojiModal() { + openModal(modalProps => ( + + )); +} + +function AddKaomojiModal({ modalProps }: { modalProps: RenderModalProps; }) { + const { userKaomoji } = useKaomojiStore(); + const [isAdvanced, setIsAdvanced] = useState(false); + + const [id, setId] = useState(""); + const [value, setValue] = useState(""); + const [category, setCategory] = useState(""); + const [pendingCategories, setPendingCategories] = useState([]); + const [rawInput, setRawInput] = useState(""); + + const categories = Array.from(new Set([...getCategories(), ...pendingCategories])); + + const idTrimmed = id.trim(); + const valueTrimmed = value.trim(); + const isDuplicateId = Boolean(idTrimmed) && getAllKaomoji().some(e => e.id.toLowerCase() === idTrimmed.toLowerCase()); + const isDuplicateValue = Boolean(valueTrimmed) && getAllKaomoji().some(e => e.value === valueTrimmed); + const canSubmitSimple = Boolean(idTrimmed && valueTrimmed && !isDuplicateId && !isDuplicateValue); + + const parsedItems = parseUserSetting(rawInput); + const validItems = parsedItems.filter(item => !getAllKaomoji().some(exist => exist.value === item.value)); + const duplicate = parsedItems.length - validItems.length; + const canSubmitAdvanced = validItems.length > 0; + + const canSubmit = isAdvanced ? canSubmitAdvanced : canSubmitSimple; + + const errorMessage = parsedItems.length > 0 && validItems.length === 0 + ? "All entered kaomojis already exists" + : duplicate > 0 + ? `${duplicate} duplicate ${duplicate === 1 ? "kaomoji" : "kaomojis"} will be skipped` + : undefined; + + function handleSubmit() { + if (!canSubmit) return; + + if (isAdvanced) { + saveUserKaomoji([...userKaomoji, ...validItems]); + } else { + const kaomoji: Kaomoji = { + id: idTrimmed, + value: valueTrimmed, + tags: [category], + }; + saveUserKaomoji([...userKaomoji, kaomoji]); + } + modalProps.onClose(); + } + + return ( + + + {!isAdvanced ? ( + <> +
+ + ID + setIsAdvanced(true)} + > + Advanced addition(s) + + + +
+
+ Kaomoji + +
+
+ Category + ({ label: c.charAt(0).toUpperCase() + c.slice(1), value: c }))} + value={category} + multi={false} + onChange={setCategory} + placeholder="Select category" + /> +
+ { + openCreateCategoryModal(name => { + const tag = name.trim().toLowerCase(); + if (tag) { + if (!categories.includes(tag)) { + setPendingCategories([...pendingCategories, tag]); + } + setCategory(tag); + } + }); + }} + > + Create new category + +
+
+ + ) : ( + <> +
+ + Import String / JSON + setIsAdvanced(false)} + > + Simple addition + + +