diff --git a/plugins/custom-user-tags/manifest.json b/plugins/custom-user-tags/manifest.json index 35a7505..bf94e59 100644 --- a/plugins/custom-user-tags/manifest.json +++ b/plugins/custom-user-tags/manifest.json @@ -1,6 +1,6 @@ { "name": "Custom User Tags", - "description": "Create your own tags (text + color) and assign them to specific users - long-press a name in the member list or profile popout to manage one, or add one by user ID in settings.", + "description": "Create your own tags (text, color, and icon) and assign them to specific users - long-press a name in the member list or profile popout to manage one, or add one by user ID in settings. Includes a color picker with hex/RGB support and a library of custom icons.", "authors": [ { "name": "everestmcarthur" } ], diff --git a/plugins/custom-user-tags/src/lib/icons.ts b/plugins/custom-user-tags/src/lib/icons.ts new file mode 100644 index 0000000..a248aab --- /dev/null +++ b/plugins/custom-user-tags/src/lib/icons.ts @@ -0,0 +1,82 @@ +export interface IconDef { + id: string; + name: string; + path: string; + fallback: string; + viewBox?: string; +} + +export const ICONS: IconDef[] = [ + { id: "none", name: "No icon", path: "", fallback: "" }, + { id: "star", name: "Star", path: "M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z", fallback: "โ˜…" }, + { id: "heart", name: "Heart", path: "M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z", fallback: "โ™ฅ" }, + { id: "crown", name: "Crown", path: "M5 16L3 5l5.5 5L12 4l3.5 6L21 5l-2 11H5zm14 3c0 .6-.4 1-1 1H6c-.6 0-1-.4-1-1v-1h14v1z", fallback: "โ™›" }, + { id: "gem", name: "Gem", path: "M12 2l8 5-8 15-8-15 8-5z", fallback: "๐Ÿ’Ž" }, + { id: "shield", name: "Shield", path: "M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4z", fallback: "๐Ÿ›ก" }, + { id: "bolt", name: "Bolt", path: "M11 21h-1l1-7H7.5c-.58 0-.57-.32-.38-.66.19-.34.05-.08.07-.12C8.48 10.94 11.07 6 13.5 6c.46 0 .85.33.96.79L15 10h3.5c.49 0 .64.32.45.66-.19.34-.05.08-.07.12C17.52 14.06 15.07 19 13 19l-1 2z", fallback: "โšก" }, + { id: "flame", name: "Flame", path: "M13.5.67s.74 2.65.74 4.8c0 2.06-1.35 3.73-3.41 3.73-2.07 0-3.63-1.67-3.63-3.73 0-2.15.74-4.8.74-4.8C5.26 3.12 2 7.05 2 12c0 5.52 4.48 10 10 10s10-4.48 10-10c0-4.95-3.26-8.88-7.5-11.33z", fallback: "๐Ÿ”ฅ" }, + { id: "check", name: "Check", path: "M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z", fallback: "โœ“" }, + { id: "cross", name: "Cross", path: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z", fallback: "โœ•" }, + { id: "bell", name: "Bell", path: "M18 16v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z", fallback: "๐Ÿ””" }, + { id: "pin", name: "Pin", path: "M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2l-2-2z", fallback: "๐Ÿ“Œ" }, + { id: "lock", name: "Lock", path: "M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z", fallback: "๐Ÿ”’" }, + { id: "key", name: "Key", path: "M12.65 10a5.998 5.998 0 0 0-6.88-3.88c-2.29.46-4.15 2.29-4.63 4.58A6.006 6.006 0 0 0 7 18a5.99 5.99 0 0 0 5.65-4H17v4h4v-4h2v-4H12.65zM7 14c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z", fallback: "๐Ÿ”‘" }, + { id: "eye", name: "Eye", path: "M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z", fallback: "๐Ÿ‘" }, + { id: "flag", name: "Flag", path: "M14.4 6L14 4H5v17h2v-7h5.6l.4 2h7V6z", fallback: "๐Ÿšฉ" }, + { id: "tag", name: "Tag", path: "M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z", fallback: "๐Ÿท" }, + { id: "badge", name: "Badge", path: "M20.5 6c-2.61.7-5.67 1-8.5 1s-5.89-.3-8.5-1L3 8c1.86.5 4 .83 6 1v13h2v-6h2v6h2V9c2-.17 4.14-.5 6-1l-.5-2zM12 6c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z", fallback: "๐ŸŽ–" }, + { id: "robot", name: "Robot", path: "M20 9V7c0-1.1-.9-2-2-2h-3.2c-.5-1.9-2.2-3.3-4.2-3.3S7.9 3.1 7.4 5H4.2C3 5 2 6 2 7.2v9.6C2 18 3 19 4.2 19h.2v1c0 .6.4 1 1 1h1c.6 0 1-.4 1-1v-1h8v1c0 .6.4 1 1 1h1c.6 0 1-.4 1-1v-1h.2c1.2 0 2.2-1 2.2-2.2V9h-2zm-4 7H8v-2h8v2zm0-4H8V9h8v3z", fallback: "๐Ÿค–" }, + { id: "moon", name: "Moon", path: "M9 2c-1.05 0-2.05.16-3 .46 1.69 1.23 2.8 3.24 2.8 5.54 0 3.87-3.13 7-7 7-1.04 0-2.02-.23-2.9-.64C2.24 18.24 6.36 22 11.5 22c5.5 0 10-4.5 10-10S17 2 11.5 2c-.83 0-1.63.1-2.39.29C9.32 2.1 9.16 2 9 2z", fallback: "๐ŸŒ™" }, + { id: "sun", name: "Sun", path: "M6.76 4.84l-1.8-1.79-1.41 1.41 1.79 1.79 1.42-1.41zM4 10.5H1v2h3v-2zm13-6.9l-1.41 1.41 1.79 1.79 1.42-1.41-1.8-1.79zM20 10.5v2h3v-2h-3zm-8-5c-3.87 0-7 3.13-7 7s3.13 7 7 7 7-3.13 7-7-3.13-7-7-7zm0 12c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm-4 6.91v3h2v-3h-2zm6 0h-2v3h2v-3z", fallback: "โ˜€" }, + { id: "cloud", name: "Cloud", path: "M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96z", fallback: "โ˜" }, + { id: "music", name: "Music", path: "M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z", fallback: "๐ŸŽต" }, + { id: "mic", name: "Mic", path: "M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zM17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z", fallback: "๐ŸŽค" }, + { id: "gamepad", name: "Gamepad", path: "M21 6H3c-1.1 0-2 .9-2 2v8c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm-10 7H8v3H6v-3H3v-2h3V8h2v3h3v2zm4.5 2c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm4-3c-.83 0-1.5-.67-1.5-1.5S18.67 9 19.5 9s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z", fallback: "๐ŸŽฎ" }, + { id: "diamond", name: "Diamond", path: "M19 3H5L2 9l10 12L22 9l-3-6zM7.43 9.14 9.18 5h2.14l1.75 4.14H7.43zm5.47 0h3.9l1.75-4.14h-2.14l-1.75 4.14zM12 17.5l-3.14-4.36h6.28L12 17.5z", fallback: "โ™ฆ" }, + { id: "spade", name: "Spade", path: "M12 2C9 7 4 9.91 4 14c0 2.76 1.79 5.02 4.28 5.83L7 23h10l-1.28-3.17C18.21 19.02 20 16.76 20 14c0-4.09-5-7-8-12z", fallback: "โ™ " }, + { id: "anchor", name: "Anchor", path: "M19 13v6c0 1.1-.9 2-2 2H7c-1.1 0-2-.9-2-2v-6c0-1.1.9-2 2-2h2.2c.55-1.84 2.2-3.15 4.05-3.19.23-1.22 1.12-2.22 2.3-2.5V3h2v2.11c1.18.28 2.07 1.28 2.3 2.5 1.85.04 3.5 1.35 4.05 3.19H17c1.1 0 2 .9 2 2z", fallback: "โš“" }, + { id: "feather", name: "Feather", path: "M20.41 3c-2.04 0-3.92.64-5.48 1.72-.94.66-1.72 1.47-2.32 2.4C11.92 6.41 10.56 6 9.12 6c-2.97 0-5.69 1.66-7.05 4.3l13.93 13.93C18.64 22.59 20.3 19.87 20.3 17c0-1.44-.41-2.8-1.12-3.95.93-.6 1.74-1.38 2.4-2.32C22.66 9.85 23.31 7.97 23.31 6c0-.33-.02-.65-.06-.97l-2.84-2.03z", fallback: "๐Ÿชถ" }, + { id: "leaf", name: "Leaf", path: "M17 8C8 10 5.9 16.17 3.82 21.34l1.89.66.95-2.3c.48.17.98.3 1.34.3C19 20 22 3 22 3c-1 2-3 5-5 5z", fallback: "๐Ÿƒ" }, + { id: "flower", name: "Flower", path: "M12 22c4.97 0 9-4.03 9-9-4.97 0-9-4.03-9-9-4.97 0-9 4.03-9 9 0 4.97 4.03 9 9 9zm0-16c3.87 0 7 3.13 7 7s-3.13 7-7 7-7-3.13-7-7 3.13-7 7-7z", fallback: "โ€" }, + { id: "snowflake", name: "Snowflake", path: "M22 11h-2.65l1.76-1.76-1.41-1.41L17.24 9.88V7h-2v2.88l-2.47-2.47-1.41 1.41L13.35 11H11V8.35l1.76-1.76-1.41-1.41L9.88 6.76V4h-2v2.76L5.41 4.18 4 5.59 5.76 7.35H3v2h2.65l-1.76 1.76 1.41 1.41L6.76 11.12V14h2v-2.88l2.47 2.47 1.41-1.41L10.65 11H13v2.65l-1.76 1.76 1.41 1.41 2.47-2.47V14h2v-2.88l2.47 2.47 1.41-1.41L19.35 13H22v-2z", fallback: "โ„" }, + { id: "zap", name: "Zap", path: "M11 21h-1l1-7H7.5c-.58 0-.57-.32-.38-.66.19-.34.05-.08.07-.12C8.48 10.94 11.07 6 13.5 6c.46 0 .85.33.96.79L15 10h3.5c.49 0 .64.32.45.66-.19.34-.05.08-.07.12C17.52 14.06 15.07 19 13 19l-1 2z", fallback: "โšก" }, + { id: "planet", name: "Planet", path: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z", fallback: "๐Ÿช" }, + { id: "rocket", name: "Rocket", path: "M12 2.5s-4.5 2-6.5 6c-2 4-1 9-1 9s3.5-1 5.5-3v4c0 1.66 1.34 3 3 3s3-1.34 3-3v-4c2 2 5.5 3 5.5 3s1-5-1-9c-2-4-6.5-6-6.5-6zm0 3c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2z", fallback: "๐Ÿš€" }, + { id: "ghost", name: "Ghost", path: "M12 2C7.58 2 4 5.58 4 10v11c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-4h10v4c0 .55.45 1 1 1h1c.55 0 1-.45 1-1V10c0-4.42-3.58-8-8-8zm0 3c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2z", fallback: "๐Ÿ‘ป" }, + { id: "skull", name: "Skull", path: "M12 2c-4.42 0-8 3.58-8 8 0 2.64 1.28 4.99 3.27 6.46L8 21h8l.73-4.54C18.72 14.99 20 12.64 20 10c0-4.42-3.58-8-8-8z", fallback: "๐Ÿ’€" }, + { id: "paw", name: "Paw", path: "M17.5 10.5c.83 0 1.5-.67 1.5-1.5S18.33 7.5 17.5 7.5 16 8.17 16 9s.67 1.5 1.5 1.5zm-11 0c.83 0 1.5-.67 1.5-1.5S7.33 7.5 6.5 7.5 5 8.17 5 9s.67 1.5 1.5 1.5zm5.5 4c-2.28 0-4.32 1.23-5.42 3.05.73 1.34 2.15 2.25 3.79 2.25 1.45 0 2.73-.75 3.48-1.89.37.58.83 1.09 1.37 1.5 1.02.76 2.27 1.18 3.56 1.18 1.46 0 2.8-.52 3.85-1.38C23.2 16.6 20.5 14.5 17.5 14.5c-.83 0-1.61.18-2.32.49-.73-.32-1.53-.49-2.38-.49z", fallback: "๐Ÿพ" }, + { id: "pizza", name: "Pizza", path: "M12 2C8.43 2 5.23 3.54 3.1 6.04 5.34 15.38 12 22 12 22s6.66-6.62 8.9-15.96C18.77 3.54 15.57 2 12 2zm0 15c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm0-5c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z", fallback: "๐Ÿ•" }, + { id: "coffee", name: "Coffee", path: "M18.5 3H6c-1.1 0-2 .9-2 2v5.71c0 3.83 2.95 7.04 6.78 7.29 3.96.26 7.22-3.06 7.22-7v-1h.5c1.93 0 3.5-1.57 3.5-3.5S20.43 3 18.5 3z", fallback: "โ˜•" }, + { id: "cake", name: "Cake", path: "M19 5h-2V3H7v2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm-7 9c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3z", fallback: "๐ŸŽ‚" }, + { id: "gift", name: "Gift", path: "M20 6h-2.18c.11-.31.18-.65.18-1 0-1.66-1.34-3-3-3-1.05 0-1.96.63-2.4 1.5-.44-.87-1.35-1.5-2.4-1.5-1.66 0-3 1.34-3 3 0 .35.07.69.18 1H4c-1.11 0-1.99.89-1.99 2L2 19c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2zm-5-2c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm-7 2h14v2H8V6zm0 4h14v8H8v-8z", fallback: "๐ŸŽ" }, + { id: "trophy", name: "Trophy", path: "M19 5h-2V3H7v2H5c-1.1 0-2 .9-2 2v4c0 1.1.9 2 2 2h2v2H7v2h10v-2h-2v-2h2c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2z", fallback: "๐Ÿ†" }, + { id: "medal", name: "Medal", path: "M20 2H4c-1.1 0-2 .9-2 2v4c0 1.1.9 2 2 2h2v10h2V10h8v10h2V10h2c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zM4 6h4v2H4V6zm16 0h-4v2h4V6z", fallback: "๐Ÿ…" }, + { id: "lightbulb", name: "Lightbulb", path: "M9 21c0 .55.45 1 1 1h4c.55 0 1-.45 1-1v-1H9v1zm3-19C8.14 2 5 5.14 5 9c0 2.38 1.19 4.47 3 5.74V17c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-2.26c1.81-1.27 3-3.36 3-5.74 0-3.86-3.14-7-7-7zm2.85 11.1l-.85.6V16h-4v-2.3l-.85-.6C7.8 12.16 7 10.63 7 9c0-2.76 2.24-5 5-5s5 2.24 5 5c0 1.63-.8 3.16-2.15 4.1z", fallback: "๐Ÿ’ก" }, + { id: "battery", name: "Battery", path: "M15.67 4H14V2h-4v2H8.33C7.6 4 7 4.6 7 5.33v15.33C7 21.4 7.6 22 8.33 22h7.33c.74 0 1.34-.6 1.34-1.33V5.33C17 4.6 16.4 4 15.67 4z", fallback: "๐Ÿ”‹" }, + { id: "wifi", name: "Wifi", path: "M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.08 2.93 1 9zm8 8l3 3 3-3a4.237 4.237 0 0 0-6 0zm-4-4l2 2a7.074 7.074 0 0 1 10 0l2-2C15.14 9.14 8.87 9.14 5 13z", fallback: "๐Ÿ“ถ" }, + { id: "code", name: "Code", path: "M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z", fallback: "" }, + { id: "terminal", name: "Terminal", path: "M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 14H4V6h16v12zM7.5 15l4.5-3-4.5-3v6zM13 15h5v-2h-5v2z", fallback: "โŒ˜" }, + { id: "bug", name: "Bug", path: "M19 8h-1.81a5.98 5.98 0 0 0-1.19-2.56l1.5-1.5-1.41-1.41-1.5 1.5A5.96 5.96 0 0 0 13 3.06V1h-2v2.06A5.96 5.96 0 0 0 8.91 4.03l-1.5-1.5-1.41 1.41 1.5 1.5A5.98 5.98 0 0 0 6.81 8H5v2h1.09c-.05.33-.09.66-.09 1v1H5v2h1v1c0 .34.04.67.09 1H5v2h1.81c1.04 2.95 3.86 5 7.19 5s6.15-2.05 7.19-5H19v-2h-1.09c.05-.33.09-.66.09-1v-1h1v-2h-1v-1c0-.34-.04-.67-.09-1H19V8zm-4 9c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3z", fallback: "๐Ÿ›" }, + { id: "search", name: "Search", path: "M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z", fallback: "๐Ÿ”" }, + { id: "home", name: "Home", path: "M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z", fallback: "โŒ‚" }, + { id: "mail", name: "Mail", path: "M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z", fallback: "โœ‰" }, + { id: "phone", name: "Phone", path: "M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17 0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2z", fallback: "๐Ÿ“ž" }, + { id: "pen", name: "Pen", path: "M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z", fallback: "โœŽ" }, + { id: "trash", name: "Trash", path: "M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z", fallback: "๐Ÿ—‘" }, + { id: "clock", name: "Clock", path: "M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v7l5.25 3.15.75-1.23-4.5-2.67z", fallback: "๐Ÿ•" }, + { id: "calendar", name: "Calendar", path: "M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM9 10H7v2h2v-2zm0 4H7v2h2v-2zm0-8H7v2h2V6zm4 0h-2v2h2V6zm0 8h-2v2h2v-2zm0-4h-2v2h2v-2z", fallback: "๐Ÿ“…" }, + { id: "globe", name: "Globe", path: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z", fallback: "๐ŸŒ" }, + { id: "cog", name: "Cog", path: "M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.488.488 0 0 0-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 0 0-.48-.42h-3.84a.484.484 0 0 0-.48.42l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96a.488.488 0 0 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .43-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z", fallback: "โš™" }, + { id: "megaphone", name: "Megaphone", path: "M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z", fallback: "๐Ÿ“ฃ" }, + { id: "cart", name: "Cart", path: "M7 18c-1.1 0-1.99.9-1.99 2S5.9 22 7 22s2-.9 2-2-.9-2-2-2zM1 2v2h2l3.6 7.59-1.35 2.45C4.52 15.37 5.48 17 7 17h12v-2H7.42c-.14 0-.25-.11-.21-.26l.04-.15L8.1 13h7.45c.75 0 1.41-.41 1.75-1.03l3.58-6.49A1.003 1.003 0 0 0 20 4H5.21l-.94-2H1zm16 16c-1.1 0-1.99.9-1.99 2s.89 2 1.99 2 2-.9 2-2-.9-2-2-2z", fallback: "๐Ÿ›’" }, + { id: "basket", name: "Basket", path: "M17.21 9l-4.38-6.56a.993.993 0 0 0-1.66 0L6.79 9H2v2h3.05l1.65 8.33A2.01 2.01 0 0 0 8.68 21h6.64c.97 0 1.81-.71 1.96-1.67L18.95 11H22V9h-4.79zM10 5.74 13.76 10H6.24L10 5.74z", fallback: "๐Ÿงบ" }, + { id: "umbrella", name: "Umbrella", path: "M12 2c-5.33 4.55-8 8.48-8 11.8 0 4.98 3.8 8.2 8 8.2s8-3.22 8-8.2C20 10.48 17.33 6.55 12 2zm0 18c-2.21 0-4.39-1.79-4.39-4.38 0-2.22 1.4-4.38 4.39-4.38s4.39 2.16 4.39 4.38c0 2.59-2.18 4.38-4.39 4.38z", fallback: "โ˜‚" }, + { id: "anchor2", name: "Anchor 2", path: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z", fallback: "โ—Ž" } +]; + +export const ICONS_BY_ID: Record = Object.fromEntries(ICONS.map((i) => [i.id, i])); + +export function getIcon(id: string | undefined): IconDef | undefined { + if (!id || id === "none") return undefined; + return ICONS_BY_ID[id]; +} diff --git a/plugins/custom-user-tags/src/lib/resolveTag.ts b/plugins/custom-user-tags/src/lib/resolveTag.ts index a2d6625..50201d5 100644 --- a/plugins/custom-user-tags/src/lib/resolveTag.ts +++ b/plugins/custom-user-tags/src/lib/resolveTag.ts @@ -2,19 +2,29 @@ import { chroma } from "@vendetta/metro/common"; import { rawColors } from "@vendetta/ui"; import { isValidHex } from "@shared/lib/color"; import { getUserTag } from "./tags"; +import { getIcon, IconDef } from "./icons"; export interface ResolvedTag { text: string; textColor: any; backgroundColor: string; + icon?: IconDef; + iconColor: any; } export default function resolveTag(userId: string | undefined): ResolvedTag | undefined { const tag = getUserTag(userId); - if (!tag?.text || !isValidHex(tag.color)) return undefined; + if ((!tag?.text && !tag?.icon) || !isValidHex(tag.color)) return undefined; const backgroundColor = tag.color; const textColor = chroma(backgroundColor).get("lab.l") < 70 ? rawColors.WHITE_500 : rawColors.BLACK_500; + const icon = getIcon(tag.icon); - return { text: tag.text, textColor, backgroundColor }; + return { + text: tag.text || "", + textColor, + backgroundColor, + icon, + iconColor: textColor + }; } diff --git a/plugins/custom-user-tags/src/lib/tags.ts b/plugins/custom-user-tags/src/lib/tags.ts index 57a1e92..72d79c6 100644 --- a/plugins/custom-user-tags/src/lib/tags.ts +++ b/plugins/custom-user-tags/src/lib/tags.ts @@ -3,6 +3,7 @@ import { storage } from "@vendetta/plugin"; export interface UserTag { text: string; color: string; + icon?: string; } export function allTags(): Record { diff --git a/plugins/custom-user-tags/src/patches/chat.ts b/plugins/custom-user-tags/src/patches/chat.ts index 2ed25cf..eb5ba8a 100644 --- a/plugins/custom-user-tags/src/patches/chat.ts +++ b/plugins/custom-user-tags/src/patches/chat.ts @@ -17,9 +17,15 @@ export default () => { const tag = resolveTag(message?.author?.id); if (!tag) return; + const tagText = tag.icon + ? tag.text + ? `${tag.icon.fallback} ${tag.text}` + : tag.icon.fallback + : tag.text; + return { ...ret, - tagText: tag.text, + tagText, tagTextColor: tag.textColor ? ReactNative.processColor(chroma(tag.textColor).hex()) : undefined, tagBackgroundColor: ReactNative.processColor(chroma(tag.backgroundColor).hex()), tagVerified: false, diff --git a/plugins/custom-user-tags/src/patches/details.tsx b/plugins/custom-user-tags/src/patches/details.tsx index b71100b..8afb9c1 100644 --- a/plugins/custom-user-tags/src/patches/details.tsx +++ b/plugins/custom-user-tags/src/patches/details.tsx @@ -42,6 +42,8 @@ const rowPatch = ([{ user }]: any[], res: any) => { text: tag.text, textColor: tag.textColor, backgroundColor: tag.backgroundColor, + icon: tag.icon, + iconColor: tag.iconColor, verified: false }); return; @@ -58,6 +60,8 @@ const rowPatch = ([{ user }]: any[], res: any) => { text={tag.text} textColor={tag.textColor} backgroundColor={tag.backgroundColor} + icon={tag.icon} + iconColor={tag.iconColor} verified={false} /> ); diff --git a/plugins/custom-user-tags/src/patches/name.tsx b/plugins/custom-user-tags/src/patches/name.tsx index 149eef6..245adbe 100644 --- a/plugins/custom-user-tags/src/patches/name.tsx +++ b/plugins/custom-user-tags/src/patches/name.tsx @@ -28,6 +28,8 @@ export default () => { text: tag.text, textColor: tag.textColor, backgroundColor: tag.backgroundColor, + icon: tag.icon, + iconColor: tag.iconColor, verified: false }; return; @@ -43,6 +45,8 @@ export default () => { text={tag.text} textColor={tag.textColor} backgroundColor={tag.backgroundColor} + icon={tag.icon} + iconColor={tag.iconColor} verified={false} /> ); diff --git a/plugins/custom-user-tags/src/patches/profile.tsx b/plugins/custom-user-tags/src/patches/profile.tsx index e0d3b99..66fdfda 100644 --- a/plugins/custom-user-tags/src/patches/profile.tsx +++ b/plugins/custom-user-tags/src/patches/profile.tsx @@ -41,6 +41,8 @@ export default function patchProfile(): () => void { text: tag.text, textColor: tag.textColor, backgroundColor: tag.backgroundColor, + icon: tag.icon, + iconColor: tag.iconColor, verified: false }); return; @@ -60,6 +62,8 @@ export default function patchProfile(): () => void { text={tag.text} textColor={tag.textColor} backgroundColor={tag.backgroundColor} + icon={tag.icon} + iconColor={tag.iconColor} verified={false} /> ); diff --git a/plugins/custom-user-tags/src/patches/tag.tsx b/plugins/custom-user-tags/src/patches/tag.tsx index 5e4f0a8..13f14b0 100644 --- a/plugins/custom-user-tags/src/patches/tag.tsx +++ b/plugins/custom-user-tags/src/patches/tag.tsx @@ -1,23 +1,37 @@ +import { React, ReactNative } from "@vendetta/metro/common"; import { findByProps } from "@vendetta/metro"; import { after } from "@vendetta/patcher"; import { findInReactTree } from "@vendetta/utils"; +import Icon from "../ui/Icon"; const Tag = findByProps("getBotLabel"); +const { View, Text } = ReactNative; /** * Discord's own Tag component ignores custom text/color props unless we push them into the - * already-rendered tree ourselves - this is what actually makes our overrides visible. Identical to - * Staff Tags' own copy of this patch - fully generic, doesn't care which plugin computed the tag. + * already-rendered tree ourselves - this is what actually makes our overrides visible. Now also + * supports an `icon` prop (an IconDef from our icon library) and `iconColor` so tags can render + * with or without text. */ export default () => { if (!Tag) return () => {}; - return after("default", Tag, ([{ text, textColor, backgroundColor }], ret) => { + return after("default", Tag, ([{ text, textColor, backgroundColor, icon, iconColor }], ret) => { const label = findInReactTree(ret, (c) => typeof c?.props?.children === "string"); if (!label) return; - if (text) label.props.children = text; + if (text != null) label.props.children = text; if (textColor) label.props.style?.push?.({ color: textColor }); if (backgroundColor) ret?.props?.style?.push?.({ backgroundColor }); + + if (icon?.path) { + const style = label.props.style ?? {}; + ret.props.children = ( + + + {text != null ? text : label.props.children} + + ); + } }); }; diff --git a/plugins/custom-user-tags/src/ui/ColorInput.tsx b/plugins/custom-user-tags/src/ui/ColorInput.tsx new file mode 100644 index 0000000..cc6f3ed --- /dev/null +++ b/plugins/custom-user-tags/src/ui/ColorInput.tsx @@ -0,0 +1,264 @@ +import { React, ReactNative } from "@vendetta/metro/common"; +import { isValidHex, normalizeHex } from "@shared/lib/color"; +import { resolveSemanticColorSafe } from "@shared/lib/color"; + +const { View, Text, TextInput, TouchableOpacity } = ReactNative; + +interface ColorInputProps { + title: string; + value?: string; + placeholder?: string; + onChange: (value: string) => void; +} + +const PRESETS = [ + "#F23F42", "#F0B232", "#FEE75C", "#23A55A", + "#1ABC9C", "#5865F2", "#9B59B6", "#EB459E", + "#99AAB5", "#FFFFFF", + "#FF6B6B", "#FF9F43", "#FDCB6E", "#00B894", + "#00CEC9", "#0984E3", "#6C5CE7", "#FD79A8", + "#A29BFE", "#DFE6E9", "#636E72", "#2D3436", + "#E17055", "#D63031", "#74B9FF", "#A8E6CF", + "#FDFFAB", "#FFB7B2", "#FF8B94", "#C7CEEA", + "#FF0000", "#00FF00", "#0000FF", "#FFFF00", + "#00FFFF", "#FF00FF", "#000000", "#808080" +]; + +function clamp(n: number, min: number, max: number) { + return Math.max(min, Math.min(max, Math.round(n))); +} + +function hexToRgb(hex: string): [number, number, number] { + const full = normalizeHex(hex).slice(1); + return [ + parseInt(full.slice(0, 2), 16), + parseInt(full.slice(2, 4), 16), + parseInt(full.slice(4, 6), 16) + ]; +} + +function rgbToHex(r: number, g: number, b: number): string { + return "#" + [r, g, b].map((n) => clamp(n, 0, 255).toString(16).padStart(2, "0")).join(""); +} + +function rgbToHsv(r: number, g: number, b: number): [number, number, number] { + r /= 255; g /= 255; b /= 255; + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + const d = max - min; + let h = 0; + if (d !== 0) { + if (max === r) h = ((g - b) / d + 6) % 6; + else if (max === g) h = (b - r) / d + 2; + else h = (r - g) / d + 4; + h *= 60; + } + return [h, max === 0 ? 0 : d / max, max]; +} + +function hsvToRgb(h: number, s: number, v: number): [number, number, number] { + const c = v * s; + const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); + const m = v - c; + let [r, g, b] = [0, 0, 0]; + if (h < 60) [r, g, b] = [c, x, 0]; + else if (h < 120) [r, g, b] = [x, c, 0]; + else if (h < 180) [r, g, b] = [0, c, x]; + else if (h < 240) [r, g, b] = [0, x, c]; + else if (h < 300) [r, g, b] = [x, 0, c]; + else [r, g, b] = [c, 0, x]; + return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)]; +} + +function rainbow(count: number): string[] { + return Array.from({ length: count }, (_, i) => rgbToHex(...hsvToRgb((i * 360) / count, 1, 1))); +} + +function shades(base: string, count: number): string[] { + const [h, s, v] = rgbToHsv(...hexToRgb(base)); + return Array.from({ length: count }, (_, i) => rgbToHex(...hsvToRgb(h, s, 0.2 + (0.8 * i) / (count - 1)))); +} + +function randomHex(): string { + return rgbToHex(Math.floor(Math.random() * 256), Math.floor(Math.random() * 256), Math.floor(Math.random() * 256)); +} + +export default function ColorInput({ title, value, placeholder, onChange }: ColorInputProps) { + const normalized = value ? normalizeHex(value) : ""; + const valid = !value || isValidHex(value); + const swatchColor = isValidHex(value) ? normalized : (isValidHex(placeholder) ? normalizeHex(placeholder) : "#5865F2"); + const [r, g, b] = hexToRgb(swatchColor); + const textColor = resolveSemanticColorSafe(["TEXT_NORMAL", "TEXT_DEFAULT"], "#dbdee1"); + + const updateHex = (text: string) => { + const t = text.trim().toLowerCase(); + if (!t) { + onChange(""); + return; + } + if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(t)) { + onChange(t.toUpperCase()); + } else { + onChange(text); + } + }; + + const updateRgb = (component: "r" | "g" | "b", raw: string) => { + const num = parseInt(raw, 10); + if (Number.isNaN(num)) return; + const next = [r, g, b]; + if (component === "r") next[0] = num; + else if (component === "g") next[1] = num; + else next[2] = num; + onChange(rgbToHex(...next as [number, number, number])); + }; + + const hueColors = rainbow(12); + const shadeColors = shades(swatchColor, 6); + + return ( + + + + {title} + + + + + {valid && ( + + {(["r", "g", "b"] as const).map((c, i) => ( + + {c} + updateRgb(c, v)} + style={{ + borderWidth: 1, + borderColor: "rgba(128,128,128,0.35)", + borderRadius: 6, + paddingHorizontal: 8, + paddingVertical: 6, + fontSize: 13, + color: textColor + }} + /> + + ))} + + )} + + {valid && ( + + {shadeColors.map((color) => ( + onChange(color.toUpperCase())} + style={{ + width: 28, + height: 28, + borderRadius: 6, + backgroundColor: color, + marginRight: 6, + marginBottom: 6, + borderWidth: value?.toLowerCase() === color.toLowerCase() ? 2 : 1, + borderColor: value?.toLowerCase() === color.toLowerCase() ? "#ffffff" : "rgba(128,128,128,0.35)" + }} + /> + ))} + + )} + + {valid && ( + + {hueColors.map((color) => ( + onChange(color.toUpperCase())} + style={{ + width: 28, + height: 28, + borderRadius: 14, + backgroundColor: color, + marginRight: 6, + marginBottom: 6, + borderWidth: value?.toLowerCase() === color.toLowerCase() ? 2 : 1, + borderColor: value?.toLowerCase() === color.toLowerCase() ? "#ffffff" : "rgba(128,128,128,0.35)" + }} + /> + ))} + + )} + + onChange(randomHex())} + style={{ + paddingVertical: 8, + paddingHorizontal: 10, + borderRadius: 8, + backgroundColor: "rgba(88,101,242,0.2)", + alignItems: "center", + marginBottom: 8 + }} + > + Random color + + + + {PRESETS.map((preset) => { + const active = value?.toLowerCase() === preset.toLowerCase(); + return ( + onChange(preset)} + style={{ + width: 24, + height: 24, + borderRadius: 6, + backgroundColor: preset, + marginRight: 8, + marginBottom: 8, + borderWidth: active ? 2 : 1, + borderColor: active ? "#ffffff" : "rgba(128,128,128,0.35)" + }} + /> + ); + })} + + + {!valid && ( + + Invalid hex color, e.g. #5865F2 + + )} + + ); +} diff --git a/plugins/custom-user-tags/src/ui/Icon.tsx b/plugins/custom-user-tags/src/ui/Icon.tsx new file mode 100644 index 0000000..4a616da --- /dev/null +++ b/plugins/custom-user-tags/src/ui/Icon.tsx @@ -0,0 +1,24 @@ +import { React } from "@vendetta/metro/common"; +import { findByProps } from "@vendetta/metro"; +import { IconDef } from "../lib/icons"; + +const svgModule = findByProps("Svg") as Record | undefined; +const Svg = svgModule?.Svg ?? (() => null); +const Path = svgModule?.Path ?? (() => null); + +interface IconProps { + icon: IconDef; + size?: number; + color?: string; + style?: any; +} + +export default function Icon({ icon, size = 12, color = "#fff", style }: IconProps) { + if (!icon.path) return null; + const viewBox = icon.viewBox ?? "0 0 24 24"; + return ( + + + + ); +} diff --git a/plugins/custom-user-tags/src/ui/IconPicker.tsx b/plugins/custom-user-tags/src/ui/IconPicker.tsx new file mode 100644 index 0000000..96068a0 --- /dev/null +++ b/plugins/custom-user-tags/src/ui/IconPicker.tsx @@ -0,0 +1,57 @@ +import { React, ReactNative } from "@vendetta/metro/common"; +import { resolveSemanticColorSafe } from "@shared/lib/color"; +import { ICONS } from "../lib/icons"; +import Icon from "./Icon"; + +const { View, Text, TouchableOpacity, ScrollView } = ReactNative; + +interface IconPickerProps { + title?: string; + value?: string; + onChange: (value: string) => void; + color?: string; +} + +export default function IconPicker({ title = "Icon", value, onChange, color = "#5865F2" }: IconPickerProps) { + const textColor = resolveSemanticColorSafe(["TEXT_NORMAL", "TEXT_DEFAULT"], "#dbdee1"); + const iconColor = resolveSemanticColorSafe(["TEXT_NORMAL", "TEXT_DEFAULT"], "#dbdee1"); + + return ( + + {title} + + + {ICONS.map((icon) => { + const active = value === icon.id; + return ( + onChange(icon.id)} + style={{ + width: 40, + height: 40, + borderRadius: 8, + alignItems: "center", + justifyContent: "center", + marginRight: 8, + backgroundColor: active ? color : "rgba(128,128,128,0.15)", + borderWidth: active ? 2 : 1, + borderColor: active ? "#ffffff" : "rgba(128,128,128,0.35)" + }} + > + {icon.path ? ( + + ) : ( + โ€” + )} + + ); + })} + + + + {ICONS.find((i) => i.id === value)?.name ?? "No icon"} + + + ); +} diff --git a/plugins/custom-user-tags/src/ui/Settings.tsx b/plugins/custom-user-tags/src/ui/Settings.tsx index e7f280d..c3d9cf6 100644 --- a/plugins/custom-user-tags/src/ui/Settings.tsx +++ b/plugins/custom-user-tags/src/ui/Settings.tsx @@ -5,10 +5,12 @@ import { TableRowGroup, TextInput } from "@shared/ui/table"; import { showToast } from "@vendetta/ui/toasts"; import SettingsScaffold from "@shared/ui/SettingsScaffold"; import ListSection from "@shared/ui/ListSection"; -import ColorInput from "@shared/ui/ColorInput"; import PrimaryButton from "@shared/ui/PrimaryButton"; import NoteBox from "@shared/ui/NoteBox"; import { allTags, setUserTag, removeUserTag } from "../lib/tags"; +import { getIcon } from "../lib/icons"; +import ColorInput from "./ColorInput"; +import IconPicker from "./IconPicker"; const { View } = ReactNative; @@ -16,7 +18,8 @@ function AddTagForm() { const [userId, setUserId] = React.useState(""); const [text, setText] = React.useState(""); const [color, setColor] = React.useState("#5865F2"); - const canSave = !!userId.trim() && !!text.trim(); + const [icon, setIcon] = React.useState("none"); + const canSave = !!userId.trim() && (!!text.trim() || icon !== "none"); return ( @@ -26,17 +29,19 @@ function AddTagForm() { value={userId} onChange={setUserId} /> - + + { - setUserTag(userId.trim(), { text: text.trim(), color }); + setUserTag(userId.trim(), { text: text.trim(), color, icon: icon === "none" ? undefined : icon }); showToast(`Tagged user ${userId.trim()}`, undefined); setUserId(""); setText(""); + setIcon("none"); }} /> @@ -62,15 +67,19 @@ export default function Settings() { ({ - key: id, - label: tags[id].text, - subLabel: `${id} โ€ข Tap to remove`, - onPress: () => { - removeUserTag(id); - showToast("Removed tag", undefined); - } - }))} + items={userIds.map((id) => { + const icon = getIcon(tags[id].icon)?.fallback; + const label = icon ? `${icon} ${tags[id].text}`.trim() : tags[id].text; + return { + key: id, + label, + subLabel: `${id} โ€ข Tap to remove`, + onPress: () => { + removeUserTag(id); + showToast("Removed tag", undefined); + } + }; + })} /> ); diff --git a/plugins/custom-user-tags/src/ui/Tag.tsx b/plugins/custom-user-tags/src/ui/Tag.tsx new file mode 100644 index 0000000..b686e94 --- /dev/null +++ b/plugins/custom-user-tags/src/ui/Tag.tsx @@ -0,0 +1,29 @@ +import { React, ReactNative } from "@vendetta/metro/common"; +import { ResolvedTag } from "../lib/resolveTag"; +import Icon from "./Icon"; + +const { View, Text } = ReactNative; + +const baseStyle = { + flexDirection: "row", + alignItems: "center", + borderRadius: 4, + paddingHorizontal: 4, + paddingVertical: 2, + marginLeft: 4, + overflow: "hidden" +}; + +interface TagProps { + tag: ResolvedTag; + style?: any; +} + +export default function CustomTag({ tag, style }: TagProps) { + return ( + + {tag.icon && tag.icon.path && } + {!!tag.text && {tag.text}} + + ); +} diff --git a/plugins/custom-user-tags/src/ui/TagEditorAlert.tsx b/plugins/custom-user-tags/src/ui/TagEditorAlert.tsx index de179c8..299b87a 100644 --- a/plugins/custom-user-tags/src/ui/TagEditorAlert.tsx +++ b/plugins/custom-user-tags/src/ui/TagEditorAlert.tsx @@ -2,7 +2,8 @@ import { React, ReactNative } from "@vendetta/metro/common"; import { findByProps } from "@vendetta/metro"; import { TextInput } from "@shared/ui/table"; import { showToast } from "@vendetta/ui/toasts"; -import ColorInput from "@shared/ui/ColorInput"; +import ColorInput from "./ColorInput"; +import IconPicker from "./IconPicker"; import { getUserTag, setUserTag, removeUserTag } from "../lib/tags"; const { View } = ReactNative; @@ -28,16 +29,17 @@ function TagEditor({ userId, username }: { userId: string; username: string }) { const existing = getUserTag(userId); const [text, setText] = React.useState(existing?.text ?? ""); const [color, setColor] = React.useState(existing?.color ?? "#5865F2"); + const [icon, setIcon] = React.useState(existing?.icon ?? "none"); const { dismissAlert, AlertModal, AlertActions, AlertActionButton } = alertParts(); if (!AlertModal || !AlertActions || !AlertActionButton) return null; const save = () => { - if (!text.trim()) { - showToast("Enter some tag text first", undefined); + if (!text.trim() && icon === "none") { + showToast("Enter some tag text or choose an icon first", undefined); return; } - setUserTag(userId, { text: text.trim(), color }); + setUserTag(userId, { text: text.trim(), color, icon: icon === "none" ? undefined : icon }); showToast(`Tagged ${username}`, undefined); dismissAlert?.(ALERT_KEY); }; @@ -54,6 +56,7 @@ function TagEditor({ userId, username }: { userId: string; username: string }) { content={ + }