├── .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 | 
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 |
22 |
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 |
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 |
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 |
71 |
72 |
73 |
79 |
80 |
81 |
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 | }
31 | onClick={() => setOpen(true)}>
32 | {t("how_to_use")}
33 |
34 |
35 |
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 |
--------------------------------------------------------------------------------