summaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
authorHaishan <[email protected]>2020-04-26 17:35:03 +0800
committerHaishan <[email protected]>2020-04-26 17:59:02 +0800
commit94e2b1e3985f8f4cfeb26a43c59cada184c7d4aa (patch)
tree2d88e5e3ace986e1c08f3eca5e787d1339249dd2 /src/components
parent7cdbba5bf47062f80a0dc7d80a62ff977d4f568e (diff)
feat: allow change proxies sorting in group
Diffstat (limited to 'src/components')
-rw-r--r--src/components/Modal.module.css2
-rw-r--r--src/components/ModalCloseAllConnections.module.css2
-rw-r--r--src/components/Proxies.js62
-rw-r--r--src/components/Proxies.module.css10
-rw-r--r--src/components/ProxyGroup.js90
-rw-r--r--src/components/ProxyProvider.js41
-rw-r--r--src/components/Root.css20
-rw-r--r--src/components/proxies/Settings.js75
-rw-r--r--src/components/proxies/Settings.module.css17
-rw-r--r--src/components/rtf.css9
-rw-r--r--src/components/shared/BaseModal.js30
-rw-r--r--src/components/shared/BaseModal.module.css17
-rw-r--r--src/components/shared/Select.module.css29
-rw-r--r--src/components/shared/Select.tsx21
-rw-r--r--src/components/svg/Equalizer.tsx30
15 files changed, 366 insertions, 89 deletions
diff --git a/src/components/Modal.module.css b/src/components/Modal.module.css
index 1b183bc..6192a1f 100644
--- a/src/components/Modal.module.css
+++ b/src/components/Modal.module.css
@@ -10,7 +10,7 @@
.content {
outline: none;
- position: absolute;
+ position: relative;
color: #ddd;
top: 50%;
left: 50%;
diff --git a/src/components/ModalCloseAllConnections.module.css b/src/components/ModalCloseAllConnections.module.css
index f3b54c1..9bb7c6a 100644
--- a/src/components/ModalCloseAllConnections.module.css
+++ b/src/components/ModalCloseAllConnections.module.css
@@ -6,7 +6,7 @@
color: var(--color-text);
max-width: 300px;
line-height: 1.4;
- transform: translate(-50%, -50%) scale(1.5);
+ transform: translate(-50%, -50%) scale(1.2);
opacity: 0.6;
transition: all 0.3s ease;
}
diff --git a/src/components/Proxies.js b/src/components/Proxies.js
index acb26dd..729b57a 100644
--- a/src/components/Proxies.js
+++ b/src/components/Proxies.js
@@ -1,39 +1,34 @@
import React from 'react';
-import { connect, useStoreActions } from './StateProvider';
+import { connect } from './StateProvider';
+import Button from './Button';
import ContentHeader from './ContentHeader';
import ProxyGroup from './ProxyGroup';
-import { Zap, Filter, Circle } from 'react-feather';
+import BaseModal from './shared/BaseModal';
+import Settings from './proxies/Settings';
+import Equalizer from './svg/Equalizer';
+import { Zap } from 'react-feather';
import ProxyProviderList from './ProxyProviderList';
-import { Fab, Action } from 'react-tiny-fab';
+import { Fab } from 'react-tiny-fab';
import './rtf.css';
import s0 from './Proxies.module.css';
import {
getDelay,
- getRtFilterSwitch,
getProxyGroupNames,
getProxyProviders,
fetchProxies,
- requestDelayAll
+ requestDelayAll,
} from '../store/proxies';
import { getClashAPIConfig } from '../store/app';
-const { useEffect, useCallback, useRef } = React;
+const { useState, useEffect, useCallback, useRef } = React;
-function Proxies({
- dispatch,
- groupNames,
- delay,
- proxyProviders,
- apiConfig,
- filterZeroRT
-}) {
+function Proxies({ dispatch, groupNames, delay, proxyProviders, apiConfig }) {
const refFetchedTimestamp = useRef({});
- const { toggleUnavailableProxiesFilter } = useStoreActions();
const requestDelayAllFn = useCallback(
() => dispatch(requestDelayAll(apiConfig)),
[apiConfig, dispatch]
@@ -62,11 +57,27 @@ function Proxies({
return () => window.removeEventListener('focus', fn, false);
}, [fetchProxiesHooked]);
+ const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
+ const closeSettingsModal = useCallback(() => {
+ setIsSettingsModalOpen(false);
+ }, []);
+
return (
<>
+ <div className={s0.topBar}>
+ <Button kind="minimal" onClick={() => setIsSettingsModalOpen(true)}>
+ <Equalizer size={16} />
+ </Button>
+ </div>
+ <BaseModal
+ isOpen={isSettingsModalOpen}
+ onRequestClose={closeSettingsModal}
+ >
+ <Settings />
+ </BaseModal>
<ContentHeader title="Proxies" />
<div>
- {groupNames.map(groupName => {
+ {groupNames.map((groupName) => {
return (
<div className={s0.group} key={groupName}>
<ProxyGroup
@@ -81,27 +92,20 @@ function Proxies({
</div>
<ProxyProviderList items={proxyProviders} />
<div style={{ height: 60 }} />
- <Fab icon={<Circle />}>
- <Action text="Test Latency" onClick={requestDelayAllFn}>
- <Zap width={16} />
- </Action>
- <Action
- text={(filterZeroRT ? 'Show' : 'Hide') + ' Unavailable Proxies'}
- onClick={toggleUnavailableProxiesFilter}
- >
- <Filter width={16} />
- </Action>
- </Fab>
+ <Fab
+ icon={<Zap width={16} />}
+ onClick={requestDelayAllFn}
+ text="Test Latency"
+ ></Fab>
</>
);
}
-const mapState = s => ({
+const mapState = (s) => ({
apiConfig: getClashAPIConfig(s),
groupNames: getProxyGroupNames(s),
proxyProviders: getProxyProviders(s),
delay: getDelay(s),
- filterZeroRT: getRtFilterSwitch(s)
});
export default connect(mapState)(Proxies);
diff --git a/src/components/Proxies.module.css b/src/components/Proxies.module.css
index 5520a2e..2a72f51 100644
--- a/src/components/Proxies.module.css
+++ b/src/components/Proxies.module.css
@@ -1,3 +1,13 @@
+.topBar {
+ position: sticky;
+ top: 0;
+ z-index: 1;
+ background: var(--color-background);
+ display: flex;
+ justify-content: flex-end;
+ padding: 5px 5px 2px 0;
+}
+
.group {
padding: 10px 15px;
@media (--breakpoint-not-small) {
diff --git a/src/components/ProxyGroup.js b/src/components/ProxyGroup.js
index d7cf203..b1a53c0 100644
--- a/src/components/ProxyGroup.js
+++ b/src/components/ProxyGroup.js
@@ -3,8 +3,12 @@ import cx from 'classnames';
import memoizeOne from 'memoize-one';
import { connect, useStoreActions } from './StateProvider';
-import { getProxies, getRtFilterSwitch } from '../store/proxies';
-import { getCollapsibleIsOpen } from '../store/app';
+import { getProxies } from '../store/proxies';
+import {
+ getCollapsibleIsOpen,
+ getProxySortBy,
+ getHideUnavailableProxies,
+} from '../store/app';
import CollapsibleSectionHeader from './CollapsibleSectionHeader';
import Proxy, { ProxySmall } from './Proxy';
@@ -18,7 +22,7 @@ function ProxyGroup({ name, all, type, now, isOpen, apiConfig, dispatch }) {
const isSelectable = useMemo(() => type === 'Selector', [type]);
const {
- app: { updateCollapsibleIsOpen }
+ app: { updateCollapsibleIsOpen },
} = useStoreActions();
const toggle = useCallback(() => {
@@ -26,7 +30,7 @@ function ProxyGroup({ name, all, type, now, isOpen, apiConfig, dispatch }) {
}, [isOpen, updateCollapsibleIsOpen, name]);
const itemOnTapCallback = useCallback(
- proxyName => {
+ (proxyName) => {
if (!isSelectable) return;
dispatch(switchProxy(apiConfig, name, proxyName));
},
@@ -60,23 +64,23 @@ type ProxyListProps = {
all: string[],
now?: string,
isSelectable?: boolean,
- itemOnTapCallback?: string => void,
- show?: boolean
+ itemOnTapCallback?: (string) => void,
+ show?: boolean,
};
export function ProxyList({
all,
now,
isSelectable,
itemOnTapCallback,
- sortedAll
+ sortedAll,
}: ProxyListProps) {
const proxies = sortedAll || all;
return (
<div className={s0.list}>
- {proxies.map(proxyName => {
+ {proxies.map((proxyName) => {
const proxyClassName = cx(s0.proxy, {
- [s0.proxySelectable]: isSelectable
+ [s0.proxySelectable]: isSelectable,
});
return (
<div
@@ -107,7 +111,7 @@ const getSortDelay = (d, w) => {
};
function filterAvailableProxies(list, delay) {
- return list.filter(name => {
+ return list.filter((name) => {
const d = delay[name];
if (d === undefined) {
return true;
@@ -120,19 +124,50 @@ function filterAvailableProxies(list, delay) {
});
}
-function filterAvailableProxiesAndSortImpl(all, delay, filterByRt) {
+const ProxySortingFns = {
+ Natural: (proxies, _delay) => {
+ return proxies;
+ },
+ LatencyAsc: (proxies, delay) => {
+ return proxies.sort((a, b) => {
+ const d1 = getSortDelay(delay[a], 999999);
+ const d2 = getSortDelay(delay[b], 999999);
+ return d1 - d2;
+ });
+ },
+ LatencyDesc: (proxies, delay) => {
+ return proxies.sort((a, b) => {
+ const d1 = getSortDelay(delay[a], 999999);
+ const d2 = getSortDelay(delay[b], 999999);
+ return d2 - d1;
+ });
+ },
+ NameAsc: (proxies) => {
+ return proxies.sort();
+ },
+ NameDesc: (proxies) => {
+ return proxies.sort((a, b) => {
+ if (a > b) return -1;
+ if (a < b) return 1;
+ return 0;
+ });
+ },
+};
+
+function filterAvailableProxiesAndSortImpl(
+ all,
+ delay,
+ hideUnavailableProxies,
+ proxySortBy
+) {
// all is freezed
let filtered = [...all];
- if (filterByRt) {
+ if (hideUnavailableProxies) {
filtered = filterAvailableProxies(all, delay);
}
-
- return filtered.sort((first, second) => {
- const d1 = getSortDelay(delay[first], 999999);
- const d2 = getSortDelay(delay[second], 999999);
- return d1 - d2;
- });
+ return ProxySortingFns[proxySortBy](filtered, delay);
}
+
export const filterAvailableProxiesAndSort = memoizeOne(
filterAvailableProxiesAndSortImpl
);
@@ -141,13 +176,13 @@ export function ProxyListSummaryView({
all,
now,
isSelectable,
- itemOnTapCallback
+ itemOnTapCallback,
}: ProxyListProps) {
return (
<div className={s0.list}>
- {all.map(proxyName => {
+ {all.map((proxyName) => {
const proxyClassName = cx(s0.proxy, {
- [s0.proxySelectable]: isSelectable
+ [s0.proxySelectable]: isSelectable,
});
return (
<div
@@ -168,14 +203,21 @@ export function ProxyListSummaryView({
export default connect((s, { name, delay }) => {
const proxies = getProxies(s);
- const filterByRt = getRtFilterSwitch(s);
const collapsibleIsOpen = getCollapsibleIsOpen(s);
+ const proxySortBy = getProxySortBy(s);
+ const hideUnavailableProxies = getHideUnavailableProxies(s);
+
const group = proxies[name];
const { all, type, now } = group;
return {
- all: filterAvailableProxiesAndSort(all, delay, filterByRt),
+ all: filterAvailableProxiesAndSort(
+ all,
+ delay,
+ hideUnavailableProxies,
+ proxySortBy
+ ),
type,
now,
- isOpen: collapsibleIsOpen[`proxyGroup:${name}`]
+ isOpen: collapsibleIsOpen[`proxyGroup:${name}`],
};
})(ProxyGroup);
diff --git a/src/components/ProxyProvider.js b/src/components/ProxyProvider.js
index 9486128..64dc93b 100644
--- a/src/components/ProxyProvider.js
+++ b/src/components/ProxyProvider.js
@@ -9,16 +9,20 @@ import CollapsibleSectionHeader from './CollapsibleSectionHeader';
import {
ProxyList,
ProxyListSummaryView,
- filterAvailableProxiesAndSort
+ filterAvailableProxiesAndSort,
} from './ProxyGroup';
import Button from './Button';
-import { getClashAPIConfig, getCollapsibleIsOpen } from '../store/app';
+import {
+ getClashAPIConfig,
+ getCollapsibleIsOpen,
+ getProxySortBy,
+ getHideUnavailableProxies,
+} from '../store/app';
import {
getDelay,
- getRtFilterSwitch,
updateProviderByName,
- healthcheckProviderByName
+ healthcheckProviderByName,
} from '../store/proxies';
import s from './ProxyProvider.module.css';
@@ -31,8 +35,8 @@ type Props = {
type: 'Proxy' | 'Rule',
vehicleType: 'HTTP' | 'File' | 'Compatible',
updatedAt?: string,
- dispatch: any => void,
- isOpen: boolean
+ dispatch: (any) => void,
+ isOpen: boolean,
};
function ProxyProvider({
@@ -42,7 +46,7 @@ function ProxyProvider({
updatedAt,
isOpen,
dispatch,
- apiConfig
+ apiConfig,
}: Props) {
const [isHealthcheckLoading, setIsHealthcheckLoading] = useState(false);
const updateProvider = useCallback(
@@ -56,7 +60,7 @@ function ProxyProvider({
}, [apiConfig, dispatch, name, setIsHealthcheckLoading]);
const {
- app: { updateCollapsibleIsOpen }
+ app: { updateCollapsibleIsOpen },
} = useStoreActions();
// const [isCollapsibleOpen, setCollapsibleOpen] = useState(false);
@@ -101,11 +105,11 @@ function ProxyProvider({
const button = {
rest: { scale: 1 },
// hover: { scale: 1.1 },
- pressed: { scale: 0.95 }
+ pressed: { scale: 0.95 },
};
const arrow = {
rest: { rotate: 0 },
- hover: { rotate: 360, transition: { duration: 0.3 } }
+ hover: { rotate: 360, transition: { duration: 0.3 } },
};
function Refresh() {
return (
@@ -124,18 +128,23 @@ function Refresh() {
}
const mapState = (s, { proxies, name }) => {
- const filterByRt = getRtFilterSwitch(s);
+ const hideUnavailableProxies = getHideUnavailableProxies(s);
const delay = getDelay(s);
const collapsibleIsOpen = getCollapsibleIsOpen(s);
const apiConfig = getClashAPIConfig(s);
+
+ const proxySortBy = getProxySortBy(s);
+
return {
apiConfig,
- proxies: filterAvailableProxiesAndSort(proxies, delay, filterByRt),
- isOpen: collapsibleIsOpen[`proxyProvider:${name}`]
+ proxies: filterAvailableProxiesAndSort(
+ proxies,
+ delay,
+ hideUnavailableProxies,
+ proxySortBy
+ ),
+ isOpen: collapsibleIsOpen[`proxyProvider:${name}`],
};
};
-// const mapState = s => ({
-// apiConfig: getClashAPIConfig(s)
-// });
export default connect(mapState)(ProxyProvider);
diff --git a/src/components/Root.css b/src/components/Root.css
index fd7d6d9..22cae24 100644
--- a/src/components/Root.css
+++ b/src/components/Root.css
@@ -1,15 +1,3 @@
-@font-face {
- font-family: 'Roboto Mono';
- font-style: normal;
- font-weight: 400;
- src: local('Roboto Mono'), local('RobotoMono-Regular'),
- url('https://cdn.jsdelivr.net/npm/@hsjs/[email protected]/robotomono/v5/L0x5DF4xlVMF-BfR8bXMIjhLq3-cXbKD.woff2')
- format('woff2');
- unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
- U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
- U+FEFF, U+FFFD;
-}
-
.relative {
position: relative;
}
@@ -88,6 +76,7 @@ body.dark {
--color-background: #202020;
--color-text: #ddd;
--color-text-secondary: #ccc;
+ --color-text-highlight: #fff;
--color-bg-sidebar: #2d2d30;
--color-sb-active-row-bg: #494b4e;
--color-input-bg: #2d2d30;
@@ -95,18 +84,22 @@ body.dark {
--color-toggle-bg: #353535;
--color-toggle-selected: #181818;
--color-icon: #c7c7c7;
+ --color-separator: #333;
--color-btn-bg: #232323;
--color-btn-fg: #bebebe;
--color-bg-proxy: #303030;
--color-row-odd: #282828;
--bg-modal: #1f1f20;
--bg-near-transparent: rgba(255, 255, 255, 0.1);
+ --select-border-color: #040404;
+ --select-bg-hover: url(data:image/svg+xml,%0A%20%20%20%20%3Csvg%20width%3D%228%22%20height%3D%2224%22%20viewBox%3D%220%200%208%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%20%20%3Cpath%20d%3D%22M4%207L7%2011H1L4%207Z%22%20fill%3D%22%23ffffff%22%20%2F%3E%0A%20%20%20%20%20%20%3Cpath%20d%3D%22M4%2017L1%2013L7%2013L4%2017Z%22%20fill%3D%22%23ffffff%22%20%2F%3E%0A%20%20%20%20%3C%2Fsvg%3E%0A%20%20);
}
body.light {
--color-background: #fbfbfb;
--color-text: #222;
--color-text-secondary: #646464;
+ --color-text-highlight: #040404;
--color-bg-sidebar: #e7e7e7;
--color-sb-active-row-bg: #d0d0d0;
--color-input-bg: #ffffff;
@@ -114,12 +107,15 @@ body.light {
--color-toggle-bg: #ffffff;
--color-toggle-selected: #d7d7d7;
--color-icon: #5b5b5b;
+ --color-separator: #ccc;
--color-btn-bg: #f4f4f4;
--color-btn-fg: #101010;
--color-bg-proxy: #e7e7e7;
--color-row-odd: #f5f5f5;
--bg-modal: #fbfbfb;
--bg-near-transparent: rgba(0, 0, 0, 0.1);
+ --select-border-color: #999999;
+ --select-bg-hover: url(data:image/svg+xml,%0A%20%20%20%20%3Csvg%20width%3D%228%22%20height%3D%2224%22%20viewBox%3D%220%200%208%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%20%20%3Cpath%20d%3D%22M4%207L7%2011H1L4%207Z%22%20fill%3D%22%23222222%22%20%2F%3E%0A%20%20%20%20%20%20%3Cpath%20d%3D%22M4%2017L1%2013L7%2013L4%2017Z%22%20fill%3D%22%23222222%22%20%2F%3E%0A%20%20%20%20%3C%2Fsvg%3E%0A%20%20);
}
.flexCenter {
diff --git a/src/components/proxies/Settings.js b/src/components/proxies/Settings.js
new file mode 100644
index 0000000..e21ae72
--- /dev/null
+++ b/src/components/proxies/Settings.js
@@ -0,0 +1,75 @@
+import * as React from 'react';
+
+import { getProxySortBy, getHideUnavailableProxies } from '../../store/app';
+
+import Switch from '../SwitchThemed';
+import { connect, useStoreActions } from '../StateProvider';
+import Select from '../shared/Select';
+import s from './Settings.module.css';
+
+const options = [
+ ['Natural', 'Original order in config file'],
+ ['LatencyAsc', 'By latency from small to big'],
+ ['LatencyDesc', 'By latency from big to small'],
+ ['NameAsc', 'By name alphabetically (A-Z)'],
+ ['NameDesc', 'By name alphabetically (Z-A)'],
+];
+
+const { useCallback } = React;
+
+function Settings({ appConfig }) {
+ const {
+ app: { updateAppConfig },
+ } = useStoreActions();
+
+ const handleProxySortByOnChange = useCallback(
+ (e) => {
+ updateAppConfig('proxySortBy', e.target.value);
+ },
+ [updateAppConfig]
+ );
+
+ const handleHideUnavailablesSwitchOnChange = useCallback(
+ (v) => {
+ updateAppConfig('hideUnavailableProxies', v);
+ },
+ [updateAppConfig]
+ );
+ return (
+ <>
+ <div className={s.labeledInput}>
+ <span>Sorting in group</span>
+ <div>
+ <Select
+ options={options}
+ selected={appConfig.proxySortBy}
+ onChange={handleProxySortByOnChange}
+ />
+ </div>
+ </div>
+ <hr />
+ <div className={s.labeledInput}>
+ <span>Hide unavailable proxies</span>
+ <div>
+ <Switch
+ name="hideUnavailableProxies"
+ checked={appConfig.hideUnavailableProxies}
+ onChange={handleHideUnavailablesSwitchOnChange}
+ />
+ </div>
+ </div>
+ </>
+ );
+}
+
+const mapState = (s) => {
+ const proxySortBy = getProxySortBy(s);
+ const hideUnavailableProxies = getHideUnavailableProxies(s);
+ return {
+ appConfig: {
+ proxySortBy,
+ hideUnavailableProxies,
+ },
+ };
+};
+export default connect(mapState)(Settings);
diff --git a/src/components/proxies/Settings.module.css b/src/components/proxies/Settings.module.css
new file mode 100644
index 0000000..364d07d
--- /dev/null
+++ b/src/components/proxies/Settings.module.css
@@ -0,0 +1,17 @@
+.labeledInput {
+ max-width: 85vw;
+ width: 400px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 13px;
+ padding: 13px 0;
+}
+
+hr {
+ height: 1px;
+ background-color: var(--color-separator);
+ border: none;
+ outline: none;
+ margin: 1rem 0px;
+}
diff --git a/src/components/rtf.css b/src/components/rtf.css
index 1a68f6b..a61b35d 100644
--- a/src/components/rtf.css
+++ b/src/components/rtf.css
@@ -13,7 +13,7 @@
}
.rtf.open .rtf--mb > * {
transform-origin: center center;
- transform: rotate(315deg);
+ transform: rotate(360deg);
transition: ease-in-out transform 0.2s;
}
.rtf.open .rtf--mb > ul {
@@ -107,18 +107,15 @@
}
.rtf--mb {
- height: 56px;
- width: 56px;
+ height: 48px;
+ width: 48px;
z-index: 9999;
- /* background-color: #666666; */
background: #387cec;
- /* background: var(--color-btn-bg); */
display: inline-flex;
justify-content: center;
align-items: center;
position: relative;
border: none;
- /* border: 1px solid #555; */
border-radius: 50%;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.14), 0 4px 8px rgba(0, 0, 0, 0.28);
cursor: pointer;
diff --git a/src/components/shared/BaseModal.js b/src/components/shared/BaseModal.js
new file mode 100644
index 0000000..8bbdbf4
--- /dev/null
+++ b/src/components/shared/BaseModal.js
@@ -0,0 +1,30 @@
+import * as React from 'react';
+
+import Modal from 'react-modal';
+import cx from 'classnames';
+
+import modalStyle from '../Modal.module.css';
+import s from './BaseModal.module.css';
+
+const { useMemo } = React;
+
+export default function BaseModal({ isOpen, onRequestClose, children }) {
+ const className = useMemo(
+ () => ({
+ base: cx(modalStyle.content, s.cnt),
+ afterOpen: s.afterOpen,
+ beforeClose: '',
+ }),
+ []
+ );
+ return (
+ <Modal
+ isOpen={isOpen}
+ onRequestClose={onRequestClose}
+ className={className}
+ overlayClassName={cx(modalStyle.overlay, s.overlay)}
+ >
+ {children}
+ </Modal>
+ );
+}
diff --git a/src/components/shared/BaseModal.module.css b/src/components/shared/BaseModal.module.css
new file mode 100644
index 0000000..1d206e1
--- /dev/null
+++ b/src/components/shared/BaseModal.module.css
@@ -0,0 +1,17 @@
+.overlay {
+ background-color: rgba(0, 0, 0, 0.6);
+}
+.cnt {
+ position: absolute;
+ background-color: var(--bg-modal);
+ color: var(--color-text);
+ line-height: 1.4;
+ opacity: 0.6;
+ transition: all 0.3s ease;
+ transform: translate(-50%, -50%) scale(1.2);
+ box-shadow: rgba(0, 0, 0, 0.12) 0px 4px 4px, rgba(0, 0, 0, 0.24) 0px 16px 32px;
+}
+.afterOpen {
+ opacity: 1;
+ transform: translate(-50%, -50%) scale(1);
+}
diff --git a/src/components/shared/Select.module.css b/src/components/shared/Select.module.css
new file mode 100644
index 0000000..32343ea
--- /dev/null
+++ b/src/components/shared/Select.module.css
@@ -0,0 +1,29 @@
+.select {
+ height: 30px;
+ width: 100%;
+ padding-left: 8px;
+ background-color: transparent;
+ appearance: none;
+ /* background-color: rgb(36, 36, 36); */
+ /* -webkit-appearance: none; */
+ color: var(--color-text);
+ /* color: rgb(153, 153, 153); */
+ padding-right: 20px;
+ background-image: url(data:image/svg+xml,%0A%20%20%20%20%3Csvg%20width%3D%228%22%20height%3D%2224%22%20viewBox%3D%220%200%208%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%20%20%3Cpath%20d%3D%22M4%207L7%2011H1L4%207Z%22%20fill%3D%22%23999999%22%20%2F%3E%0A%20%20%20%20%20%20%3Cpath%20d%3D%22M4%2017L1%2013L7%2013L4%2017Z%22%20fill%3D%22%23999999%22%20%2F%3E%0A%20%20%20%20%3C%2Fsvg%3E%0A%20%20);
+ border-radius: 4px;
+ border-width: 1px;
+ border-style: solid;
+ border-image: initial;
+ border-color: var(--select-border-color);
+ transition: all 100ms ease 0s;
+ background-position: calc(100% - 8px) center;
+ background-repeat: no-repeat;
+}
+
+.select:hover,
+.select:focus {
+ border-color: rgb(52, 52, 52);
+ outline: none !important;
+ color: var(--color-text-highlight);
+ background-image: var(--select-bg-hover);
+}
diff --git a/src/components/shared/Select.tsx b/src/components/shared/Select.tsx
new file mode 100644
index 0000000..03ac084
--- /dev/null
+++ b/src/components/shared/Select.tsx
@@ -0,0 +1,21 @@
+import * as React from 'react';
+
+import s from './Select.module.css';
+
+type Props = {
+ options: Array<string[]>;
+ selected: string;
+ onChange: (event: React.ChangeEvent<HTMLSelectElement>) => any;
+};
+
+export default function Select({ options, selected, onChange }: Props) {
+ return (
+ <select className={s.select} value={selected} onChange={onChange}>
+ {options.map(([value, name]) => (
+ <option key={value} value={value}>
+ {name}
+ </option>
+ ))}
+ </select>
+ );
+}
diff --git a/src/components/svg/Equalizer.tsx b/src/components/svg/Equalizer.tsx
new file mode 100644
index 0000000..274720f
--- /dev/null
+++ b/src/components/svg/Equalizer.tsx
@@ -0,0 +1,30 @@
+import * as React from 'react';
+
+type Props = {
+ size?: number;
+ color?: string;
+};
+
+export default function Equalizer({
+ color = 'currentColor',
+ size = 24,
+}: Props) {
+ return (
+ <svg
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 24 24"
+ width={size}
+ height={size}
+ stroke={color}
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ >
+ <path d="M2 6h9M18.5 6H22" />
+ <circle cx="16" cy="6" r="2" />
+ <path d="M22 18h-9M6 18H2" />
+ <circle r="2" transform="matrix(-1 0 0 1 8 18)" />
+ </svg>
+ );
+}