├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── pull_request_template.md ├── .gitignore ├── .netlify └── state.json ├── .storybook ├── main.js └── preview.js ├── .vscode └── settings.json ├── .yarn └── releases │ └── yarn-3.6.4.cjs ├── .yarnrc.yml ├── LICENSE ├── README.md ├── __snapshots__ └── tests │ └── visual.spec.tsx-snapshots │ ├── Visual-comparisons-example-test-1-chromium-win32.png │ ├── Visual-comparisons-example-test-1-firefox-win32.png │ └── Visual-comparisons-example-test-1-webkit-win32.png ├── index.html ├── netlify.toml ├── package-lock.json ├── package.json ├── src ├── App.css ├── App.tsx ├── component │ ├── CheckBoxListItem.tsx │ ├── ClipboardButton.tsx │ ├── ColorPickerListItem.tsx │ ├── CssMaker.tsx │ ├── CssString.tsx │ ├── Discord-icon.svg │ ├── Discord.css │ ├── DiscordIconPreview.tsx │ ├── InputArea.tsx │ ├── InputUserIdForm.tsx │ ├── SelectorListItem.tsx │ ├── SelectorToggleButtonGroup.tsx │ ├── SliderListItem.tsx │ ├── TutorialButton.tsx │ ├── animation.css │ ├── img │ │ ├── 1_add_browser.png │ │ ├── 1_obs_empty.png │ │ ├── 2_Discord-StreamKit-Overlay.png │ │ ├── Discord-start-icon.png │ │ ├── copy-css.png │ │ ├── discord-setting-detail.jpg │ │ ├── discord-url.png │ │ ├── discord-user-setting.jpg │ │ ├── id_copy.jpg │ │ ├── obs-complete.png │ │ └── obs-css.png │ └── stories │ │ ├── ClipboardButton.stories.tsx │ │ ├── CssString.stories.tsx │ │ ├── DiscordIconPreview.stories.tsx │ │ ├── SelectorListItem.stories.tsx │ │ ├── SliderListItem.stories.tsx │ │ └── TutorialButton.stories.tsx ├── configs │ └── i18n.ts ├── favicon.ico ├── index.css ├── lib │ ├── cssObj.ts │ ├── cssText.ts │ └── getCssKeyFrames.ts ├── main.tsx ├── theme.ts ├── translations │ ├── chinese_cs.ts │ ├── chinese_ct.ts │ ├── english.ts │ ├── index.ts │ └── japanese.ts └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── yarn.lock /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | Please include changes, motivations and which issues are fixed. 4 | 5 | # How did you test this change? 6 | 7 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | !.vscode/settings.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | /test-results/ 27 | /playwright-report/ 28 | /playwright/.cache/ 29 | /test-results/ 30 | /playwright-report/ 31 | /playwright/.cache/ 32 | cypress/support/customCommands.d.ts 33 | .yarn/* 34 | !.yarn/patches 35 | !.yarn/plugins 36 | !.yarn/releases 37 | !.yarn/sdks 38 | !.yarn/versions 39 | /test-results/ 40 | /playwright-report/ 41 | /blob-report/ 42 | /playwright/.cache/ 43 | -------------------------------------------------------------------------------- /.netlify/state.json: -------------------------------------------------------------------------------- 1 | { 2 | "siteId": "cb99a370-7d7e-4662-8887-ac3779a42b6e" 3 | } -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "stories": [ 3 | "../src/**/*.stories.mdx", 4 | "../src/**/*.stories.@(js|jsx|ts|tsx)" 5 | ], 6 | "addons": [ 7 | "@storybook/addon-links", 8 | "@storybook/addon-essentials", 9 | "@storybook/addon-interactions" 10 | ], 11 | "framework": "@storybook/react", 12 | typescript: { 13 | check: false, 14 | checkOptions: {}, 15 | reactDocgen: 'react-docgen-typescript', 16 | reactDocgenTypescriptOptions: { 17 | shouldExtractLiteralValuesFromEnum: true, 18 | propFilter: (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true), 19 | }, 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | export const parameters = { 2 | actions: { argTypesRegex: "^on[A-Z].*" }, 3 | controls: { 4 | matchers: { 5 | color: /(background|color)$/i, 6 | date: /Date$/, 7 | }, 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "docgen", 4 | "GIGAZINE", 5 | "niconico", 6 | "reactid", 7 | "vite", 8 | "vitejs" 9 | ] 10 | } -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | yarnPath: .yarn/releases/yarn-3.6.4.cjs 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 alfe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OBSのDiscordアイコン外観変更ジェネレーター 2 | 3 | Discordで通話中のメンバーをOBS Studioに表示するときに、横並びにしたりアイコンを四角にしたりするためのカスタムCSSをつくるジェネレーター 4 | Generator to create custom CSS for displaying members of Discord call in OBS Studio, such as jumping, side-by-side or square icons. 5 | 6 | 7 | ## Web app 8 | ![obs-discord-icon alfebelow com_](https://user-images.githubusercontent.com/1934979/160101965-63418f24-59ed-4265-9b7d-2ad93cc11da4.png) 9 | 10 | https://obs-discord-icon.alfebelow.com/ 11 | 12 | ## Wake up app 13 | 14 | ```shell 15 | > yarn 16 | > yarn start 17 | ``` 18 | 19 | ## Storybook 20 | https://www.chromatic.com/library?appId=622f09369f85e7003adefe61 21 | -------------------------------------------------------------------------------- /__snapshots__/tests/visual.spec.tsx-snapshots/Visual-comparisons-example-test-1-chromium-win32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfe/obs-discord-icons-css-generator/d89b320305ceeee54956c029ddd6214c8e151e7d/__snapshots__/tests/visual.spec.tsx-snapshots/Visual-comparisons-example-test-1-chromium-win32.png -------------------------------------------------------------------------------- /__snapshots__/tests/visual.spec.tsx-snapshots/Visual-comparisons-example-test-1-firefox-win32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfe/obs-discord-icons-css-generator/d89b320305ceeee54956c029ddd6214c8e151e7d/__snapshots__/tests/visual.spec.tsx-snapshots/Visual-comparisons-example-test-1-firefox-win32.png -------------------------------------------------------------------------------- /__snapshots__/tests/visual.spec.tsx-snapshots/Visual-comparisons-example-test-1-webkit-win32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfe/obs-discord-icons-css-generator/d89b320305ceeee54956c029ddd6214c8e151e7d/__snapshots__/tests/visual.spec.tsx-snapshots/Visual-comparisons-example-test-1-webkit-win32.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | OBSのDiscordアイコン外観変更ジェネレーター 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 27 | 28 | 29 |
30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/*" 3 | to = "/index.html" 4 | status = 200 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obs-discord-icons-css-generator", 3 | "version": "1.1.5", 4 | "description": "Generator to create custom CSS for displaying members of Discord call in OBS Studio, such as jumping, side-by-side or square icons.", 5 | "keywords": [ 6 | "obs", 7 | "discord", 8 | "icons", 9 | "css", 10 | "generator" 11 | ], 12 | "author": { 13 | "name": "alfe", 14 | "email": "alfe10251@gmail.com", 15 | "url": "https://alfebelow.com/" 16 | }, 17 | "license": "MIT", 18 | "homepage": "https://obs-discord-icon.alfebelow.com/", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/alfe/obs-discord-icons-css-generator" 22 | }, 23 | "scripts": { 24 | "dev": "vite", 25 | "start": "vite", 26 | "build": "tsc && vite build", 27 | "preview": "vite preview", 28 | "storybook": "start-storybook -p 6006", 29 | "build-storybook": "build-storybook", 30 | "chromatic": "npx chromatic --project-token=75fb6554e7e1 --auto-accept-changes" 31 | }, 32 | "dependencies": { 33 | "@emotion/react": "^11.14.0", 34 | "@emotion/styled": "^11.14.0", 35 | "@mui/icons-material": "7.1.0", 36 | "@mui/material": "7.1.0", 37 | "i18next": "^25.1.3", 38 | "i18next-browser-languagedetector": "^8.1.0", 39 | "lodash.omit": "^4.5.0", 40 | "mui-color-input": "^7.0.0", 41 | "react": "^19.1.0", 42 | "react-copy-to-clipboard": "^5.1.0", 43 | "react-dom": "^19.1.0", 44 | "react-i18next": "^15.5.1" 45 | }, 46 | "devDependencies": { 47 | "@babel/core": "^7.27.1", 48 | "@storybook/addon-actions": "^8.6.12", 49 | "@storybook/addon-essentials": "^8.6.12", 50 | "@storybook/addon-interactions": "^8.6.12", 51 | "@storybook/addon-links": "^8.6.12", 52 | "@storybook/react": "^8.6.12", 53 | "@storybook/testing-library": "^0.2.2", 54 | "@types/lodash": "^4.17.16", 55 | "@types/node": "^22.15.18", 56 | "@types/react": "^19.1.4", 57 | "@types/react-copy-to-clipboard": "5.0.7", 58 | "@types/react-dom": "^19.1.5", 59 | "@vitejs/plugin-react": "^4.4.1", 60 | "babel-loader": "^10.0.0", 61 | "chromatic": "^11.28.2", 62 | "typescript": "^5.8.3", 63 | "vite": "^6.3.5" 64 | }, 65 | "resolutions": { 66 | "@types/react": "^18.2.20" 67 | }, 68 | "packageManager": "yarn@3.6.4" 69 | } 70 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | 2 | .App-content { 3 | --main-bg-color: #282c34; 4 | --main-color: #5865F2; 5 | --main-link-color: #979eeb; 6 | 7 | background-color: var(--main-bg-color); 8 | background: 9 | linear-gradient(63deg, rgba(0,0,0,.1) 23%, transparent 23%) 7px 0, 10 | linear-gradient(63deg, transparent 74%, rgba(0,0,0,.1) 78%), 11 | linear-gradient(63deg, transparent 34%, rgba(0,0,0,.1) 38%, rgba(0,0,0,.1) 58%, transparent 62%), 12 | var(--main-bg-color); 13 | background-size: 16px 48px; 14 | min-height: 100vh; 15 | display: flex; 16 | flex-direction: column; 17 | align-items: center; 18 | font-size: calc(10px + 2vmin); 19 | color: white; 20 | } 21 | 22 | .App-footer { 23 | background-color: var(--main-bg-color); 24 | width: 100%; 25 | text-align: center; 26 | } 27 | 28 | .App-footer p { 29 | font-size: .9rem; 30 | margin: .5rem; 31 | } 32 | 33 | .App-footer { 34 | width: 100%; 35 | padding-top: 2rem; 36 | background: linear-gradient(transparent, 10%, var(--main-bg-color)); 37 | } 38 | .App-footer a { 39 | color: var(--main-link-color); 40 | } 41 | 42 | header { 43 | width: 100%; 44 | background: linear-gradient(var(--main-bg-color), 90%, transparent); 45 | } 46 | header a { 47 | color: var(--main-link-color); 48 | } -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | import { ThemeProvider } from '@mui/material/styles'; 4 | import Box from '@mui/material/Box'; 5 | import Button from '@mui/material/Button'; 6 | import Container from '@mui/material/Container'; 7 | import Typography from '@mui/material/Typography'; 8 | import CssMaker from './component/CssMaker' 9 | import TutorialButton from './component/TutorialButton'; 10 | import theme from './theme'; 11 | import './App.css' 12 | 13 | const App = () => { 14 | return ( 15 | 16 |
17 |
18 | 19 | 20 | 21 |
23 |
24 | ); 25 | }; 26 | export default App 27 | 28 | const Header = () => { 29 | const { t, i18n } = useTranslation("translation", { keyPrefix: "header" }); 30 | const changeLanguage = (language: string) => { 31 | i18n.changeLanguage(language); 32 | location.replace(`${location.origin}/${language === 'ja' ? '' : language}`) 33 | }; 34 | useEffect(() => { 35 | switch (location.pathname) { 36 | case '/en': i18n.changeLanguage('en'); break; 37 | case '/cs': i18n.changeLanguage('cs'); break; 38 | case '/ct': i18n.changeLanguage('ct'); break; 39 | case '/ja': i18n.changeLanguage('ja'); break; 40 | case '': 41 | default: 42 | break; 43 | } 44 | }, []) 45 | 46 | return ( 47 |
48 | 49 | 54 | 59 | 64 | 69 | 74 | 75 | 76 | <>{t("title")} 77 | 78 | 79 | 80 | <>{t("title_anno")} 81 | 82 | 83 | {t('icon_link')} /  84 | {t('text_link')} 85 |
<>{t('news')} 86 |
87 | 88 |
89 |
90 |
91 | ); 92 | }; 93 | 94 | const Footer = () => { 95 | const { t,i18n } = useTranslation("translation", { keyPrefix: "footer" }); 96 | return ( 97 | 128 | ); 129 | }; 130 | -------------------------------------------------------------------------------- /src/component/CheckBoxListItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Box from '@mui/material/Box'; 3 | import FormLabel from '@mui/material/FormLabel'; 4 | import ListItem from '@mui/material/ListItem'; 5 | import Checkbox from '@mui/material/Checkbox'; 6 | 7 | export type CheckBoxListItemProps = { 8 | title: string; 9 | onChange: (value: boolean) => void; 10 | }; 11 | const CheckBoxListItem = ({ title, onChange }: CheckBoxListItemProps) => { 12 | const [value, setValue] = React.useState(true); 13 | const handleChange = (_: any, val: boolean) => { 14 | if (typeof val !== 'boolean') return; 15 | setValue(val); 16 | onChange(val); 17 | }; 18 | return ( 19 | 20 | {title} 21 | 22 | 26 | 27 | 28 | ); 29 | }; 30 | export default CheckBoxListItem; 31 | -------------------------------------------------------------------------------- /src/component/ClipboardButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import CopyToClipboard from 'react-copy-to-clipboard'; 3 | import IconButton from '@mui/material/IconButton'; 4 | import Tooltip from '@mui/material/Tooltip'; 5 | import AssignmentIcon from '@mui/icons-material/Assignment'; 6 | import AssignmentTurnedInIcon from '@mui/icons-material/AssignmentTurnedIn'; 7 | import { grey } from '@mui/material/colors'; 8 | 9 | export type ClipboardButtonProps = { 10 | value: string; 11 | }; 12 | const ClipboardButton = (props: ClipboardButtonProps) => { 13 | const [openTip, setOpenTip] = React.useState(false); 14 | 15 | const handleMouseDownPassword = (event: { preventDefault: () => void; }) => { 16 | event.preventDefault(); 17 | }; 18 | 19 | const handleClickButton = (): void => { 20 | setOpenTip(true); 21 | setTimeout(() => { 22 | setOpenTip(false); 23 | }, 1000); 24 | }; 25 | 26 | return ( 27 | 33 |
34 | 35 | 45 | {openTip ? : } 46 | 47 | 48 |
49 |
50 | ); 51 | } 52 | export default ClipboardButton; 53 | -------------------------------------------------------------------------------- /src/component/ColorPickerListItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | MuiColorInput, 4 | MuiColorInputValue, 5 | MuiColorInputColors, 6 | } from 'mui-color-input' 7 | import Box from '@mui/material/Box'; 8 | import FormLabel from '@mui/material/FormLabel'; 9 | import ListItem from '@mui/material/ListItem'; 10 | import Slider from '@mui/material/Slider'; 11 | 12 | export type ColorPickerListItemProps = { 13 | title: string; 14 | disabled?: boolean; 15 | onChange: (value: string) => void; 16 | defaultValue?: string; 17 | }; 18 | const ColorPickerListItem = ({ title, disabled, onChange, defaultValue }: ColorPickerListItemProps) => { 19 | const [value, setValue] = React.useState(defaultValue || '#ffffff'); 20 | 21 | React.useEffect(() => { 22 | onChange(`${value}`); 23 | }, [disabled]) 24 | 25 | const handleChange = (newValue: string, colors: MuiColorInputColors) => { 26 | setValue(newValue); 27 | onChange(`${newValue}`); 28 | } 29 | 30 | return ( 31 | 32 | {title} 33 | 34 | 38 | 39 | 40 | ); 41 | }; 42 | export default ColorPickerListItem; 43 | -------------------------------------------------------------------------------- /src/component/CssMaker.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useTranslation } from "react-i18next"; 3 | import Box from '@mui/material/Box'; 4 | import Divider from '@mui/material/Divider'; 5 | import Grid from '@mui/material/Grid' 6 | import List from '@mui/material/List' 7 | import cssObj, { setIconSpeakingStyle } from '../lib/cssObj' 8 | import { getCssText } from '../lib/cssText' 9 | import { getCssKeyFrames } from '../lib/getCssKeyFrames'; 10 | import CheckBoxListItem from './CheckBoxListItem'; 11 | import ColorPickerListItem from './ColorPickerListItem'; 12 | import CssString from './CssString'; 13 | import DiscordIconPreview, { CustomStyle } from './DiscordIconPreview' 14 | import InputArea from './InputArea' 15 | import InputUserIdForm from './InputUserIdForm'; 16 | import SelectorListItem from './SelectorListItem' 17 | import SelectorToggleButtonGroup from './SelectorToggleButtonGroup'; 18 | import SliderListItem from './SliderListItem' 19 | 20 | const CssMaker = () => { 21 | const [styles, setStyles] = React.useState({ 22 | voiceContainer: {}, 23 | voiceStates: { 24 | display: 'flex', 25 | flexDirection: 'column', 26 | }, 27 | voiceState: { 28 | display: 'flex', 29 | height: 'initial', 30 | marginBottom: '0', 31 | }, 32 | avatar: { 33 | marginRight: '0', 34 | }, 35 | avatarSpeaking: {}, 36 | name: {}, 37 | }); 38 | 39 | const [alignment, setAlignment] = React.useState('vertical'); 40 | const [activeNamePosition, setActiveNamePosition] = React.useState(true); 41 | const [speakingStyles, setSpeakingStyles] = React.useState(['border']); 42 | const [animationColor, setAnimationColor] = React.useState('#FFFFFF'); 43 | const [hiddenUserId, setHiddenUserId] = React.useState(''); 44 | const { t } = useTranslation("translation", { keyPrefix: "css_maker" }); 45 | console.log(styles) 46 | 47 | return ( 48 | 49 | 50 | 51 | 52 | { 55 | cssObj.iconAlign({val, styles, setStyles}); 56 | setAlignment(val); 57 | }} 58 | options={[ 59 | { value: 'vertical', label: t('vertical') }, 60 | { value: 'horizontal', label: t('horizontal') }, 61 | ]} /> 62 | {alignment === 'horizontal' && ( 63 | 64 | cssObj.iconRowGap({val, styles, setStyles})} /> 69 | cssObj.iconColumnGap({val, styles, setStyles})} /> 74 | 75 | )} 76 | 77 | cssObj.iconShape({val, styles, setStyles})} 80 | options={[ 81 | { value: 'circle', label: t('circle') }, 82 | { value: 'rect-r', label: t('rounded') }, 83 | { value: 'rect', label: t('square') }, 84 | { value: 'tall', label: t('tall') }, 85 | ]} /> 86 | cssObj.iconSize({val, styles, setStyles})} 89 | options={[ 90 | { value: 'normal', label: t('normal') }, 91 | { value: 'lg', label: t('large') }, 92 | { value: 'xg', label: t('huge') }, 93 | ]} /> 94 | 95 | { 98 | setSpeakingStyles(val); 99 | setIconSpeakingStyle({val, animationColor, styles, setStyles}); 100 | }} 101 | options={[ 102 | { value: 'border', label: t('border') }, 103 | { value: 'light', label: t('blinking') }, 104 | { value: 'jump', label: t('jump') }, 105 | ]} /> 106 | 107 | {(speakingStyles.includes('light') || speakingStyles.includes('jump')) && ( 108 | 109 | cssObj.iconSpeakingDuration({val, styles, setStyles})} /> 112 | 113 | )} 114 | 115 | {(speakingStyles.includes('light') || speakingStyles.includes('border')) && ( 116 | 117 | { 121 | setAnimationColor(`${value}`); 122 | setIconSpeakingStyle({ val: speakingStyles, animationColor, styles, setStyles }); 123 | }} /> 124 | 125 | )} 126 | 127 | { 130 | const val = value ? 'exist' : 'hidden'; 131 | cssObj.nameVisibility({val, styles, setStyles}); 132 | setActiveNamePosition(val === 'exist'); 133 | }} /> 134 | {activeNamePosition && ( 135 | 136 | cssObj.nameStyle({val, styles, setStyles})} 139 | disabled={!activeNamePosition} 140 | options={[ 141 | { value: 'blackBk', label: t('black_base') }, 142 | { value: 'bordering', label: t('border') }, 143 | { value: 'none', label: t('text_only') }, 144 | ]} /> 145 | cssObj.namePositionVertical({val, styles, setStyles})} /> 149 | cssObj.namePositionHorizontal({val, styles, setStyles})} /> 153 | 154 | )} 155 | 156 | setHiddenUserId(userId)} /> 159 | 160 | 161 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | ); 173 | }; 174 | export default CssMaker; 175 | 176 | type AnimationStyleProps = { 177 | speakingStyles: string[]; 178 | animationColor: string; 179 | }; 180 | const AnimationStyle = ((props: AnimationStyleProps) => { 181 | if ((props.speakingStyles || []).length === 0 || !props.animationColor) return null; 182 | return ( 183 | <> 184 | ); 185 | }); 186 | -------------------------------------------------------------------------------- /src/component/CssString.tsx: -------------------------------------------------------------------------------- 1 | import OutlinedInput from '@mui/material/OutlinedInput'; 2 | import InputAdornment from '@mui/material/InputAdornment'; 3 | import InputArea from './InputArea' 4 | import ClipboardButton from './ClipboardButton'; 5 | 6 | export type CssStringProps = { 7 | value: string; 8 | }; 9 | const CssString = (props: CssStringProps) => { 10 | return ( 11 | 12 | 17 | 18 | 19 | } 20 | value={props.value} /> 21 | 22 | ); 23 | }; 24 | export default CssString; 25 | -------------------------------------------------------------------------------- /src/component/Discord-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/component/Discord.css: -------------------------------------------------------------------------------- 1 | .voice-container { 2 | font-family: "Catamaran"; 3 | font-weight: 600; 4 | font-size: 16px; 5 | line-height: 19px; 6 | color: #fff 7 | } 8 | 9 | .voice-container .voice-states { 10 | list-style-type: none; 11 | padding-left: 15px 12 | } 13 | 14 | .voice-container .voice-states .voice-state { 15 | height: 50px; 16 | margin-bottom: 8px 17 | } 18 | 19 | .voice-container .voice-states .voice-state .avatar { 20 | height: 50px; 21 | width: 50px; 22 | border: 3px solid transparent; 23 | border-radius: 50%; 24 | float: left; 25 | margin-right: 8px 26 | } 27 | 28 | .voice-container .voice-states .voice-state .avatar.speaking { 29 | border-color: #FFFFFF 30 | } 31 | 32 | .voice-container .voice-states .voice-state .user { 33 | padding-top: 18px 34 | } 35 | 36 | .voice-container .voice-states .voice-state .user .name { 37 | background-color: #1e2124; 38 | border-radius: 3px; 39 | padding: 4px 6px 40 | } 41 | 42 | .voice-container .voice-states .voice-state.small-avatar { 43 | height: 40px 44 | } 45 | 46 | .voice-container .voice-states .voice-state.small-avatar .avatar { 47 | height: 40px; 48 | width: 40px 49 | } 50 | 51 | .voice-container .voice-states .voice-state.small-avatar .user { 52 | padding-top: 12px 53 | } 54 | 55 | 56 | .status-container { 57 | height: 64px; 58 | width: 312px; 59 | font-size: 14px; 60 | background-color: #1e2124; 61 | border-radius: 3px 62 | } 63 | 64 | .status-container .status { 65 | font-family: "Catamaran"; 66 | background-size: 32px 30px; 67 | background-repeat: no-repeat; 68 | background-position: 267px 17px; 69 | padding: 14px 20px 70 | } 71 | 72 | .status-container .status .server-icon { 73 | float: left; 74 | padding-right: 10px 75 | } 76 | 77 | .status-container .status .server-icon img { 78 | height: 35px; 79 | border-radius: 50% 80 | } 81 | 82 | .status-container .status .server-info { 83 | line-height: 16px; 84 | font-weight: 700 85 | } 86 | 87 | .status-container .status .server-info .name { 88 | text-overflow: ellipsis; 89 | max-width: 160px; 90 | display: inline-block; 91 | margin-bottom: -3px; 92 | overflow: hidden 93 | } 94 | 95 | .status-container .status .server-info .online-count { 96 | opacity: .3; 97 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=30)"; 98 | filter: alpha(opacity=30); 99 | text-transform: uppercase; 100 | font-size: 12px; 101 | line-height: 15px; 102 | padding-left: 4px 103 | } 104 | 105 | .status-container .status .invite-link { 106 | font-size: 16px; 107 | line-height: 19px; 108 | font-weight: 600 109 | } 110 | -------------------------------------------------------------------------------- /src/component/DiscordIconPreview.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | import './animation.css' 4 | import './Discord.css' 5 | // https://discord.com/branding 6 | import DiscordIcon from './Discord-icon.svg' 7 | 8 | export type CustomStyle = { 9 | voiceContainer?: { [key: string]: string }, 10 | voiceStates?: { [key: string]: string }; 11 | voiceState?: { [key: string]: string }; 12 | avatar?: { [key: string]: string }; 13 | avatarSpeaking?: { [key: string]: string }; 14 | user?: { [key: string]: string }; 15 | name?: { [key: string]: string }; 16 | } 17 | export type DiscordIconPreviewProps = { 18 | styles: CustomStyle, 19 | } 20 | const DiscordIconPreview = ({ styles }: DiscordIconPreviewProps) => { 21 | const [speaking, setSpeaking] = React.useState(true); 22 | const { t } = useTranslation("translation", { keyPrefix: "preview" }); 23 | return ( 24 |
25 |
29 |
30 |
    31 | setSpeaking(!speaking)} /> 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
41 |
42 |
43 |
44 | ); 45 | }; 46 | export default DiscordIconPreview; 47 | 48 | type UserProps = { 49 | userId: string; 50 | userName: string; 51 | backgroundColor: string; 52 | speaking?: boolean; 53 | src?: string; 54 | onClick?: React.MouseEventHandler; 55 | styles: CustomStyle; 56 | } 57 | const User = ({ userId, userName, backgroundColor, speaking, src, onClick, styles }: UserProps) => { 58 | return ( 59 |
  • 60 | 65 |
    66 | 75 | {userName} 76 | 77 |
    78 |
  • 79 | ); 80 | }; 81 | -------------------------------------------------------------------------------- /src/component/InputArea.tsx: -------------------------------------------------------------------------------- 1 | import Paper from '@mui/material/Paper' 2 | import { styled } from '@mui/material/styles'; 3 | 4 | const InputArea = styled(Paper)(({ theme }) => ({ 5 | background: 'linear-gradient(34deg, #282c34, 90%, #282c34)', 6 | boxShadow: '2px 2px 16px inset #ffffff10, 2px 2px 8px #0000004a', 7 | backgroundColor: theme.palette.mode === 'dark' ? '#1A2027' : '#fff', 8 | ...theme.typography.body2, 9 | padding: theme.spacing(1), 10 | textAlign: 'center', 11 | color: 'white', 12 | })); 13 | export default InputArea; 14 | -------------------------------------------------------------------------------- /src/component/InputUserIdForm.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useTranslation } from 'react-i18next'; 3 | import Button from '@mui/material/Button'; 4 | import Dialog from '@mui/material/Dialog'; 5 | import DialogActions from '@mui/material/DialogActions'; 6 | import DialogContent from '@mui/material/DialogContent'; 7 | import DialogTitle from '@mui/material/DialogTitle'; 8 | import ListItem from '@mui/material/ListItem'; 9 | import TextField from '@mui/material/TextField'; 10 | import FormLabel from '@mui/material/FormLabel'; 11 | import HelpOutlineIcon from '@mui/icons-material/HelpOutline'; 12 | import Box from '@mui/material/Box'; 13 | import Typography from '@mui/material/Typography'; 14 | import idCopy from './img/id_copy.jpg'; 15 | import discordSettingDetail from './img/discord-setting-detail.jpg'; 16 | import discordUserSetting from './img/discord-user-setting.jpg'; 17 | import { t } from 'i18next'; 18 | 19 | export type InputUserIdFormProps = { 20 | title: string; 21 | onChange: (userId: string) => void; 22 | }; 23 | const InputUserIdForm = ({ title, onChange }: InputUserIdFormProps) => { 24 | const [userId, setUserId] = React.useState(''); 25 | const [open, setOpen] = React.useState(false); 26 | const { t } = useTranslation("translation", { keyPrefix: "discord_user_id" }); 27 | 28 | React.useEffect(() => { 29 | onChange(userId); 30 | }, [userId]) 31 | 32 | // @ts-ignore 33 | const handleUserIdChange = (event: React.ChangeEvent) => { setUserId(event?.target?.value || ''); }; 34 | 35 | return ( 36 | <> 37 | 39 | {title} 40 | 42 | 45 | 46 | )} onChange={handleUserIdChange} InputLabelProps={{ shrink: true }} placeholder="739000000000000000" /> 47 | 48 | 49 | { setOpen(false) }}> 50 | {t('what_discord_user_id')} 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | ); 60 | }; 61 | export default InputUserIdForm; 62 | 63 | const AboutDiscordUserIdDialogContent = () => { 64 | const [open, setOpen] = React.useState(false); 65 | const { t } = useTranslation("translation", { keyPrefix: "discord_user_id" }); 66 | return ( 67 | <> 68 | {t('explain_discord_user_id')} 69 | 70 | メンバーを右クリックしてIDをコピー 71 | 72 | 73 | 79 | 80 | 81 | { setOpen(false) }}> 82 | <>{t('how_to_enable_copy')} 83 | 84 | 85 | <>{t('on_developer_mode')} 86 | 87 | 88 | 89 | <>{t('open_user_setting')} 90 | 91 | 92 | Discordの左下からユーザー設定を開く 93 | 94 | 95 | 96 | <>{t('check_developer_mode')} 97 | 98 | 99 | 詳細設定を開き、開発モードの欄をチェック 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | ); 108 | } 109 | 110 | type MouthAnimationStyleProps = { 111 | userId: string; 112 | mouthImgUrl: string; 113 | }; 114 | const MouthAnimationStyle = React.memo((props: MouthAnimationStyleProps) => { 115 | if (!props.userId || !props.mouthImgUrl) return null; 116 | return ( 117 | 128 | ); 129 | }, (p, n) => p.userId === n.userId && p.mouthImgUrl === n.mouthImgUrl); 130 | -------------------------------------------------------------------------------- /src/component/SelectorListItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import FormLabel from '@mui/material/FormLabel'; 3 | import ListItem from '@mui/material/ListItem'; 4 | import ToggleButton from '@mui/material/ToggleButton'; 5 | import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; 6 | 7 | type Option = { label: string; value: string }; 8 | export type SelectorListItemProps = { 9 | title: string; 10 | options: Option[]; 11 | disabled?: boolean; 12 | onChange: (value: string) => void; 13 | }; 14 | const SelectorListItem = ({ title, options, disabled, onChange }: SelectorListItemProps) => { 15 | const [value, setValue] = React.useState(options[0].value || ''); 16 | 17 | React.useEffect(() => { 18 | onChange(value); 19 | }, [disabled]) 20 | 21 | const handleChange = (_: any, val: string) => { 22 | if (!val) return; 23 | setValue(val); 24 | onChange(val); 25 | }; 26 | return ( 27 | 29 | {title} 30 | 36 | {(options || []).map((item: Option) => ( 37 | {item.label} 38 | ))} 39 | 40 | 41 | ); 42 | }; 43 | export default SelectorListItem; 44 | -------------------------------------------------------------------------------- /src/component/SelectorToggleButtonGroup.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import FormLabel from '@mui/material/FormLabel'; 3 | import ListItem from '@mui/material/ListItem'; 4 | import ToggleButton from '@mui/material/ToggleButton'; 5 | import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; 6 | 7 | type Option = { label: string; value: string }; 8 | export type SelectorToggleButtonGroupProps = { 9 | title: string; 10 | options: Option[]; 11 | onChange: (value: string[]) => void; 12 | }; 13 | const SelectorToggleButtonGroup = ({ title, options, onChange }: SelectorToggleButtonGroupProps) => { 14 | const [value, setValue] = React.useState([options[0].value || '']); 15 | const handleChange = (_: any, val: string[]) => { 16 | if (!val) return; 17 | setValue(val); 18 | onChange(val); 19 | }; 20 | return ( 21 | 22 | {title} 23 | 27 | {(options || []).map((item: Option) => ( 28 | {item.label} 29 | ))} 30 | 31 | 32 | ); 33 | }; 34 | export default SelectorToggleButtonGroup; 35 | -------------------------------------------------------------------------------- /src/component/SliderListItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Box from '@mui/material/Box'; 3 | import FormLabel from '@mui/material/FormLabel'; 4 | import ListItem from '@mui/material/ListItem'; 5 | import Slider from '@mui/material/Slider'; 6 | 7 | export type SliderListItemProps = { 8 | title: string; 9 | disabled?: boolean; 10 | onChange: (value: string) => void; 11 | min?: number; 12 | max?: number; 13 | }; 14 | const SliderListItem = ({ title, disabled, onChange, min, max }: SliderListItemProps) => { 15 | const [value, setValue] = React.useState(0); 16 | 17 | React.useEffect(() => { 18 | onChange(`${value}`); 19 | }, [disabled]) 20 | 21 | const handleChange = (_: any, val: number | number[], activeThumb: number) => { 22 | if (typeof val !== 'number') return; 23 | setValue(val); 24 | onChange(`${val}`); 25 | }; 26 | return ( 27 | 29 | {title} 30 | 31 | 39 | 40 | 41 | ); 42 | }; 43 | export default SliderListItem; 44 | -------------------------------------------------------------------------------- /src/component/TutorialButton.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import Button from "@mui/material/Button"; 3 | import HelpIcon from '@mui/icons-material/Help'; 4 | import OpenInNewIcon from '@mui/icons-material/OpenInNew'; 5 | import { Box, Dialog, DialogContent, DialogTitle, Link, Typography } from "@mui/material"; 6 | import { useTranslation } from "react-i18next"; 7 | import Add_browser from './img/1_add_browser.png'; 8 | import obs_empty from './img/1_obs_empty.png'; 9 | import DiscordStreamKitOverlay from './img/2_Discord-StreamKit-Overlay.png'; 10 | import discordStartIcon from './img/Discord-start-icon.png'; 11 | import discordUrl from './img/discord-url.png'; 12 | import copyCss from './img/copy-css.png'; 13 | import obsCss from './img/obs-css.png'; 14 | import obsComplete from './img/obs-complete.png'; 15 | import { grey } from "@mui/material/colors"; 16 | 17 | const TutorialButton = () => { 18 | const [open, setOpen] = useState(false); 19 | const { t, i18n } = useTranslation("translation", { keyPrefix: "tutorial" }); 20 | return ( 21 | <> 22 | 23 | 34 | 35 | setOpen(false)}> 36 | 42 | <>{t("how_to_use")} 43 | 44 | 45 | {i18n.language!=="ja" && ( 46 | 47 | 48 | please see English introduction by 49 | GIGAZINE 50 | 51 | 52 | 53 | )} 54 | 55 |

    1. OBSのソースにブラウザを追加

    56 |

    OBSを起動 57 |
    OBSのシーンを選択後、「ソース」欄で右クリックしてブラウザを追加

    58 | 59 | 60 | 61 | 62 |

    2. Discord を起動

    63 |

    起動していない場合は起動してください

    64 | 65 | 66 | 67 | 68 |

    3. Discord StreamKit Overlay にアクセス

    69 | 70 | https://streamkit.discord.com/overlay 71 | 72 | にアクセス 73 | 74 | 75 | 80 | 81 | 82 | 83 |

    4. ボイスウィジェットのURLを取得

    84 |

    「Install for OBS」をクリックし、「VOICE WIDGET」のタブを選択

    85 |

    「Server」と「Voice Channel」からボイスチャンネルを選択

    86 |

    右下に表示されるURLをコピー

    87 |

    88 | ※画面が見切れている場合はCtrlを押しながらスクロールして縮小して確認します 89 |

    90 | 91 | 92 | 93 | 94 |

    5. OBSにURLを入力

    95 |

    OBSに戻り、中央にあるURLの入力欄に 3. でコピーしたURLを貼り付け

    96 | 97 | 98 | 99 | 100 |

    6. カスタムCSSを作成

    101 |

    アイコン外観変更ジェネレーターで好みの見た目を決めたあと、下に表示されるCSSをコピー

    102 | 103 | 104 | 105 | 106 |

    7. OBSにカスタムCSSを入力

    107 |

    OBSに戻り、URLの少し下にあるカスタムCSSの入力欄に 5. でコピーしたカスタムCSSを貼り付け

    108 | 109 | 110 | 111 | 112 |

    8. 作成完了

    113 |

    ダイアログをOKで閉じて完了です
    OBSに話している人が表示されます

    114 | 115 | 116 | 117 |
    118 |
    119 | 120 | ); 121 | }; 122 | export default TutorialButton; 123 | -------------------------------------------------------------------------------- /src/component/animation.css: -------------------------------------------------------------------------------- 1 | 2 | @keyframes speak-light { 3 | 0% { 4 | box-shadow: 0 0 4px #ffffff; 5 | } 6 | 50% { 7 | box-shadow: 0 0 16px #ffffff; 8 | } 9 | 100% { 10 | box-shadow: 0 0 4px #ffffff; 11 | } 12 | } 13 | @keyframes speak-jump { 14 | 0% { 15 | bottom: 0px; 16 | } 17 | 50% { 18 | bottom: 10px; 19 | } 20 | 100% { 21 | bottom: 0px; 22 | } 23 | } -------------------------------------------------------------------------------- /src/component/img/1_add_browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfe/obs-discord-icons-css-generator/d89b320305ceeee54956c029ddd6214c8e151e7d/src/component/img/1_add_browser.png -------------------------------------------------------------------------------- /src/component/img/1_obs_empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfe/obs-discord-icons-css-generator/d89b320305ceeee54956c029ddd6214c8e151e7d/src/component/img/1_obs_empty.png -------------------------------------------------------------------------------- /src/component/img/2_Discord-StreamKit-Overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfe/obs-discord-icons-css-generator/d89b320305ceeee54956c029ddd6214c8e151e7d/src/component/img/2_Discord-StreamKit-Overlay.png -------------------------------------------------------------------------------- /src/component/img/Discord-start-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfe/obs-discord-icons-css-generator/d89b320305ceeee54956c029ddd6214c8e151e7d/src/component/img/Discord-start-icon.png -------------------------------------------------------------------------------- /src/component/img/copy-css.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfe/obs-discord-icons-css-generator/d89b320305ceeee54956c029ddd6214c8e151e7d/src/component/img/copy-css.png -------------------------------------------------------------------------------- /src/component/img/discord-setting-detail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfe/obs-discord-icons-css-generator/d89b320305ceeee54956c029ddd6214c8e151e7d/src/component/img/discord-setting-detail.jpg -------------------------------------------------------------------------------- /src/component/img/discord-url.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfe/obs-discord-icons-css-generator/d89b320305ceeee54956c029ddd6214c8e151e7d/src/component/img/discord-url.png -------------------------------------------------------------------------------- /src/component/img/discord-user-setting.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfe/obs-discord-icons-css-generator/d89b320305ceeee54956c029ddd6214c8e151e7d/src/component/img/discord-user-setting.jpg -------------------------------------------------------------------------------- /src/component/img/id_copy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfe/obs-discord-icons-css-generator/d89b320305ceeee54956c029ddd6214c8e151e7d/src/component/img/id_copy.jpg -------------------------------------------------------------------------------- /src/component/img/obs-complete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfe/obs-discord-icons-css-generator/d89b320305ceeee54956c029ddd6214c8e151e7d/src/component/img/obs-complete.png -------------------------------------------------------------------------------- /src/component/img/obs-css.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfe/obs-discord-icons-css-generator/d89b320305ceeee54956c029ddd6214c8e151e7d/src/component/img/obs-css.png -------------------------------------------------------------------------------- /src/component/stories/ClipboardButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import ClipboardButton from '../ClipboardButton'; 3 | 4 | type Story = StoryObj; 5 | 6 | const meta: Meta = { 7 | title: 'component/ClipboardButton', 8 | component: ClipboardButton, 9 | }; 10 | 11 | export default meta; 12 | 13 | export const Default: Story = { 14 | args: { 15 | value: `copied text` 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/component/stories/CssString.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import CssString from '../CssString'; 3 | 4 | type Story = StoryObj; 5 | 6 | const meta: Meta = { 7 | title: 'component/CssString', 8 | component: CssString, 9 | }; 10 | 11 | export default meta; 12 | 13 | export const Default: Story = { 14 | args: { 15 | value: `#app-mount .voice-states { 16 | display: flex; 17 | } 18 | #app-mount .voice-state { 19 | display: flex; 20 | flex-direction: column; 21 | } 22 | #app-mount .name { 23 | max-width: 64px; 24 | box-sizing: border-box; 25 | text-overflow: clip; 26 | white-space: nowrap; 27 | overflow: hidden; 28 | display: block; 29 | text-align: center; 30 | }` 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/component/stories/DiscordIconPreview.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import DiscordIconPreview from '../DiscordIconPreview'; 3 | 4 | type Story = StoryObj; 5 | 6 | // More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 7 | const meta: Meta = { 8 | title: 'component/DiscordIconPreview', 9 | component: DiscordIconPreview, 10 | }; 11 | 12 | export default meta; 13 | 14 | export const Default: Story = { 15 | args: { 16 | styles: { 17 | voiceContainer: {}, 18 | voiceStates: {}, 19 | voiceState: {}, 20 | avatar: {}, 21 | avatarSpeaking: {}, 22 | name: {}, 23 | }, 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/component/stories/SelectorListItem.stories.tsx: -------------------------------------------------------------------------------- 1 | import { fn } from '@storybook/test'; 2 | import type { Meta, StoryObj } from "@storybook/react"; 3 | import SelectorListItem from '../SelectorListItem'; 4 | 5 | type Story = StoryObj; 6 | 7 | // More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 8 | const meta: Meta = { 9 | title: 'component/SelectorListItem', 10 | component: SelectorListItem, 11 | }; 12 | 13 | export default meta; 14 | 15 | export const Default: Story = { 16 | args: { 17 | title: 'title', 18 | options: [ 19 | { label: 'label1', value: 'value1' }, 20 | { label: 'label2', value: 'value2' }, 21 | { label: 'label3', value: 'value3' }, 22 | { label: 'label4', value: 'value4' }, 23 | { label: 'label5', value: 'value5' }, 24 | ], 25 | onChange: fn(), 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/component/stories/SliderListItem.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import SliderListItem from '../SliderListItem'; 3 | 4 | type Story = StoryObj; 5 | 6 | // More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 7 | const meta: Meta = { 8 | title: 'component/SliderListItem', 9 | component: SliderListItem, 10 | }; 11 | 12 | export default meta; 13 | 14 | export const Default: Story = { 15 | args: { 16 | title: 'title', 17 | onChange: () => {}, 18 | } 19 | }; 20 | 21 | export const Disabled: Story = { 22 | args: { 23 | title: 'title', 24 | onChange: () => {}, 25 | } 26 | }; 27 | 28 | export const withMax: Story = { 29 | args: { 30 | title: 'title', 31 | max: 5, 32 | onChange: () => {}, 33 | } 34 | }; 35 | 36 | export const withMin: Story = { 37 | args: { 38 | title: 'title', 39 | min: 5, 40 | onChange: () => {}, 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /src/component/stories/TutorialButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import TutorialButton from '../TutorialButton'; 3 | 4 | type Story = StoryObj; 5 | 6 | const meta: Meta = { 7 | title: 'component/TutorialButton', 8 | component: TutorialButton, 9 | }; 10 | 11 | export default meta; 12 | 13 | export const Default: Story = { 14 | args: {} 15 | }; 16 | -------------------------------------------------------------------------------- /src/configs/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18n from "i18next"; 2 | import { initReactI18next } from "react-i18next"; 3 | import LanguageDetector from "i18next-browser-languagedetector"; 4 | import { ChineseCsTranslate, ChineseCtTranslate, EnglishTranslate, JapaneseTranslate } from "../translations"; 5 | 6 | i18n 7 | .use(LanguageDetector) 8 | .use(initReactI18next) 9 | .init({ 10 | resources: { 11 | en: { translation: EnglishTranslate }, 12 | ja: { translation: JapaneseTranslate }, 13 | cs: { translation: ChineseCsTranslate }, 14 | ct: { translation: ChineseCtTranslate }, 15 | 16 | 17 | }, 18 | fallbackLng: "en", 19 | debug: true, 20 | interpolation: { escapeValue: false }, 21 | }); 22 | 23 | export default i18n; 24 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfe/obs-discord-icons-css-generator/d89b320305ceeee54956c029ddd6214c8e151e7d/src/favicon.ico -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/cssObj.ts: -------------------------------------------------------------------------------- 1 | import omit from "lodash/omit"; 2 | import { CustomStyle } from "../component/DiscordIconPreview"; 3 | 4 | type StringValArg = { 5 | val: string; 6 | styles: CustomStyle; 7 | setStyles: React.Dispatch>; 8 | }; 9 | 10 | const getIsTall = (avatar: CustomStyle['avatar']) => { 11 | const height = Number(avatar?.height?.replace('px', '') || 50); 12 | const width = Number(avatar?.width?.replace('px', '') || 50); 13 | return height !== width; 14 | } 15 | 16 | // アイコンの並び 17 | const iconAlign = ({ val, styles, setStyles }: StringValArg) => { 18 | const voiceStates = omit(styles.voiceStates, ['display', 'flexDirection', 'rowGap', 'columnGap', 'margin']); 19 | const voiceState = omit(styles.voiceState, ['display', 'flexDirection']); 20 | const user = omit(styles.user, ['paddingTop']); 21 | const name = omit(styles.name, ['boxSizing', 'textOverflow', 'whiteSpace', 'overflow', 'display', 'textAlign']); 22 | switch (val) { 23 | case 'horizontal': 24 | setStyles({ 25 | ...styles, 26 | voiceStates: { 27 | ...voiceStates, 28 | display: 'flex', 29 | flexWrap: 'wrap', 30 | margin: '32px', 31 | }, 32 | voiceState: { 33 | ...voiceState, 34 | display: 'flex', 35 | flexDirection: 'column', 36 | height: 'auto', 37 | marginBottom: '0', 38 | }, 39 | user: { 40 | ...user, 41 | paddingTop: '0px', 42 | }, 43 | name: { 44 | ...name, 45 | maxWidth: styles?.avatar?.width || '56px', 46 | boxSizing: 'border-box', 47 | textOverflow: 'clip', 48 | whiteSpace: 'nowrap', 49 | overflow: 'hidden', 50 | display: 'block', 51 | textAlign: 'center', 52 | zIndex: '2', 53 | } 54 | }); 55 | break; 56 | default: 57 | setStyles({ 58 | ...styles, 59 | voiceStates: { 60 | ...voiceStates, 61 | display: 'flex', 62 | flexDirection: 'column', 63 | }, 64 | voiceState: { 65 | ...voiceState, 66 | display: 'flex', 67 | height: 'initial', 68 | marginBottom: '0', 69 | }, 70 | user, 71 | name, 72 | }); 73 | break; 74 | } 75 | }; 76 | // アイコンの間隔(上下) 77 | const iconRowGap = ({ val, styles, setStyles }: StringValArg) => { 78 | const voiceStates = omit(styles.voiceStates, ['rowGap']); 79 | switch (val) { 80 | case '0': 81 | setStyles({ 82 | ...styles, 83 | voiceStates 84 | }); 85 | break; 86 | default: 87 | setStyles({ 88 | ...styles, 89 | voiceStates: { 90 | ...voiceStates, 91 | rowGap: `${val}px`, 92 | }, 93 | }); 94 | break; 95 | } 96 | }; 97 | 98 | // アイコンの間隔(左右) 99 | const iconColumnGap = ({ val, styles, setStyles }: StringValArg) => { 100 | const voiceStates = omit(styles.voiceStates, ['columnGap']); 101 | switch (val) { 102 | case '0': 103 | setStyles({ 104 | ...styles, 105 | voiceStates, 106 | }); 107 | break; 108 | default: 109 | setStyles({ 110 | ...styles, 111 | voiceStates: { 112 | ...voiceStates, 113 | columnGap: `${val}px`, 114 | }, 115 | }); 116 | break; 117 | } 118 | }; 119 | 120 | // アイコンの形 121 | const iconShape = ({ val, styles, setStyles }: StringValArg) => { 122 | const height = Number(styles?.avatar?.height?.replace('px', '') || 50); 123 | const wasTall = getIsTall(styles?.avatar); 124 | const avatar = omit(styles.avatar, ['borderRadius', 'objectFit', 'height']); 125 | setStyles({ 126 | ...styles, 127 | avatar: { 128 | ...avatar, 129 | ...((val === 'circle') ? {} : { 130 | borderRadius: (val === 'rect-r') ? '8px' : '0px', 131 | }), 132 | ...((val === 'tall' && !wasTall) 133 | ? { 134 | objectFit: 'cover', 135 | height: `${height * 2}px`, 136 | } 137 | : { 138 | height: `${wasTall ? height / 2 : height}px`, 139 | } 140 | ), 141 | } 142 | }) 143 | }; 144 | 145 | // 話すときの動き 146 | const iconSpeaking = ({ val, styles, setStyles }: StringValArg) => { 147 | const avatar = omit(styles.avatar, ['filter']); 148 | const avatarSpeaking = omit(styles.avatarSpeaking, ['position', 'animation', 'animationDuration', 'filter', 'borderColor']); 149 | switch (val) { 150 | case 'light': 151 | setStyles({ 152 | ...styles, 153 | avatar: { 154 | ...avatar, 155 | filter: 'brightness(70%)', 156 | }, 157 | avatarSpeaking: { 158 | ...avatarSpeaking, 159 | position: 'relative', 160 | animation: '300ms infinite alternate ease-in-out speak-light', 161 | filter: 'brightness(100%)', 162 | borderColor: 'rgba(255,255,255,.75)', // !important 163 | } 164 | }); 165 | break; 166 | case 'jump': 167 | setStyles({ 168 | ...styles, 169 | avatar: { 170 | ...avatar, 171 | filter: 'brightness(70%)', 172 | }, 173 | avatarSpeaking: { 174 | ...avatarSpeaking, 175 | position: 'relative', 176 | animation: '300ms infinite alternate ease-in-out speak-jump', 177 | filter: 'brightness(100%)', 178 | borderColor: 'transparent', // !important 179 | } 180 | }); 181 | break; 182 | default: 183 | setStyles({ 184 | ...styles, 185 | avatar, 186 | avatarSpeaking, 187 | }); 188 | break; 189 | } 190 | } 191 | 192 | type StyleInsetType = { 193 | styles: CustomStyle; 194 | setStyles: React.Dispatch>; 195 | } 196 | // 話すときの動き 197 | export const setIconSpeakingStyle = ({ 198 | val, animationColor, styles, setStyles, 199 | }: StyleInsetType & { val: string[]; animationColor: string; }) => { 200 | const avatar = omit(styles.avatar, ['filter', 'marginRight']); 201 | const avatarSpeaking = omit(styles.avatarSpeaking, ['position', 'animation', 'filter', 'borderColor', 'zIndex']); 202 | 203 | const newAnimation = val.map((animationType: string) => { 204 | switch (animationType) { 205 | case 'border': 206 | return ''; 207 | case 'light': 208 | return '750ms infinite alternate ease-in-out speak-light'; 209 | case 'jump': 210 | return '750ms infinite alternate ease-in-out speak-jump'; 211 | default: return ''; 212 | } 213 | }).filter((v) => !!v); 214 | 215 | setStyles({ 216 | ...styles, 217 | avatar: { 218 | ...avatar, 219 | filter: 'brightness(70%)', 220 | marginRight: '0', 221 | }, 222 | avatarSpeaking: { 223 | ...avatarSpeaking, 224 | position: 'relative', 225 | filter: 'brightness(100%)', 226 | ...(!val.includes('border') ? { borderColor: 'transparent' } : { borderColor: animationColor }), 227 | ...(newAnimation.length === 0 ? '' : { animation: newAnimation.join(',')}), 228 | zIndex: '1', 229 | } 230 | }); 231 | } 232 | 233 | // 動きの速さ 234 | const iconSpeakingDuration = ({ val, styles, setStyles }: StringValArg) => { 235 | const avatarSpeaking = omit(styles.avatarSpeaking, ['animationDuration']); 236 | const animationDuration = val === '0' ? '750ms' : `${751 - Number(val) * 5}ms`; 237 | setStyles({ 238 | ...styles, 239 | avatarSpeaking: { 240 | ...avatarSpeaking, 241 | animationDuration, 242 | }, 243 | }); 244 | }; 245 | 246 | // アイコンの大きさ 247 | const iconSize = ({ val, styles, setStyles }: StringValArg) => { 248 | const isTall = getIsTall(styles?.avatar); 249 | const avatar = omit(styles.avatar, ['width', 'height', 'marginBottom']); 250 | 251 | const getStyle = (size: number) => ({ 252 | ...styles, 253 | avatar: { 254 | ...avatar, 255 | width: `${size}px`, 256 | height: `${size * (!!isTall ? 2 : 1)}px`, 257 | marginBottom: '8px', 258 | }, 259 | name: { 260 | ...styles.name, 261 | maxWidth: `${size + 6}px`, // アイコンサイズ+6px 262 | }, 263 | }) 264 | switch (val) { 265 | case 'lg': 266 | setStyles(getStyle(80)); 267 | break; 268 | case 'xg': 269 | setStyles(getStyle(96)); 270 | break; 271 | default: 272 | setStyles(getStyle(50)); 273 | break; 274 | } 275 | }; 276 | 277 | // 名前 278 | const nameVisibility = ({ val, styles, setStyles }: StringValArg) => { 279 | const name = omit(styles.name, ['visibility']); 280 | switch (val) { 281 | case 'exist': 282 | setStyles({ 283 | ...styles, 284 | name, 285 | }); 286 | break; 287 | default: 288 | setStyles({ 289 | ...styles, 290 | name: { 291 | ...name, 292 | visibility: 'hidden', 293 | }, 294 | }); 295 | break; 296 | } 297 | }; 298 | 299 | // 名前の見た目 300 | const nameStyle = ({ val, styles, setStyles }: StringValArg) => { 301 | const name = omit(styles.name, ['backgroundColor', 'textShadow']); 302 | switch (val) { 303 | case 'bordering': 304 | setStyles({ 305 | ...styles, 306 | name: { 307 | ...name, 308 | backgroundColor: 'transparent', 309 | textShadow: '2px 2px 2px black, -2px -2px 2px black, 2px -2px 2px black, -2px 2px 2px black', 310 | }, 311 | }); 312 | break; 313 | case 'none': 314 | setStyles({ 315 | ...styles, 316 | name: { 317 | ...name, 318 | backgroundColor: 'transparent', 319 | }, 320 | }); 321 | break; 322 | default: 323 | setStyles({ 324 | ...styles, 325 | name, 326 | }); 327 | break; 328 | } 329 | }; 330 | 331 | // 名前の位置(上下) 332 | const namePositionVertical = ({ val, styles, setStyles }: StringValArg) => { 333 | const name = omit(styles.name, ['position', 'top']); 334 | switch (val) { 335 | default: 336 | setStyles({ 337 | ...styles, 338 | name: { 339 | ...name, 340 | top: `${val}px`, 341 | position: 'relative', 342 | }, 343 | }); 344 | break; 345 | } 346 | }; 347 | 348 | // 名前の位置(左右) 349 | const namePositionHorizontal = ({ val, styles, setStyles }: StringValArg) => { 350 | const name = omit(styles.name, ['position', 'left']); 351 | switch (val) { 352 | default: 353 | setStyles({ 354 | ...styles, 355 | name: { 356 | ...name, 357 | left: `${val}px`, 358 | position: 'relative', 359 | }, 360 | }); 361 | break; 362 | } 363 | }; 364 | export default { 365 | iconAlign, 366 | iconRowGap, 367 | iconColumnGap, 368 | iconShape, 369 | iconSpeaking, 370 | iconSpeakingDuration, 371 | iconSize, 372 | nameVisibility, 373 | nameStyle, 374 | namePositionVertical, 375 | namePositionHorizontal, 376 | }; 377 | -------------------------------------------------------------------------------- /src/lib/cssText.ts: -------------------------------------------------------------------------------- 1 | import { CustomStyle } from "../component/DiscordIconPreview"; 2 | import { getCssKeyFrames } from "./getCssKeyFrames"; 3 | 4 | const toKebabCase = (string: string) => string 5 | .replace(/([a-z])([A-Z])/g, "$1-$2") 6 | .replace(/[\s_]+/g, '-') 7 | .toLowerCase(); 8 | 9 | const toImportant = (property: string, className: string): string => { 10 | switch (true) { 11 | case className === 'name' && property === 'backgroundColor': 12 | return ' !important'; 13 | default: 14 | return ''; 15 | } 16 | }; 17 | 18 | const convertStyleObjToCssClassText = (styles: CustomStyle, className: keyof CustomStyle) => (` 19 | [class*="Voice_${className}__"] {${Object.keys(styles[className] || {}).map(k => ` 20 | ${toKebabCase(k)}: ${styles[className]?.[k]}${toImportant(k, className)};`).join(``)} 21 | }`); 22 | 23 | 24 | const getHiddenUserIdText = (hiddenUserId?: string) => (!hiddenUserId ? '' : ` 25 | [src*="avatars/${hiddenUserId}/"], [src*="avatars/${hiddenUserId}/"] + [class*="Voice_user_"] { 26 | display: none; 27 | } 28 | `); 29 | 30 | export const getCssText = ({ 31 | styles, speakingStyles, animationColor, hiddenUserId, 32 | }: { 33 | styles: CustomStyle; 34 | hiddenUserId?: string; 35 | speakingStyles?: string[]; 36 | animationColor?: string; 37 | }) => { 38 | 39 | return ( 40 | (Object.keys(styles) as (keyof CustomStyle)[]) 41 | .map((className) => (Object.keys(styles[className] || {}).length === 0) ? '' : convertStyleObjToCssClassText(styles, className)) 42 | .join(` `).trim() 43 | + getHiddenUserIdText(hiddenUserId) 44 | + getCssKeyFrames(speakingStyles, animationColor) 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /src/lib/getCssKeyFrames.ts: -------------------------------------------------------------------------------- 1 | export const getCssKeyFrames = (speakingStyles: string[] = [], animationColor: string = '#fff') => { 2 | let result = ''; 3 | if (speakingStyles.includes('jump')) { 4 | result += ` 5 | @keyframes speak-jump { 6 | 0% { 7 | bottom: 0px; 8 | } 9 | 50% { 10 | bottom: 10px; 11 | } 12 | 100% { 13 | bottom: 0px; 14 | } 15 | }`; 16 | } 17 | if (speakingStyles.includes('light') && speakingStyles.includes('border')) { 18 | result += ` 19 | @keyframes speak-light { 20 | 0% { 21 | filter: drop-shadow(0 0 2px ${animationColor}) brightness(100%) drop-shadow(2px 2px 0px ${animationColor}) drop-shadow(-2px -2px 0px ${animationColor}) drop-shadow(-2px 2px 0px ${animationColor}) drop-shadow(2px -2px 0px ${animationColor}); 22 | } 23 | 50% { 24 | filter: drop-shadow(0 0 8px ${animationColor}) brightness(100%) drop-shadow(2px 2px 0px ${animationColor}) drop-shadow(-2px -2px 0px ${animationColor}) drop-shadow(-2px 2px 0px ${animationColor}) drop-shadow(2px -2px 0px ${animationColor}); 25 | } 26 | 100% { 27 | filter: drop-shadow(0 0 2px ${animationColor}) brightness(100%) drop-shadow(2px 2px 0px ${animationColor}) drop-shadow(-2px -2px 0px ${animationColor}) drop-shadow(-2px 2px 0px ${animationColor}) drop-shadow(2px -2px 0px ${animationColor}); 28 | } 29 | }`; 30 | } else { 31 | if (speakingStyles.includes('light')) { 32 | result += ` 33 | @keyframes speak-light { 34 | 0% { 35 | filter: drop-shadow(0 0 2px ${animationColor}); 36 | } 37 | 50% { 38 | filter: drop-shadow(0 0 8px ${animationColor}); 39 | } 40 | 100% { 41 | filter: drop-shadow(0 0 2px ${animationColor}); 42 | } 43 | }`; 44 | } 45 | if (speakingStyles.includes('border')) { 46 | result += ` 47 | @keyframes speak-border { 48 | 0% { 49 | filter: drop-shadow(2px 2px 0px ${animationColor}) drop-shadow(-2px -2px 0px ${animationColor}) drop-shadow(-2px 2px 0px ${animationColor}) drop-shadow(2px -2px 0px ${animationColor}); 50 | } 51 | 50% { 52 | filter: drop-shadow(2px 2px 0px ${animationColor}) drop-shadow(-2px -2px 0px ${animationColor}) drop-shadow(-2px 2px 0px ${animationColor}) drop-shadow(2px -2px 0px ${animationColor}); 53 | } 54 | 100% { 55 | filter: drop-shadow(2px 2px 0px ${animationColor}) drop-shadow(-2px -2px 0px ${animationColor}) drop-shadow(-2px 2px 0px ${animationColor}) drop-shadow(2px -2px 0px ${animationColor}); 56 | } 57 | }`; 58 | } 59 | } 60 | return result; 61 | }; 62 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | import './index.css' 3 | import "./configs/i18n"; 4 | import App from './App' 5 | 6 | const container = document.getElementById('root'); 7 | const root = createRoot(container!); 8 | root.render(); 9 | -------------------------------------------------------------------------------- /src/theme.ts: -------------------------------------------------------------------------------- 1 | import { grey } from '@mui/material/colors'; 2 | import { createTheme } from '@mui/material/styles'; 3 | 4 | const DISCORD_BLUE = '#5865F2'; 5 | const theme = createTheme({ 6 | palette: { 7 | primary: { 8 | main: DISCORD_BLUE, 9 | }, 10 | text: { 11 | primary: grey[50], 12 | secondary: grey[100], 13 | } 14 | }, 15 | components: { 16 | MuiPaper: { 17 | styleOverrides: { 18 | root: { 19 | color: grey[900], 20 | }, 21 | }, 22 | }, 23 | MuiToggleButton: { 24 | styleOverrides: { 25 | root: { 26 | color: grey[200], 27 | } 28 | } 29 | }, 30 | MuiToggleButtonGroup: { 31 | styleOverrides: { 32 | root: { 33 | color: grey[50], 34 | '.Mui-selected.Mui-selected': { 35 | backgroundColor: '#5865F2', 36 | color: grey[50], 37 | boxShadow: 2, 38 | borderRadius: '4px', 39 | }, 40 | '.Mui-selected.Mui-selected:hover': { 41 | backgroundColor: '#5865F2', 42 | color: grey[50], 43 | boxShadow: 2, 44 | borderRadius: '4px', 45 | } 46 | } 47 | }, 48 | }, 49 | }, 50 | }); 51 | export default theme; 52 | -------------------------------------------------------------------------------- /src/translations/chinese_cs.ts: -------------------------------------------------------------------------------- 1 | const en = { 2 | header: { 3 | title: "OBS 的 Discord 头像外观CSS生成器", 4 | title_anno: "生成器可以创建自定义CSS,用于在OBS Studio中显示Discord呼叫的成员,如并排或方形图标。", 5 | icon_link: '如果你想把头像变成立绘,请点击这里。', 6 | text_link: '如果你想改变文字频道的显示方式,请点击这里。', 7 | news: '', 8 | }, 9 | footer: { 10 | commentary_article: '文章解说', 11 | blog: '博客', 12 | commentary_video: '视频解说', 13 | niconico: 'Niconico视频', 14 | youtube: '油管', 15 | media_introduction: '媒体介绍', 16 | gigazine: 'GIGAZINE', 17 | }, 18 | tutorial: { 19 | how_to_use: "如何使用", 20 | }, 21 | css_maker: { 22 | icon_alignment: "头像方向", 23 | icon_row_gap: "头像间距 (上下)", 24 | icon_column_gap: "头像间距 (左右)", 25 | vertical: "纵向排列", 26 | horizontal: "横向排列", 27 | icon_shape: "头像形状", 28 | circle: "⚪ 圆形", 29 | rounded: "🔲 圆角方形", 30 | square: "⬜ 正方形", 31 | tall: "▯ 纵长", 32 | movement: "说话时的显示效果", 33 | border: "边缘", 34 | blinking: "闪烁", 35 | jump: "跳跃", 36 | speed_of_movement: "运作速度", 37 | color: "颜色", 38 | icon_size: "头像尺寸", 39 | normal: "标准", 40 | large: "大型", 41 | huge: "非常大", 42 | name: "用户名称", 43 | show: "显示", 44 | none: "隐藏", 45 | look_of_the_name: "名称外观", 46 | black_base: "黑色背景", 47 | text_only: "仅限文本", 48 | top_and_bottom: "名称位置 (上下)", 49 | left_right: "名称位置 (左右)", 50 | hide_particular_user: "屏蔽特定用户", 51 | }, 52 | preview: { 53 | user_click_to_switch: '用户(点击切换)', 54 | user: '用户', 55 | user_always_talking: '用户(一直在说话)', 56 | }, 57 | discord_user_id: { 58 | discord_user_id: 'Discord用户ID', 59 | what_discord_user_id: '什么是 Discord 用户 ID?', 60 | explain_discord_user_id: '你可以通过右键点击Discord服务器的任何一个成员,并在出现的菜单中点击 "复制ID "来获得', 61 | disabled_id_copy: '没有出现「复制ID」', 62 | how_to_enable_copy: '如何使用「复制ID」?', 63 | on_developer_mode: '在Discord中激活 "开发者模式 "以显示它', 64 | open_user_setting: '打开用户设置', 65 | check_developer_mode: '点击 "高级设置 "中的 "开发者模式 "复选框以启用它', 66 | }, 67 | }; 68 | export default en; 69 | -------------------------------------------------------------------------------- /src/translations/chinese_ct.ts: -------------------------------------------------------------------------------- 1 | const en = { 2 | header: { 3 | title: "OBS 的 Discord 頭像外觀語法產生器", 4 | title_anno: "用於 OBS Studio 中的顯示 Discord 頻道成員的自定義 CSS 產生器,例如水平對齊或方形頭像", 5 | icon_link: '如果你想把頭像變成立繪,請點擊這裡。', 6 | text_link: '如果你想改變文字頻道的顯示方式,請點擊這裡。', 7 | news: '', 8 | }, 9 | footer: { 10 | commentary_article: '教學文章', 11 | blog: '部落格', 12 | commentary_video: '教學影片', 13 | niconico: 'Niconico動畫', 14 | youtube: 'Youtube', 15 | media_introduction: '媒體介紹', 16 | gigazine: 'GIGAZINE', 17 | }, 18 | tutorial: { 19 | how_to_use: "如何使用", 20 | }, 21 | css_maker: { 22 | icon_alignment: "頭像的方向", 23 | icon_row_gap: "頭像間距 (上下)", 24 | icon_column_gap: "頭像間距 (左右)", 25 | vertical: "垂直排列", 26 | horizontal: "橫向排列", 27 | icon_shape: "頭像形狀", 28 | circle: "⚪ 圓形", 29 | rounded: "🔲 圓角方形", 30 | square: "⬜ 正方形", 31 | tall: "▯ 縱長", 32 | movement: "說話時的顯示效果", 33 | border: "邊緣", 34 | blinking: "閃爍", 35 | jump: "跳躍", 36 | speed_of_movement: "運作速度", 37 | color: "顔色", 38 | icon_size: "頭像尺寸", 39 | normal: "標準", 40 | large: "大型", 41 | huge: "非常大", 42 | name: "用戶名稱", 43 | show: "顯示", 44 | none: "隱藏", 45 | look_of_the_name: "名牌外觀", 46 | black_base: "黑色背景", 47 | text_only: "僅限文字", 48 | top_and_bottom: "名稱位置 (上下)", 49 | left_right: "名稱位置 (左右)", 50 | hide_particular_user: "隱藏特定用戶", 51 | }, 52 | preview: { 53 | user_click_to_switch: '用戶 (點擊切換)', 54 | user: '用戶', 55 | user_always_talking: '用戶 (持續說話中)', 56 | }, 57 | discord_user_id: { 58 | discord_user_id: 'Discord用戶ID', 59 | what_discord_user_id: 'Discord用戶ID是什麼?', 60 | explain_discord_user_id: '你可以通過對Discord伺服器成員點擊右鍵來複製用戶ID,ID顯示如:123450000000000000', 61 | disabled_id_copy: '沒有出現「複製ID」', 62 | how_to_enable_copy: '如何使用「複製ID」?', 63 | on_developer_mode: '在Discord設定中開啟「開發者模式」來顯示它', 64 | open_user_setting: '先打開用戶設置之後', 65 | check_developer_mode: '開啟在「進階」中的「開發者模式」', 66 | }, 67 | }; 68 | export default en; 69 | -------------------------------------------------------------------------------- /src/translations/english.ts: -------------------------------------------------------------------------------- 1 | const en = { 2 | header: { 3 | title: "OBS Discord Icon Appearance Change Generator", 4 | title_anno: "Generator to create custom CSS for displaying members on a Discord call in OBS Studio, such as horizontal alignment or square icons", 5 | icon_link: 'Tool for changing from icon to picture', 6 | text_link: 'Tool for changing the appearance of the text channel', 7 | news: '', 8 | }, 9 | footer: { 10 | commentary_article: 'Commentary article', 11 | blog: 'Blog', 12 | commentary_video: 'Commentary video', 13 | niconico: 'niconico', 14 | youtube: 'Youtube', 15 | media_introduction: 'Media introduction', 16 | gigazine: 'GIGAZINE', 17 | }, 18 | tutorial: { 19 | how_to_use: "How to use", 20 | }, 21 | css_maker: { 22 | icon_alignment: "Icon alignment", 23 | icon_row_gap: "Icon row gap", 24 | icon_column_gap: "Icon column gap", 25 | vertical: "Vertical", 26 | horizontal: "Horizontal", 27 | icon_shape: "Icon shape", 28 | circle: "⚪ Circle", 29 | rounded: "🔲 Rounded", 30 | square: "⬜ Square", 31 | tall: "▯ Tall", 32 | movement: "Movement", 33 | border: "Border", 34 | blinking: "Blinking", 35 | jump: "Jump", 36 | speed_of_movement: "Speed of movement", 37 | color: "color", 38 | icon_size: "Icon size", 39 | normal: "Normal", 40 | large: "Large", 41 | huge: "Huge", 42 | name: "Name", 43 | show: "Show", 44 | none: "None", 45 | look_of_the_name: "Look of the name", 46 | black_base: "Black base", 47 | text_only: "Text only", 48 | top_and_bottom: "Position (top and bottom)", 49 | left_right: "Position (left and right)", 50 | hide_particular_user: "Hide particular user", 51 | }, 52 | preview: { 53 | user_click_to_switch: 'User (click to switch)', 54 | user: 'User', 55 | user_always_talking: 'User (always talking)', 56 | }, 57 | discord_user_id: { 58 | discord_user_id: 'Discord User ID', 59 | what_discord_user_id: 'What\'s Discord User ID?', 60 | explain_discord_user_id: 'The ID that can be obtained by right-clicking on a member of the discord server and clicking on "Copy ID" from the menu that appears when you right-click on a member of the discord server.', 61 | disabled_id_copy: 'Copy ID doesn\'t appear.', 62 | how_to_enable_copy: 'How do I get "Copy ID" to appear?', 63 | on_developer_mode: 'You can enable "Developer Mode" in Discord to display it.', 64 | open_user_setting: 'After opening the user settings', 65 | check_developer_mode: 'Advanced Settings and check the Developer Mode checkbox to enable it.', 66 | }, 67 | }; 68 | export default en; 69 | -------------------------------------------------------------------------------- /src/translations/index.ts: -------------------------------------------------------------------------------- 1 | export { default as JapaneseTranslate } from "./japanese"; 2 | export { default as EnglishTranslate } from "./english"; 3 | export { default as ChineseCsTranslate } from "./chinese_cs"; 4 | export { default as ChineseCtTranslate } from "./chinese_ct"; 5 | -------------------------------------------------------------------------------- /src/translations/japanese.ts: -------------------------------------------------------------------------------- 1 | const ja = { 2 | header: { 3 | title: "OBSのDiscordアイコン外観変更ジェネレーター", 4 | title_anno: "Discordで通話中のメンバーをOBS Studioに表示するときに、横並びにしたりアイコンを四角にしたりするためのカスタムCSSをつくるジェネレーター", 5 | icon_link: 'アイコンから立ち絵に変えたいときはこちら', 6 | text_link: 'テキストチャンネルの見た目を変えたいときはこちら', 7 | news: '', 8 | }, 9 | footer: { 10 | commentary_article: '解説記事', 11 | blog: 'ブログ', 12 | commentary_video: '解説動画', 13 | niconico: 'ニコニコ動画', 14 | youtube: 'Youtube', 15 | media_introduction: 'メディア紹介', 16 | gigazine: 'GIGAZINE', 17 | }, 18 | tutorial: { 19 | how_to_use: "使い方", 20 | }, 21 | css_maker: { 22 | icon_alignment: "アイコンの並び", 23 | icon_row_gap: "アイコンの間隔(上下)", 24 | icon_column_gap: "アイコンの間隔(左右)", 25 | vertical: "縦並び", 26 | horizontal: "横並び", 27 | icon_shape: "アイコンの形", 28 | circle: "⚪ 丸", 29 | rounded: "🔲角丸四角", 30 | square: "⬜四角", 31 | tall: "▯ 縦長", 32 | movement: "話すときの動き", 33 | border: "縁取り", 34 | blinking: "点滅", 35 | jump: "ぴょこぴょこ", 36 | speed_of_movement: "動きの速さ", 37 | color: "色", 38 | icon_size: "アイコンの大きさ", 39 | normal: "標準", 40 | large: "大きい", 41 | huge: "とても大きい", 42 | name: "名前", 43 | show: "あり", 44 | none: "なし", 45 | look_of_the_name: "名前の見た目", 46 | black_base: "黒背景", 47 | text_only: "文字のみ", 48 | top_and_bottom: "名前の位置(上下)", 49 | left_right: "名前の位置(左右)", 50 | hide_particular_user: "特定のユーザを隠す", 51 | }, 52 | preview: { 53 | user_click_to_switch: 'ユーザ(クリックで切替え)', 54 | user: 'ユーザ', 55 | user_always_talking: 'ユーザ(常にお話し中)', 56 | }, 57 | discord_user_id: { 58 | discord_user_id: 'DiscordユーザID', 59 | what_discord_user_id: 'DiscordユーザIDとは?', 60 | explain_discord_user_id: 'ディスコードサーバーのメンバーを右クリックすると出てくるメニューから、「IDをコピー」をクリックで取れるIDのことです', 61 | disabled_id_copy: '「IDをコピー」がでない', 62 | how_to_enable_copy: '「IDをコピー」を出すには', 63 | on_developer_mode: 'Discordで「開発者モード」を有効にすると表示ができるようになります。', 64 | open_user_setting: 'ユーザー設定を開いてから', 65 | check_developer_mode: '詳細設定の開発モードにチェックを入れると有効になります。', 66 | }, 67 | }; 68 | export default ja; 69 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | server: { 8 | open: true, 9 | }, 10 | }) 11 | --------------------------------------------------------------------------------