diff --git a/babel.config.js b/babel.config.js index 34d07d6..c31161d 100644 --- a/babel.config.js +++ b/babel.config.js @@ -10,7 +10,8 @@ const presets = [ corejs: 3 } ], - '@babel/preset-react' + '@babel/preset-react', + '@babel/preset-flow' ]; const plugins = [ diff --git a/package.json b/package.json index a79a7f7..a93b9ff 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "@babel/plugin-syntax-dynamic-import": "^7.0.0", "@babel/plugin-transform-runtime": "^7.6.2", "@babel/preset-env": "^7.7.1", + "@babel/preset-flow": "^7.7.4", "@babel/preset-react": "^7.7.0", "autoprefixer": "^9.7.1", "babel-eslint": "^10.0.3", diff --git a/src/api/connections.js b/src/api/connections.js index 594fc5e..fbc9abd 100644 --- a/src/api/connections.js +++ b/src/api/connections.js @@ -1,10 +1,39 @@ +import { getURLAndInit } from 'm/request-helper'; + const endpoint = '/connections'; let fetched = false; let subscribers = []; +// see also https://github.com/Dreamacro/clash/blob/dev/constant/metadata.go#L41 +type UUID = string; +type ConnectionItem = { + id: UUID, + metadata: { + network: 'tcp' | 'udp', + type: 'HTTP' | 'HTTP Connect' | 'Socks5' | 'Redir' | 'Unknown', + sourceIP: string, + destinationIP: string, + sourcePort: string, + destinationPort: string, + host: string + }, + upload: number, + download: number, + // e.g. "2019-11-30T22:48:13.416668+08:00", + start: string, + chains: Array, + // e.g. 'Match', 'DomainKeyword' + rule: string +}; +type ConnectionsData = { + downloadTotal: number, + uploadTotal: number, + connections: Array +}; + function appendData(s) { - let o; + let o: ConnectionsData; try { o = JSON.parse(s); } catch (err) { @@ -48,4 +77,9 @@ function subscribe(listener) { }; } -export { fetchData }; +async function closeAllConnections(apiConfig) { + const { url, init } = getURLAndInit(apiConfig); + return await fetch(url + endpoint, { ...init, method: 'DELETE' }); +} + +export { fetchData, closeAllConnections }; diff --git a/src/components/Button.js b/src/components/Button.js index d7928c7..f56049e 100644 --- a/src/components/Button.js +++ b/src/components/Button.js @@ -1,20 +1,29 @@ import React from 'react'; -import PropTypes from 'prop-types'; import s0 from 'c/Button.module.css'; const noop = () => {}; -const Button = React.memo(function Button({ label, onClick = noop }) { +const { memo, forwardRef } = React; + +function Button({ children, label, onClick = noop }, ref) { return ( - ); -}); +} -Button.propTypes = { - label: PropTypes.string.isRequired, - onClick: PropTypes.func -}; +function WithIcon({ text, icon, onClick = noop }, ref) { + return ( + + ); +} -export default Button; +export const ButtonWithIcon = memo(forwardRef(WithIcon)); + +export default memo(forwardRef(Button)); diff --git a/src/components/Button.module.css b/src/components/Button.module.css index 8fb6a92..205bfe9 100644 --- a/src/components/Button.module.css +++ b/src/components/Button.module.css @@ -25,3 +25,11 @@ padding: 6px 12px; } } + +.withIconWrapper { + display: flex; + align-items: center; + .txt { + margin-left: 5px; + } +} diff --git a/src/components/ConnectionTable.js b/src/components/ConnectionTable.js index 17293c0..2e7ba7f 100644 --- a/src/components/ConnectionTable.js +++ b/src/components/ConnectionTable.js @@ -34,6 +34,13 @@ function renderCell(cell, now) { } } +const tableState = { + sortBy: [ + // maintain a more stable order + { id: 'start', desc: true } + ] +}; + function Table({ data }) { const now = new Date(); const { @@ -45,7 +52,8 @@ function Table({ data }) { } = useTable( { columns, - data + data, + initialState: tableState }, useSortBy ); diff --git a/src/components/Connections.js b/src/components/Connections.js index 2607606..8953722 100644 --- a/src/components/Connections.js +++ b/src/components/Connections.js @@ -4,12 +4,15 @@ import ConnectionTable from 'c/ConnectionTable'; import useRemainingViewPortHeight from '../hooks/useRemainingViewPortHeight'; import { useStoreState } from 'm/store'; import { getClashAPIConfig } from 'd/app'; +import { X as IconClose } from 'react-feather'; import SvgYacd from './SvgYacd'; +import { ButtonWithIcon } from './Button'; +import ModalCloseAllConnections from './ModalCloseAllConnections'; import * as connAPI from '../api/connections'; import s from './Connections.module.css'; -const { useEffect, useState, useRef } = React; +const { useEffect, useState, useRef, useCallback, useMemo } = React; const paddingBottom = 30; @@ -31,6 +34,17 @@ function Conn() { const [refContainer, containerHeight] = useRemainingViewPortHeight(); const config = useStoreState(getClashAPIConfig); const [conns, setConns] = useState([]); + const [isCloseAllModalOpen, setIsCloseAllModalOpen] = useState(false); + const openCloseAllModal = useCallback(() => setIsCloseAllModalOpen(true), []); + const closeCloseAllModal = useCallback( + () => setIsCloseAllModalOpen(false), + [] + ); + const closeAllConnections = useCallback(() => { + connAPI.closeAllConnections(config); + closeCloseAllModal(); + }, [config, closeCloseAllModal]); + const iconClose = useMemo(() => , []); const prevConnsRef = useRef(conns); useEffect(() => { function read({ connections }) { @@ -65,6 +79,18 @@ function Conn() { )} +
+ +
+ ); } diff --git a/src/components/ModalCloseAllConnections.js b/src/components/ModalCloseAllConnections.js new file mode 100644 index 0000000..8a06393 --- /dev/null +++ b/src/components/ModalCloseAllConnections.js @@ -0,0 +1,44 @@ +import React from 'react'; + +import Modal from 'react-modal'; +import Button from './Button'; +import cx from 'classnames'; + +import modalStyle from './Modal.module.css'; +import s from './ModalCloseAllConnections.module.css'; + +const { useRef, useCallback, useMemo } = React; + +export default function Comp({ isOpen, onRequestClose, primaryButtonOnTap }) { + const primaryButtonRef = useRef(null); + const onAfterOpen = useCallback(() => { + primaryButtonRef.current.focus(); + }, []); + const className = useMemo( + () => ({ + base: cx(modalStyle.content, s.cnt), + afterOpen: s.afterOpen, + beforeClose: '' + }), + [] + ); + return ( + +

Are you sure you want to close all connections?

+
+ + {/* im lazy :) */} +
+ +
+ + ); +} diff --git a/src/components/ModalCloseAllConnections.module.css b/src/components/ModalCloseAllConnections.module.css new file mode 100644 index 0000000..f3b54c1 --- /dev/null +++ b/src/components/ModalCloseAllConnections.module.css @@ -0,0 +1,23 @@ +.overlay { + background-color: rgba(0, 0, 0, 0.6); +} +.cnt { + background-color: var(--bg-modal); + color: var(--color-text); + max-width: 300px; + line-height: 1.4; + transform: translate(-50%, -50%) scale(1.5); + opacity: 0.6; + transition: all 0.3s ease; +} +.afterOpen { + opacity: 1; + transform: translate(-50%, -50%) scale(1); +} + +.btngrp { + display: flex; + align-items: center; + justify-content: center; + margin-top: 30px; +} diff --git a/src/components/Proxies.js b/src/components/Proxies.js index c66ed83..fef6a14 100644 --- a/src/components/Proxies.js +++ b/src/components/Proxies.js @@ -1,9 +1,10 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { useActions, useStoreState } from 'm/store'; import ContentHeader from 'c/ContentHeader'; import ProxyGroup from 'c/ProxyGroup'; -import Button from 'c/Button'; +import { ButtonWithIcon } from 'c/Button'; +import { Zap } from 'react-feather'; import s0 from 'c/Proxies.module.css'; @@ -14,6 +15,8 @@ import { requestDelayAll } from 'd/proxies'; +const { useEffect, useMemo } = React; + const mapStateToProps = s => ({ proxies: getProxies(s), groupNames: getProxyGroupNames(s) @@ -33,13 +36,19 @@ export default function Proxies() { })(); }, [fetchProxies, requestDelayAll]); const { groupNames } = useStoreState(mapStateToProps); + const icon = useMemo(() => , []); return ( <>
-
- */}
{groupNames.map(groupName => { return ( diff --git a/src/components/Proxies.module.css b/src/components/Proxies.module.css index a832ebe..72b70fb 100644 --- a/src/components/Proxies.module.css +++ b/src/components/Proxies.module.css @@ -8,10 +8,3 @@ padding: 10px 40px; } } - -.fabgrp { - position: fixed; - z-index: 1; - right: 20px; - bottom: 20px; -} diff --git a/src/components/Root.css b/src/components/Root.css index 88b3d6c..ae25dca 100644 --- a/src/components/Root.css +++ b/src/components/Root.css @@ -92,6 +92,7 @@ body.dark { --color-btn-fg: #bebebe; --color-bg-proxy-selected: #303030; --color-row-odd: #282828; + --bg-modal: #1f1f20; } body.light { @@ -109,4 +110,12 @@ body.light { --color-btn-fg: #101010; --color-bg-proxy-selected: #cfcfcf; --color-row-odd: #f5f5f5; + --bg-modal: #fbfbfb; +} + +/* TODO remove fabgrp in component css files */ +.fabgrp { + position: fixed; + right: 20px; + bottom: 20px; } diff --git a/src/components/Rules.js b/src/components/Rules.js index 96056ee..2e2db3c 100644 --- a/src/components/Rules.js +++ b/src/components/Rules.js @@ -1,7 +1,8 @@ -import React, { memo, useEffect } from 'react'; +import React from 'react'; import { useActions, useStoreState } from 'm/store'; -import Button from 'c/Button'; +import { ButtonWithIcon } from 'c/Button'; import { FixedSizeList as List, areEqual } from 'react-window'; +import { RotateCw } from 'react-feather'; import ContentHeader from 'c/ContentHeader'; import Rule from 'c/Rule'; @@ -10,7 +11,9 @@ import useRemainingViewPortHeight from '../hooks/useRemainingViewPortHeight'; import { getRules, fetchRules, fetchRulesOnce } from 'd/rules'; -import s0 from './Rules.module.css'; +const { memo, useEffect, useMemo } = React; + +// import s from './Rules.module.css'; const paddingBottom = 30; const mapStateToProps = s => ({ @@ -43,7 +46,7 @@ export default function Rules() { fetchRulesOnce(); }, [fetchRulesOnce]); const [refRulesContainer, containerHeight] = useRemainingViewPortHeight(); - + const refreshIcon = useMemo(() => , []); return (
@@ -60,8 +63,12 @@ export default function Rules() { {Row}
-
-
); diff --git a/src/components/Rules.module.css b/src/components/Rules.module.css index 1fb94eb..79a9626 100644 --- a/src/components/Rules.module.css +++ b/src/components/Rules.module.css @@ -1,5 +1 @@ -.fabgrp { - position: fixed; - right: 20px; - bottom: 20px; -} +/* */ diff --git a/src/components/SvgYacd.js b/src/components/SvgYacd.js index 42cd425..b1bc8f0 100644 --- a/src/components/SvgYacd.js +++ b/src/components/SvgYacd.js @@ -12,8 +12,6 @@ function SvgYacd({ c1 = '#eee' }) { const faceClasName = cx({ [s.path]: animate }); - // fill="#2A477A" - return (