| New file |
| | |
| | | extends: |
| | | - '@commitlint/config-conventional' |
| New file |
| | |
| | | parser: '@typescript-eslint/parser' |
| | | |
| | | parserOptions: |
| | | ecmaFeatures: |
| | | jsx: true |
| | | ecmaVersion: latest |
| | | sourceType: module |
| | | |
| | | env: |
| | | node: true |
| | | |
| | | plugins: |
| | | - '@typescript-eslint' |
| | | - prettier |
| | | |
| | | extends: |
| | | - eslint:recommended |
| | | - plugin:@typescript-eslint/eslint-recommended |
| | | - plugin:@typescript-eslint/recommended |
| | | - prettier |
| | | |
| | | settings: |
| | | import/resolver: |
| | | typescript: |
| | | node: true |
| | | |
| | | rules: |
| | | '@typescript-eslint/no-unused-vars': |
| | | - error |
| | | - argsIgnorePattern: '^_' |
| | | |
| | | no-empty: off |
| | | |
| | | padding-line-between-statements: |
| | | - error |
| | | - blankLine: always |
| | | prev: '*' |
| | | next: return |
| | | - blankLine: always |
| | | prev: '*' |
| | | next: if |
| | | - blankLine: always |
| | | prev: if |
| | | next: '*' |
| | | - blankLine: always |
| | | prev: '*' |
| | | next: switch |
| | | - blankLine: always |
| | | prev: switch |
| | | next: '*' |
| New file |
| | |
| | | .idea |
| | | node_modules |
| | | dist |
| New file |
| | |
| | | 'package.json': 'sort-package-json' |
| | | '*.{js,jsx,ts,tsx}': 'eslint --fix' |
| | | '*.{js,jsx,ts,tsx,md,html,css,json,yml,yaml}': 'prettier --write' |
| New file |
| | |
| | | dist |
| | | CHANGELOG.md |
| | | pnpm-lock.yaml |
| New file |
| | |
| | | { |
| | | "plugins": [ |
| | | "prettier-plugin-organize-imports", |
| | | "prettier-plugin-tailwindcss" |
| | | ], |
| | | "semi": false, |
| | | "singleQuote": true, |
| | | "endOfLine": "lf" |
| | | } |
| New file |
| | |
| | | # Changelog |
| | | |
| | | ## [1.124.2](https://github.com/MetaCubeX/metacubexd/compare/v1.124.1...v1.124.2) (2023-10-09) |
| | | |
| | | |
| | | ### Bug Fixes |
| | | |
| | | * **setup:** setup with url search string not working properly ([ce4622b](https://github.com/MetaCubeX/metacubexd/commit/ce4622b9543d14b304bf10bcc2d542ee248a14bc)) |
| | | |
| | | ## [1.124.1](https://github.com/MetaCubeX/metacubexd/compare/v1.124.0...v1.124.1) (2023-10-08) |
| | | |
| | | |
| | | ### Bug Fixes |
| | | |
| | | * **setup:** use location info from router instead of browser ([7ecc51c](https://github.com/MetaCubeX/metacubexd/commit/7ecc51c43c581105506f23192f4d7b6938ec944c)) |
| | | |
| | | ## [1.124.0](https://github.com/MetaCubeX/metacubexd/compare/v1.123.0...v1.124.0) (2023-10-04) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * **config:** toast error message when dns query failed for some reason, closes [#321](https://github.com/MetaCubeX/metacubexd/issues/321) ([82c2772](https://github.com/MetaCubeX/metacubexd/commit/82c2772afe913039b0695ef449f9f48b54eb822b)) |
| | | |
| | | ## [1.123.0](https://github.com/MetaCubeX/metacubexd/compare/v1.122.1...v1.123.0) (2023-10-02) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * **config:** display currently selected endpoint url ([59e0b07](https://github.com/MetaCubeX/metacubexd/commit/59e0b076bf42db40c2ff1bafa2ba126f79997d25)) |
| | | |
| | | ## [1.122.1](https://github.com/MetaCubeX/metacubexd/compare/v1.122.0...v1.122.1) (2023-09-28) |
| | | |
| | | |
| | | ### Bug Fixes |
| | | |
| | | * **rule:** rule pages filter logic is not stable, fixes [#313](https://github.com/MetaCubeX/metacubexd/issues/313) ([1f22147](https://github.com/MetaCubeX/metacubexd/commit/1f22147cd14462e11b82cd367cbf7b786358d6e5)) |
| | | |
| | | ## [1.122.0](https://github.com/MetaCubeX/metacubexd/compare/v1.121.0...v1.122.0) (2023-09-28) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * publish gh-pages with tag name as commit message ([336b954](https://github.com/MetaCubeX/metacubexd/commit/336b9545e662b78f5234d5dfcf926e017857c643)) |
| | | |
| | | ## [1.121.0](https://github.com/MetaCubeX/metacubexd/compare/v1.120.0...v1.121.0) (2023-09-27) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * **proxy:** remove collapse component overflow-visible class ([389af52](https://github.com/MetaCubeX/metacubexd/commit/389af52c74e249fac957addd43eeeb8976957022)) |
| | | |
| | | ## [1.120.0](https://github.com/MetaCubeX/metacubexd/compare/v1.119.0...v1.120.0) (2023-09-27) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * **proxy:** compact proxy node card ([40f07b8](https://github.com/MetaCubeX/metacubexd/commit/40f07b8efe7b6d48da0da5db1ce1372d07a14d3c)) |
| | | |
| | | ## [1.119.0](https://github.com/MetaCubeX/metacubexd/compare/v1.118.0...v1.119.0) (2023-09-26) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * **rule:** fuzzy filter ([f53a1a4](https://github.com/MetaCubeX/metacubexd/commit/f53a1a4cb34211d1a9b6c14ed7938702d194eb1d)) |
| | | |
| | | ## [1.118.0](https://github.com/MetaCubeX/metacubexd/compare/v1.117.0...v1.118.0) (2023-09-26) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * **rule:** filter rules and rule providers by search input ([db16200](https://github.com/MetaCubeX/metacubexd/commit/db16200b1ce42f31a813a41b6c3eedbab63baeac)) |
| | | |
| | | ## [1.117.0](https://github.com/MetaCubeX/metacubexd/compare/v1.116.0...v1.117.0) (2023-09-26) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * **rule:** implement virtual scroll on rules and rule providers, fixes [#285](https://github.com/MetaCubeX/metacubexd/issues/285) ([7145cec](https://github.com/MetaCubeX/metacubexd/commit/7145cec62fd4fc409d3f0e46f7aaaf0bea354abd)) |
| | | |
| | | ## [1.116.0](https://github.com/MetaCubeX/metacubexd/compare/v1.115.1...v1.116.0) (2023-09-26) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * bump version ([66d92a9](https://github.com/MetaCubeX/metacubexd/commit/66d92a9b529504e2edbfd087a7d6783b8f257354)) |
| | | |
| | | ## [1.115.1](https://github.com/MetaCubeX/metacubexd/compare/v1.115.0...v1.115.1) (2023-09-25) |
| | | |
| | | |
| | | ### Bug Fixes |
| | | |
| | | * **proxy:** proxy preview dots layout issue ([4b7f4c8](https://github.com/MetaCubeX/metacubexd/commit/4b7f4c899f4549ffc89d84a0a93f5f837918fdb3)) |
| | | |
| | | ## [1.115.0](https://github.com/MetaCubeX/metacubexd/compare/v1.114.0...v1.115.0) (2023-09-25) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * publish gh-pages with force_orphan option enabled ([067bedd](https://github.com/MetaCubeX/metacubexd/commit/067bedd43f9f75310335bc3b97f003ca06ea4d38)) |
| | | |
| | | ## [1.114.0](https://github.com/MetaCubeX/metacubexd/compare/v1.113.1...v1.114.0) (2023-09-25) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * publish compressed dist with tar gzip format ([0c9b31d](https://github.com/MetaCubeX/metacubexd/commit/0c9b31d03f9d6670e1fabbeee4e3791a2d17a775)) |
| | | |
| | | ## [1.113.1](https://github.com/MetaCubeX/metacubexd/compare/v1.113.0...v1.113.1) (2023-09-25) |
| | | |
| | | |
| | | ### Bug Fixes |
| | | |
| | | * **proxy:** proxy provider health check not working, closes [#282](https://github.com/MetaCubeX/metacubexd/issues/282) ([db24ad2](https://github.com/MetaCubeX/metacubexd/commit/db24ad20bb5fbfcf195dc4c71f3024091bf16f1b)) |
| | | |
| | | ## [1.113.0](https://github.com/MetaCubeX/metacubexd/compare/v1.112.0...v1.113.0) (2023-09-24) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * **proxy:** single proxy node latency testing state indicator ([0052ce4](https://github.com/MetaCubeX/metacubexd/commit/0052ce4325fec2d185df98ad187efa70171d0a28)) |
| | | |
| | | ## [1.112.0](https://github.com/MetaCubeX/metacubexd/compare/v1.111.1...v1.112.0) (2023-09-24) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * **proxy:** single proxy node latency test ([9255658](https://github.com/MetaCubeX/metacubexd/commit/9255658e5408b0c48e1f35bdaf8fecf407bba869)) |
| | | |
| | | ## [1.111.1](https://github.com/MetaCubeX/metacubexd/compare/v1.111.0...v1.111.1) (2023-09-24) |
| | | |
| | | |
| | | ### Bug Fixes |
| | | |
| | | * **proxy:** fetch proxies after latency test ([81fe550](https://github.com/MetaCubeX/metacubexd/commit/81fe55091292015b623e2545fd7d0547cfffbed0)) |
| | | |
| | | ## [1.111.0](https://github.com/MetaCubeX/metacubexd/compare/v1.110.0...v1.111.0) (2023-09-23) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * **modal:** adjust padding of the modal header and footer ([53ee2f3](https://github.com/MetaCubeX/metacubexd/commit/53ee2f31b397b3abd6a208a88492be2f6eaaeebe)) |
| | | * **modals:** add modal component ([#268](https://github.com/MetaCubeX/metacubexd/issues/268)) ([eca9a16](https://github.com/MetaCubeX/metacubexd/commit/eca9a160cc1db5558eb1a7ed14c9e544295ca7b4)) |
| | | * **proxy:** shorten the autoCloseConns i18n text ([2ee48c8](https://github.com/MetaCubeX/metacubexd/commit/2ee48c8ac90c79844254ff07a98fe48b50ca68c0)) |
| | | * **theme:** update default theme ([be0bc55](https://github.com/MetaCubeX/metacubexd/commit/be0bc55317006725c0a155162a2b604a888b6847)) |
| | | |
| | | |
| | | ### Bug Fixes |
| | | |
| | | * **rules:** reload button style issue ([04c8d25](https://github.com/MetaCubeX/metacubexd/commit/04c8d2524b6d6b0fb1b2f36b98a0ffe2a20fbf8c)) |
| | | |
| | | ## [1.110.0](https://github.com/MetaCubeX/metacubexd/compare/v1.109.0...v1.110.0) (2023-09-22) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * **config:** new version update available indicator ([857d0c5](https://github.com/MetaCubeX/metacubexd/commit/857d0c5387e9a19c6ca27d408461f10aa8b6ef9e)) |
| | | |
| | | ## [1.109.0](https://github.com/MetaCubeX/metacubexd/compare/v1.108.2...v1.109.0) (2023-09-22) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * **config:** i18n support for port numbers ([07d3e7f](https://github.com/MetaCubeX/metacubexd/commit/07d3e7fdce396fcdfb5740d943fa16c0187f4b63)) |
| | | * **config:** render versions in two grid columns ([64f2d71](https://github.com/MetaCubeX/metacubexd/commit/64f2d7139a036ec4abcd56baab6d0ca0429ecd39)) |
| | | * **i18n:** new language switcher in config page ([21d479a](https://github.com/MetaCubeX/metacubexd/commit/21d479a1df14c5fa0947e1824c7ce77609180d69)) |
| | | * overscroll-y-none ([063cb31](https://github.com/MetaCubeX/metacubexd/commit/063cb31f151e56c6caa46f868bf861fe5748b4c6)) |
| | | |
| | | ## [1.108.2](https://github.com/MetaCubeX/metacubexd/compare/v1.108.1...v1.108.2) (2023-09-22) |
| | | |
| | | |
| | | ### Bug Fixes |
| | | |
| | | * time from now date string not following locale changes ([ba21999](https://github.com/MetaCubeX/metacubexd/commit/ba21999ed17cdf8997207ac7727525ba07ce0676)) |
| | | |
| | | ## [1.108.1](https://github.com/MetaCubeX/metacubexd/compare/v1.108.0...v1.108.1) (2023-09-22) |
| | | |
| | | |
| | | ### Bug Fixes |
| | | |
| | | * **connection:** cannot filter by undefined ([08776a5](https://github.com/MetaCubeX/metacubexd/commit/08776a5bee855d3d7d7fa32b1ca43b41272ce988)) |
| | | |
| | | ## [1.108.0](https://github.com/MetaCubeX/metacubexd/compare/v1.107.0...v1.108.0) (2023-09-22) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * **config:** support tun related configurations ([512accd](https://github.com/MetaCubeX/metacubexd/commit/512accd4257a8f11cd412fb78224eb0b7c832f3e)) |
| | | |
| | | |
| | | ### Bug Fixes |
| | | |
| | | * **connection:** limit the max width of client source ip select, closes [#270](https://github.com/MetaCubeX/metacubexd/issues/270) ([ca1e3ce](https://github.com/MetaCubeX/metacubexd/commit/ca1e3ce82c7a200abb0dd2ac744827a57a410763)) |
| | | |
| | | ## [1.107.0](https://github.com/MetaCubeX/metacubexd/compare/v1.106.0...v1.107.0) (2023-09-22) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * **button:** icon button ([5ef8410](https://github.com/MetaCubeX/metacubexd/commit/5ef8410da5cbc67ad7fc6bd63775e325aaf57828)) |
| | | * **config:** move switch language button into header ([9cc99eb](https://github.com/MetaCubeX/metacubexd/commit/9cc99ebb8e380f9b5e855b6d3250dfe96e237134)) |
| | | * **config:** update config page buttons layout, closes [#263](https://github.com/MetaCubeX/metacubexd/issues/263) ([dd0452d](https://github.com/MetaCubeX/metacubexd/commit/dd0452dcd3f68bad959f756cf191ea1c2470b2c1)) |
| | | |
| | | |
| | | ### Bug Fixes |
| | | |
| | | * **modals:** close icon button style issue ([7f8d71c](https://github.com/MetaCubeX/metacubexd/commit/7f8d71ccc5b847ddb39e332199e21ede43483ea6)) |
| | | |
| | | ## [1.106.0](https://github.com/MetaCubeX/metacubexd/compare/v1.105.0...v1.106.0) (2023-09-22) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * **connection:** filter client sourceIP with tag name support, closes [#264](https://github.com/MetaCubeX/metacubexd/issues/264) ([8df4d7a](https://github.com/MetaCubeX/metacubexd/commit/8df4d7a7e93570c9d4d76b5903907e1bcb2ab45a)) |
| | | |
| | | ## [1.105.0](https://github.com/MetaCubeX/metacubexd/compare/v1.104.0...v1.105.0) (2023-09-21) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * **LogoText:** update logo navigation links ([d7e8c2f](https://github.com/MetaCubeX/metacubexd/commit/d7e8c2f4a43f8805681de725208036fdd0150496)) |
| | | |
| | | ## [1.104.0](https://github.com/MetaCubeX/metacubexd/compare/v1.103.0...v1.104.0) (2023-09-21) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * **connection:** filter connections with sourceIP, closes [#246](https://github.com/MetaCubeX/metacubexd/issues/246) ([ffa2447](https://github.com/MetaCubeX/metacubexd/commit/ffa244760f639309c64f0a41b721921a4b57e558)) |
| | | |
| | | ## [1.103.0](https://github.com/MetaCubeX/metacubexd/compare/v1.102.0...v1.103.0) (2023-09-21) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * **connection:** add sniffHost support, [#246](https://github.com/MetaCubeX/metacubexd/issues/246) ([2b16de1](https://github.com/MetaCubeX/metacubexd/commit/2b16de18b0bad7886baabe34ee7769c3e33bfff8)) |
| | | |
| | | ## [1.102.0](https://github.com/MetaCubeX/metacubexd/compare/v1.101.0...v1.102.0) (2023-09-21) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * **config:** three columns on small screen device, five on larger ([f4e82e0](https://github.com/MetaCubeX/metacubexd/commit/f4e82e0e270ff1cc6ec5ad0cec04cda807febcfa)) |
| | | * **proxy:** regression on render proxies in two columns ([8502a6e](https://github.com/MetaCubeX/metacubexd/commit/8502a6e4896e7a1c5e2049fcd2521a5434fcf70e)) |
| | | |
| | | ## [1.101.0](https://github.com/MetaCubeX/metacubexd/compare/v1.100.1...v1.101.0) (2023-09-21) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * **proxy:** fetch proxies after latency test ([2a0b21b](https://github.com/MetaCubeX/metacubexd/commit/2a0b21bcc7bd0b096e1bc8008fef3ccf1649496a)) |
| | | |
| | | ## [1.100.1](https://github.com/MetaCubeX/metacubexd/compare/v1.100.0...v1.100.1) (2023-09-19) |
| | | |
| | | |
| | | ### Performance Improvements |
| | | |
| | | * **overview:** shrink the data size every minute, closes [#234](https://github.com/MetaCubeX/metacubexd/issues/234) ([e0766fc](https://github.com/MetaCubeX/metacubexd/commit/e0766fc2c829b51728e16a587b68d13bd34c3bd9)) |
| | | |
| | | ## [1.100.0](https://github.com/MetaCubeX/metacubexd/compare/v1.99.0...v1.100.0) (2023-09-19) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * add a close button to the modal ([#239](https://github.com/MetaCubeX/metacubexd/issues/239)) ([737f1b0](https://github.com/MetaCubeX/metacubexd/commit/737f1b018975f3e13a566c0dd744e1d1f545421f)) |
| | | |
| | | ## [1.99.0](https://github.com/MetaCubeX/metacubexd/compare/v1.98.3...v1.99.0) (2023-09-18) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * show provider expire message with i18n ([#237](https://github.com/MetaCubeX/metacubexd/issues/237)) ([b9e9a95](https://github.com/MetaCubeX/metacubexd/commit/b9e9a955f26e1ebf9f45fd951fbbfe295f487c85)) |
| | | |
| | | ## [1.98.3](https://github.com/MetaCubeX/metacubexd/compare/v1.98.2...v1.98.3) (2023-09-17) |
| | | |
| | | |
| | | ### Bug Fixes |
| | | |
| | | * **proxy:** wrap subscription info usage percentage with toFinite ([ed947f6](https://github.com/MetaCubeX/metacubexd/commit/ed947f62a26c30a6f30aaf73066d7aea6f520bb0)) |
| | | |
| | | ## [1.98.2](https://github.com/MetaCubeX/metacubexd/compare/v1.98.1...v1.98.2) (2023-09-17) |
| | | |
| | | |
| | | ### Bug Fixes |
| | | |
| | | * subscriptionInfo may not be valid ([e7ba6c7](https://github.com/MetaCubeX/metacubexd/commit/e7ba6c7c34ceae395cd59337d965ee74b672e799)) |
| | | |
| | | ## [1.98.1](https://github.com/MetaCubeX/metacubexd/compare/v1.98.0...v1.98.1) (2023-09-17) |
| | | |
| | | |
| | | ### Bug Fixes |
| | | |
| | | * **proxy:** selectProxyInGroup ([dc0b286](https://github.com/MetaCubeX/metacubexd/commit/dc0b286a888085352be54de1dda50962330318cf)) |
| | | |
| | | ## [1.98.0](https://github.com/MetaCubeX/metacubexd/compare/v1.97.0...v1.98.0) (2023-09-17) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * **pwa:** add pwa shortcut icon, closes [#201](https://github.com/MetaCubeX/metacubexd/issues/201) ([3a23e99](https://github.com/MetaCubeX/metacubexd/commit/3a23e99c269373bbbaae95cdca65d7a0dbdddc10)) |
| | | |
| | | ## [1.97.0](https://github.com/MetaCubeX/metacubexd/compare/v1.96.0...v1.97.0) (2023-09-17) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * **logs:** fuzzy filter, sorting ([67345cf](https://github.com/MetaCubeX/metacubexd/commit/67345cf1e34eb309fa0a1ba5eb1bbfe441fc61fd)) |
| | | * **proxy:** show more proxy nodes on larger screen ([5277c7b](https://github.com/MetaCubeX/metacubexd/commit/5277c7bbc99f7407ef195b8c2508332efbc59a07)) |
| | | |
| | | |
| | | ### Bug Fixes |
| | | |
| | | * **logs:** inconsistent gap between search input and table ([477fb22](https://github.com/MetaCubeX/metacubexd/commit/477fb2233a56e5d3a015a27292580eac0267b7d8)) |
| | | * **logs:** inconsistent padding ([3076487](https://github.com/MetaCubeX/metacubexd/commit/30764873a834f04dd7855a69b03cfb9c68f549fd)) |
| | | |
| | | ## [1.96.0](https://github.com/MetaCubeX/metacubexd/compare/v1.95.0...v1.96.0) (2023-09-16) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * **connection:** update tag badges layout ([b7a0407](https://github.com/MetaCubeX/metacubexd/commit/b7a0407e42b4433766bda53c4dd7866dfe7c1cb3)) |
| | | |
| | | |
| | | ### Performance Improvements |
| | | |
| | | * batch signals ([0015468](https://github.com/MetaCubeX/metacubexd/commit/0015468077c146e1d7526863873b3af16f01fccc)) |
| | | * batch signals ([e04ec71](https://github.com/MetaCubeX/metacubexd/commit/e04ec712ea5582d32dc723cc1ebcef4d192ab185)) |
| | | |
| | | ## [1.95.0](https://github.com/MetaCubeX/metacubexd/compare/v1.94.0...v1.95.0) (2023-09-16) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * **config:** update page layout ([95200a5](https://github.com/MetaCubeX/metacubexd/commit/95200a56b8902e443f5e8d2c8fa38ff6c6e28c2a)) |
| | | * **config:** update page layout ([2be357e](https://github.com/MetaCubeX/metacubexd/commit/2be357e73c51c52709fe8b911b9e595ed281a7fa)) |
| | | * **config:** update page layout, limit the max width of the container ([d059ea8](https://github.com/MetaCubeX/metacubexd/commit/d059ea85b3efd8c25e1ebed158b556df7eb2b5ae)) |
| | | |
| | | ## [1.94.0](https://github.com/MetaCubeX/metacubexd/compare/v1.93.2...v1.94.0) (2023-09-16) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * **connections:** tag client source ip with name, closes [#181](https://github.com/MetaCubeX/metacubexd/issues/181) ([e538f9d](https://github.com/MetaCubeX/metacubexd/commit/e538f9d057570871cac0ac94aeae9b40c99d0217)) |
| | | |
| | | ## [1.93.2](https://github.com/MetaCubeX/metacubexd/compare/v1.93.1...v1.93.2) (2023-09-16) |
| | | |
| | | |
| | | ### Bug Fixes |
| | | |
| | | * **log:** log level value not selected ([3fbed0d](https://github.com/MetaCubeX/metacubexd/commit/3fbed0d69482da005bfa109eed9acae203a08ce2)) |
| | | |
| | | ## [1.93.1](https://github.com/MetaCubeX/metacubexd/compare/v1.93.0...v1.93.1) (2023-09-16) |
| | | |
| | | |
| | | ### Bug Fixes |
| | | |
| | | * **proxy:** dont hideUnAvailableProxies in proxy providers ([be6edd9](https://github.com/MetaCubeX/metacubexd/commit/be6edd914a32b05125337fd113d571f6066b0265)) |
| | | |
| | | ## [1.93.0](https://github.com/MetaCubeX/metacubexd/compare/v1.92.0...v1.93.0) (2023-09-15) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * **config:** reload config file, flush fake-ip data ([6ffd1a8](https://github.com/MetaCubeX/metacubexd/commit/6ffd1a860538f82ef92e2fb97ed19d67e4452ad7)) |
| | | |
| | | ## [1.92.0](https://github.com/MetaCubeX/metacubexd/compare/v1.91.0...v1.92.0) (2023-09-15) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * drop ForTwoColumns renderer ([353b5eb](https://github.com/MetaCubeX/metacubexd/commit/353b5ebff3457c4a804836ee1428efeb7f8ad633)) |
| | | |
| | | |
| | | ### Bug Fixes |
| | | |
| | | * proxy providers grid layout ([6d242a4](https://github.com/MetaCubeX/metacubexd/commit/6d242a44579522c65e086a5c5c89824329643c0b)) |
| | | * **proxy:** tabs won't be shown in proxies page when proxy provider list is empty ([6c5662a](https://github.com/MetaCubeX/metacubexd/commit/6c5662a7a9a99ccd6b6d570a8da983fa37effc0b)) |
| | | |
| | | ## [1.91.0](https://github.com/MetaCubeX/metacubexd/compare/v1.90.0...v1.91.0) (2023-09-15) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * **proxy:** hideUnAvailableProxies, closes [#200](https://github.com/MetaCubeX/metacubexd/issues/200) ([9e556d9](https://github.com/MetaCubeX/metacubexd/commit/9e556d959eb6ad801be30570118c942d9c31dc03)) |
| | | * **rules:** renderInTwoColumns ([6743ebd](https://github.com/MetaCubeX/metacubexd/commit/6743ebde8f5e13f32b27f913c620629517f63da2)) |
| | | * settings per-page ([d776260](https://github.com/MetaCubeX/metacubexd/commit/d77626058af9a3d22f22e410918d2aafb3f2ea46)) |
| | | |
| | | |
| | | ### Bug Fixes |
| | | |
| | | * failed to resolve import solid-infinite-scroll ([ebc95fe](https://github.com/MetaCubeX/metacubexd/commit/ebc95fe58592a6f187f733ea1da3b56d738a7406)) |
| | | * logMaxRows value ([b3c924e](https://github.com/MetaCubeX/metacubexd/commit/b3c924e33bdca68fed59092c107cc38324a418d0)) |
| | | |
| | | ## [1.90.0](https://github.com/MetaCubeX/metacubexd/compare/v1.89.0...v1.90.0) (2023-09-13) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * **connections:** shorten the tab names ([1a37a10](https://github.com/MetaCubeX/metacubexd/commit/1a37a106f8c421829854ffcb75b64b7885b00005)) |
| | | |
| | | ## [1.89.0](https://github.com/MetaCubeX/metacubexd/compare/v1.88.1...v1.89.0) (2023-09-12) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * **config:** select tunnel mode ([34ea1f1](https://github.com/MetaCubeX/metacubexd/commit/34ea1f1c5657d466c360b1b1771571b06f67e423)) |
| | | |
| | | |
| | | ### Bug Fixes |
| | | |
| | | * **overview:** error when reading length ([#188](https://github.com/MetaCubeX/metacubexd/issues/188)) ([9f227c9](https://github.com/MetaCubeX/metacubexd/commit/9f227c9c2c38718bf540bb5c9c5959a647d00af2)) |
| | | |
| | | ## [1.88.1](https://github.com/MetaCubeX/metacubexd/compare/v1.88.0...v1.88.1) (2023-09-10) |
| | | |
| | | |
| | | ### Bug Fixes |
| | | |
| | | * **Dockerfile:** lock node alpine version ([6f5c1da](https://github.com/MetaCubeX/metacubexd/commit/6f5c1dade011dc0359366b832bbb0879e079e99f)) |
| | | |
| | | ## [1.88.0](https://github.com/MetaCubeX/metacubexd/compare/v1.87.0...v1.88.0) (2023-09-10) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * **connections:** close filtered connections ([4f8c4b2](https://github.com/MetaCubeX/metacubexd/commit/4f8c4b2d15fdee6a546f0290b9c42c860aa334fb)) |
| | | * **connections:** fuzzy filter ([9a144e8](https://github.com/MetaCubeX/metacubexd/commit/9a144e882dc7b33fd6a6e055b122533941a0361f)) |
| | | * **connections:** keyed connection rows and cells ([e7eb3dd](https://github.com/MetaCubeX/metacubexd/commit/e7eb3dd331e0059ccf366f1411a1be208fec2a2a)) |
| | | * **connections:** set CONNECTIONS_TABLE_MAX_CLOSED_ROWS to 200 ([b34ceb3](https://github.com/MetaCubeX/metacubexd/commit/b34ceb334d1b0df41d720ea471c9af7b694ae202)) |
| | | * **connections:** use Index instead of key ([07f461f](https://github.com/MetaCubeX/metacubexd/commit/07f461f1105f3bbb1322ea4d246227a313ce02a4)) |
| | | * **logs:** use Index instead of For ([669492f](https://github.com/MetaCubeX/metacubexd/commit/669492f22c2d72ab9c6c18d1630bd0179f0f933b)) |
| | | |
| | | ## [1.87.0](https://github.com/MetaCubeX/metacubexd/compare/v1.86.1...v1.87.0) (2023-09-10) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * minor improvements ([f4788bb](https://github.com/MetaCubeX/metacubexd/commit/f4788bb4634d73bac8ad0030849b0f60f971a610)) |
| | | |
| | | ## [1.86.1](https://github.com/MetaCubeX/metacubexd/compare/v1.86.0...v1.86.1) (2023-09-10) |
| | | |
| | | |
| | | ### Bug Fixes |
| | | |
| | | * **proxies:** format proxy type ([1c61df8](https://github.com/MetaCubeX/metacubexd/commit/1c61df8b8b39fd587393bdaf8ee52c2da12e89ad)) |
| | | |
| | | ## [1.86.0](https://github.com/MetaCubeX/metacubexd/compare/v1.85.0...v1.86.0) (2023-09-09) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * **logs:** fixes [#194](https://github.com/MetaCubeX/metacubexd/issues/194) ([b000f91](https://github.com/MetaCubeX/metacubexd/commit/b000f919fc1db24ba03ab3eb4a327e8687564330)) |
| | | * **setup:** prefix endpoint url with current url protocol if missing ([e935aed](https://github.com/MetaCubeX/metacubexd/commit/e935aedfcf3629d038c084dc3491fb07bac1e54b)) |
| | | |
| | | ## [1.85.0](https://github.com/MetaCubeX/metacubexd/compare/v1.84.0...v1.85.0) (2023-09-09) |
| | | |
| | | |
| | | ### Features |
| | | |
| | | * **rules:** tab style for rules page [#163](https://github.com/MetaCubeX/metacubexd/issues/163) ([#187](https://github.com/MetaCubeX/metacubexd/issues/187)) ([a9aa8e5](https://github.com/MetaCubeX/metacubexd/commit/a9aa8e540e975d27ed7a843f1e1b9ba3abcc02bd)) |
| | | |
| | | ## [1.84.0](https://github.com/MetaCubeX/metacubexd/compare/v1.83.2...v1.84.0) (2023-09-08) |
| | | |
| | | ### Features |
| | | |
| | | - **connections:** make connections table sorting state persisted ([6957cb4](https://github.com/MetaCubeX/metacubexd/commit/6957cb4ddf9616ab6eade404098d4b84c9ed2a97)) |
| | | - **connections:** show connection detail on click of a button ([daf1682](https://github.com/MetaCubeX/metacubexd/commit/daf168208e6a1e7bb4ab921ee5b235c926f6a13a)) |
| | | - **logs:** add i18n translation support ([965ec50](https://github.com/MetaCubeX/metacubexd/commit/965ec502aceeb206c0067e36a61f755a31002bc4)) |
| | | |
| | | ## [1.83.2](https://github.com/MetaCubeX/metacubexd/compare/v1.83.1...v1.83.2) (2023-09-08) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - **proxies:** disconnect after proxy is changed ([a8cb75f](https://github.com/MetaCubeX/metacubexd/commit/a8cb75f29da3c4616237ec4918c92df91854f83b)) |
| | | |
| | | ## [1.83.1](https://github.com/MetaCubeX/metacubexd/compare/v1.83.0...v1.83.1) (2023-09-08) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - **connections:** close conns ([f3423ab](https://github.com/MetaCubeX/metacubexd/commit/f3423abe921a470fc3ef1b9e6076643efcc799e0)) |
| | | |
| | | ## [1.83.0](https://github.com/MetaCubeX/metacubexd/compare/v1.82.0...v1.83.0) (2023-09-08) |
| | | |
| | | ### Features |
| | | |
| | | - **proxies:** auto close connections when proxy selected ([15d20cd](https://github.com/MetaCubeX/metacubexd/commit/15d20cde4ad514d45a54845f18fb9b401a681208)) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - **button:** btn disable ([59a7273](https://github.com/MetaCubeX/metacubexd/commit/59a727366536588a8451ea40d305711dbfc6b36e)) |
| | | - **connections:** limit number of conns ([5263710](https://github.com/MetaCubeX/metacubexd/commit/5263710d30e08efdfcd976236f2eab76fa085264)) |
| | | - **connections:** table close conn ([d0c7c15](https://github.com/MetaCubeX/metacubexd/commit/d0c7c1569d8138724218d67c08195747be9492a0)) |
| | | - **connections:** ws error when switch end point ([b04b20f](https://github.com/MetaCubeX/metacubexd/commit/b04b20f8f01d4cfdd9f41591e03d675e1512b911)) |
| | | |
| | | ## [1.82.0](https://github.com/MetaCubeX/metacubexd/compare/v1.81.1...v1.82.0) (2023-09-07) |
| | | |
| | | ### Features |
| | | |
| | | - add a gradient color to logo text ([4da1720](https://github.com/MetaCubeX/metacubexd/commit/4da1720e550355efdef147cae3c3cfbf31e319b7)) |
| | | - **proxy:** add more adapter types to be processed by formatProxyType ([8245268](https://github.com/MetaCubeX/metacubexd/commit/82452688c93ca9df8be225f0e46cf9760e297fcf)) |
| | | |
| | | ## [1.81.1](https://github.com/MetaCubeX/metacubexd/compare/v1.81.0...v1.81.1) (2023-09-07) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - **proxy:** fixes [#174](https://github.com/MetaCubeX/metacubexd/issues/174) ([c0b0489](https://github.com/MetaCubeX/metacubexd/commit/c0b04892c8a1eb93a8d8ac25c3c2d35da45c81e6)) |
| | | - **setup:** fixes [#175](https://github.com/MetaCubeX/metacubexd/issues/175) ([8242706](https://github.com/MetaCubeX/metacubexd/commit/8242706290668f91578562d70e616a82ed5ce00c)) |
| | | |
| | | ## [1.81.0](https://github.com/MetaCubeX/metacubexd/compare/v1.80.0...v1.81.0) (2023-09-07) |
| | | |
| | | ### Features |
| | | |
| | | - **connections:** update connections page top area ([64659b2](https://github.com/MetaCubeX/metacubexd/commit/64659b26f110a4c0271c584a892cc7943bb2cf41)) |
| | | - new logo color ([e31c1eb](https://github.com/MetaCubeX/metacubexd/commit/e31c1eb9e031bed4254596e593b71374dbefcd5d)) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - **conns:** input width ([62ff9c3](https://github.com/MetaCubeX/metacubexd/commit/62ff9c3f09131072585e5ebb5ccf515a0bc75a60)) |
| | | |
| | | ## [1.80.0](https://github.com/MetaCubeX/metacubexd/compare/v1.79.0...v1.80.0) (2023-09-07) |
| | | |
| | | ### Features |
| | | |
| | | - hidden scrollbar for mobile ([2ce3016](https://github.com/MetaCubeX/metacubexd/commit/2ce30162752be27337c8e6dce7a81ee7bea6d6dd)) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - **connections:** padding fix ([8cea622](https://github.com/MetaCubeX/metacubexd/commit/8cea622251b93308a94c6ade29199a6f01dbf72d)) |
| | | - tab style for conns and proxies ([842ce9e](https://github.com/MetaCubeX/metacubexd/commit/842ce9e00fe5a3f9e117cafb6c181602ef618c94)) |
| | | |
| | | ## [1.79.0](https://github.com/MetaCubeX/metacubexd/compare/v1.78.1...v1.79.0) (2023-09-07) |
| | | |
| | | ### Features |
| | | |
| | | - **proxies:** default proxies and remove all tab ([66c7935](https://github.com/MetaCubeX/metacubexd/commit/66c793550ab4ef3c85d24422fe58dec644deb6d4)) |
| | | - update LogoText ([8d62c9f](https://github.com/MetaCubeX/metacubexd/commit/8d62c9f510de7268a3c0ead5e47a31fe938c196d)) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - btn size for mobile ([f729761](https://github.com/MetaCubeX/metacubexd/commit/f72976154df067d8c3fab3eda220dd386329b9be)) |
| | | - **rules:** request token error ([44739f6](https://github.com/MetaCubeX/metacubexd/commit/44739f6962792a5ae42684ee8e1edcb8a9a4216b)) |
| | | |
| | | ## [1.78.1](https://github.com/MetaCubeX/metacubexd/compare/v1.78.0...v1.78.1) (2023-09-06) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - unselectable ProxyNodeCard also has cursor-pointer applied ([d5112f2](https://github.com/MetaCubeX/metacubexd/commit/d5112f23da449e4cb74c9abc49d84f34a0859758)) |
| | | |
| | | ## [1.78.0](https://github.com/MetaCubeX/metacubexd/compare/v1.77.0...v1.78.0) (2023-09-06) |
| | | |
| | | ### Features |
| | | |
| | | - customize scrollbar ([c27b192](https://github.com/MetaCubeX/metacubexd/commit/c27b19259bff444b21fd84502fa638fe8ac13c7b)) |
| | | |
| | | ## [1.77.0](https://github.com/MetaCubeX/metacubexd/compare/v1.76.1...v1.77.0) (2023-09-06) |
| | | |
| | | ### Features |
| | | |
| | | - **proxy:** keep tab at the top ([1bb36a1](https://github.com/MetaCubeX/metacubexd/commit/1bb36a1bd618e41863baac694c2ff115d364dbd8)) |
| | | - update the overall look of the app ([08ed610](https://github.com/MetaCubeX/metacubexd/commit/08ed61045f199b0f2721e4a82c208f68ee70d476)) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - btn disable && var name ([6c88062](https://github.com/MetaCubeX/metacubexd/commit/6c880628ec6b3a32452b3d64c858e27f2e1c7b65)) |
| | | |
| | | ## [1.76.1](https://github.com/MetaCubeX/metacubexd/compare/v1.76.0...v1.76.1) (2023-09-06) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - **Connections:** disable grouping for time&speed ([#160](https://github.com/MetaCubeX/metacubexd/issues/160)) ([8398dff](https://github.com/MetaCubeX/metacubexd/commit/8398dff02d1bb254d806901f0a9ce49105be6d52)) |
| | | |
| | | ## [1.76.0](https://github.com/MetaCubeX/metacubexd/compare/v1.75.1...v1.76.0) (2023-09-06) |
| | | |
| | | ### Features |
| | | |
| | | - **proxy:** merge proxies and proxyProviders into one page ([93114b2](https://github.com/MetaCubeX/metacubexd/commit/93114b22a28b3e336b9fccbee4ec99b6eb8a4b61)) |
| | | - **rules:** config for render rules in two columns ([7e2fe6a](https://github.com/MetaCubeX/metacubexd/commit/7e2fe6acb0d2bc879cd724cfda7d80924abd5d40)) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - **connections:** table max row 200 ([0d61b46](https://github.com/MetaCubeX/metacubexd/commit/0d61b460cc16aa1a7c60015afe9b526c38d57ffc)) |
| | | - **proxy:** add small gap between update button and tabs ([ff7dab7](https://github.com/MetaCubeX/metacubexd/commit/ff7dab74b69b0527c721185fcdf94c005d7c5d3c)) |
| | | - **request:** ws request url ([a13060a](https://github.com/MetaCubeX/metacubexd/commit/a13060a056c9ac87c21ce7f8706ee6a5ae23be18)) |
| | | - **translation:** latencyTestTimeoutDuration ([2ebedcc](https://github.com/MetaCubeX/metacubexd/commit/2ebedcc1c97b675395cd0f55824bc11ee5af5fce)) |
| | | |
| | | ## [1.75.1](https://github.com/MetaCubeX/metacubexd/compare/v1.75.0...v1.75.1) (2023-09-06) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - **overview:** chart font size ([af70669](https://github.com/MetaCubeX/metacubexd/commit/af70669e7a035a6368e7620be7ed489db76648cd)) |
| | | |
| | | ## [1.75.0](https://github.com/MetaCubeX/metacubexd/compare/v1.74.4...v1.75.0) (2023-09-06) |
| | | |
| | | ### Features |
| | | |
| | | - **connections:** split source info into sourceIP and sourcePort ([2d61f45](https://github.com/MetaCubeX/metacubexd/commit/2d61f45372ac65a01aa9f51fe010cfdee53d9d46)) |
| | | |
| | | ## [1.74.4](https://github.com/MetaCubeX/metacubexd/compare/v1.74.3...v1.74.4) (2023-09-06) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - **config:** speedtest timeout config ([5f1e773](https://github.com/MetaCubeX/metacubexd/commit/5f1e77320d82fbc5e2abfc1ebee70e4194c0ce98)) |
| | | |
| | | ## [1.74.3](https://github.com/MetaCubeX/metacubexd/compare/v1.74.2...v1.74.3) (2023-09-06) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - **Dockerfile:** lock node version to 20.5-alpine ([c370a84](https://github.com/MetaCubeX/metacubexd/commit/c370a84b8cc6546729c1c0db88ff8f6bba2244a6)) |
| | | - **proxies:** speedtest params error ([5f0fb23](https://github.com/MetaCubeX/metacubexd/commit/5f0fb23f15f7118a76e58c3a6b6e4d97533dcfdd)) |
| | | |
| | | ## [1.74.2](https://github.com/MetaCubeX/metacubexd/compare/v1.74.1...v1.74.2) (2023-09-06) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - tableSizeClassName not been generated by tailwindcss ([5f4f9a8](https://github.com/MetaCubeX/metacubexd/commit/5f4f9a8ae8d49c4ce9816860c04ffa0090f2ead0)) |
| | | |
| | | ## [1.74.1](https://github.com/MetaCubeX/metacubexd/compare/v1.74.0...v1.74.1) (2023-09-05) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - **logs:** table size ([3a2560f](https://github.com/MetaCubeX/metacubexd/commit/3a2560f15291a60ed509cf8c32f5c699cda957e5)) |
| | | |
| | | ## [1.74.0](https://github.com/MetaCubeX/metacubexd/compare/v1.73.0...v1.74.0) (2023-09-05) |
| | | |
| | | ### Features |
| | | |
| | | - **connections:** shrink the element sizes down ([0aa9779](https://github.com/MetaCubeX/metacubexd/commit/0aa97795246978003319684b1ea8a500f7b382ca)) |
| | | |
| | | ## [1.73.0](https://github.com/MetaCubeX/metacubexd/compare/v1.72.1...v1.73.0) (2023-09-05) |
| | | |
| | | ### Features |
| | | |
| | | - **connections:** pausing support ([6076619](https://github.com/MetaCubeX/metacubexd/commit/60766194858e0c88f5af2a5f4c5f21946a0429ff)) |
| | | |
| | | ## [1.72.1](https://github.com/MetaCubeX/metacubexd/compare/v1.72.0...v1.72.1) (2023-09-05) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - **connections:** limit closed connection items count below 100 ([f97f467](https://github.com/MetaCubeX/metacubexd/commit/f97f46720a5ded52aa6b592f11b1dca2579bb0b5)) |
| | | |
| | | ## [1.72.0](https://github.com/MetaCubeX/metacubexd/compare/v1.71.0...v1.72.0) (2023-09-05) |
| | | |
| | | ### Features |
| | | |
| | | - **connections:** add closed connections tab ([f0e32d9](https://github.com/MetaCubeX/metacubexd/commit/f0e32d93f6d5ffa52a721ede1003d2f7ee20d0bc)) |
| | | |
| | | ## [1.71.0](https://github.com/MetaCubeX/metacubexd/compare/v1.70.0...v1.71.0) (2023-09-05) |
| | | |
| | | ### Features |
| | | |
| | | - **connections:** connections table now supports grouping ([4c8b38f](https://github.com/MetaCubeX/metacubexd/commit/4c8b38f7563b6c8c38c8295ff0a30ec995c88257)) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - **proxy:** updateAllProvider, some of the proxy provider update may throw ([e09aac2](https://github.com/MetaCubeX/metacubexd/commit/e09aac2f4bd55bee816fb3d2a615dd4b14169607)) |
| | | |
| | | ## [1.70.0](https://github.com/MetaCubeX/metacubexd/compare/v1.69.0...v1.70.0) (2023-09-05) |
| | | |
| | | ### Features |
| | | |
| | | - **config:** add setting, requestTimeoutDuration ([d01d148](https://github.com/MetaCubeX/metacubexd/commit/d01d148cfffc785a9c57b021696db9e2b191eb1f)) |
| | | |
| | | ## [1.69.0](https://github.com/MetaCubeX/metacubexd/compare/v1.68.0...v1.69.0) (2023-09-05) |
| | | |
| | | ### Features |
| | | |
| | | - icon success color when updating ([6575e73](https://github.com/MetaCubeX/metacubexd/commit/6575e734392d63d959dacbb3b7ad733285828475)) |
| | | - make some changes in response to community suggestions ([cc78d4e](https://github.com/MetaCubeX/metacubexd/commit/cc78d4e2b1adf729d38a63f6b147544b100f94a3)) |
| | | - make some changes in response to community suggestions ([90ecb86](https://github.com/MetaCubeX/metacubexd/commit/90ecb866039b36b4fdd1bc420bd65ef10222cd23)) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - icon vertical align ([c6f3bee](https://github.com/MetaCubeX/metacubexd/commit/c6f3beec354f632584d77c4a5295203076410e21)) |
| | | - rule provider word break ([e6facf1](https://github.com/MetaCubeX/metacubexd/commit/e6facf11758500ebddb4b534f5e843fd8f016476)) |
| | | |
| | | ## [1.68.0](https://github.com/MetaCubeX/metacubexd/compare/v1.67.5...v1.68.0) (2023-09-05) |
| | | |
| | | ### Features |
| | | |
| | | - a stable table ([e19e53d](https://github.com/MetaCubeX/metacubexd/commit/e19e53d0f68bfd30d4aff3a630796678d7a7e97d)) |
| | | - table style ([0115daf](https://github.com/MetaCubeX/metacubexd/commit/0115daf014de9737de0a30ab07bde2c4b0337492)) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - speedtest icon align ([2caf0c4](https://github.com/MetaCubeX/metacubexd/commit/2caf0c41dd8f12855b79f894e0a6b8dddcf030f6)) |
| | | |
| | | ## [1.67.5](https://github.com/MetaCubeX/metacubexd/compare/v1.67.4...v1.67.5) (2023-09-05) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - build issue, `Cannot redefine property: File` ([b50c41e](https://github.com/MetaCubeX/metacubexd/commit/b50c41e3a588b843f7b5f35afd38b981fd0372b9)) |
| | | |
| | | ## [1.67.4](https://github.com/MetaCubeX/metacubexd/compare/v1.67.3...v1.67.4) (2023-09-05) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - update pnpm-lock.yaml ([0756f13](https://github.com/MetaCubeX/metacubexd/commit/0756f133a0bf8f750b4caa2f8d895038d3816611)) |
| | | |
| | | ## [1.67.3](https://github.com/MetaCubeX/metacubexd/compare/v1.67.2...v1.67.3) (2023-09-05) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - better toggle twemoji implementation ([13abde7](https://github.com/MetaCubeX/metacubexd/commit/13abde7a9d62874dcab75de0cbba58b7285257a7)) |
| | | |
| | | ## [1.67.2](https://github.com/MetaCubeX/metacubexd/compare/v1.67.1...v1.67.2) (2023-09-04) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - rollback to h-screen :) ([b68e0c6](https://github.com/MetaCubeX/metacubexd/commit/b68e0c62c09a727c9b55b59e26f81cc55fb5222b)) |
| | | |
| | | ## [1.67.1](https://github.com/MetaCubeX/metacubexd/compare/v1.67.0...v1.67.1) (2023-09-04) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - use min-h-screen-safe instead of h-screen ([6f366fe](https://github.com/MetaCubeX/metacubexd/commit/6f366fe836d1759e6847de19ec64c0f182b242cb)) |
| | | |
| | | ## [1.67.0](https://github.com/MetaCubeX/metacubexd/compare/v1.66.0...v1.67.0) (2023-09-04) |
| | | |
| | | ### Features |
| | | |
| | | - add padding-bottom safe-area ([a55ec6f](https://github.com/MetaCubeX/metacubexd/commit/a55ec6f7b18fa8638d673c9c7c493752ae3dbbb1)) |
| | | |
| | | ## [1.66.0](https://github.com/MetaCubeX/metacubexd/compare/v1.65.0...v1.66.0) (2023-09-04) |
| | | |
| | | ### Features |
| | | |
| | | - add safe area paddings ([9840141](https://github.com/MetaCubeX/metacubexd/commit/9840141bce39188094db12373d8c358da771ffd1)) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - **proxies:** group type does not show UDP ([f0fbe09](https://github.com/MetaCubeX/metacubexd/commit/f0fbe098381c2b33c84b2eb5427f9db4b227e364)) |
| | | |
| | | ## [1.65.0](https://github.com/MetaCubeX/metacubexd/compare/v1.64.0...v1.65.0) (2023-09-04) |
| | | |
| | | ### Features |
| | | |
| | | - **connections:** optimized search ([17d9ae4](https://github.com/MetaCubeX/metacubexd/commit/17d9ae487226401d73ddf363bd76cebe9977f1b5)) |
| | | |
| | | ## [1.64.0](https://github.com/MetaCubeX/metacubexd/compare/v1.63.0...v1.64.0) (2023-09-04) |
| | | |
| | | ### Features |
| | | |
| | | - **logs:** sticky table thead ([5affd5b](https://github.com/MetaCubeX/metacubexd/commit/5affd5b91e253b433fc36e4e6d21f3a631790d3a)) |
| | | |
| | | ## [1.63.0](https://github.com/MetaCubeX/metacubexd/compare/v1.62.0...v1.63.0) (2023-09-04) |
| | | |
| | | ### Features |
| | | |
| | | - **connections:** sticky table thead ([7f8f8a9](https://github.com/MetaCubeX/metacubexd/commit/7f8f8a98816464f63cc19fcc6147a490bb58050d)) |
| | | - **overview:** reduce padding of the chart container ([3728cd2](https://github.com/MetaCubeX/metacubexd/commit/3728cd2e3289531fe61fd2da11e06aaa89a1a471)) |
| | | |
| | | ## [1.62.0](https://github.com/MetaCubeX/metacubexd/compare/v1.61.0...v1.62.0) (2023-09-04) |
| | | |
| | | ### Features |
| | | |
| | | - **overview:** update the overall look of the overview page ([c757850](https://github.com/MetaCubeX/metacubexd/commit/c757850c1a34b2757ab5ec89e850fe7c1976d533)) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - **proxies:** also display latency when PROXIES_PREVIEW_TYPE is DOTS ([347ebb6](https://github.com/MetaCubeX/metacubexd/commit/347ebb6f84e1f316bd4551ca0f079ad1050a4f01)) |
| | | - rule size ([2ff31d4](https://github.com/MetaCubeX/metacubexd/commit/2ff31d40b1349c2213b999c57d9d7fe0ce9e0af4)) |
| | | |
| | | ## [1.61.0](https://github.com/MetaCubeX/metacubexd/compare/v1.60.3...v1.61.0) (2023-09-04) |
| | | |
| | | ### Features |
| | | |
| | | - config for render proxies in same page ([5bc540e](https://github.com/MetaCubeX/metacubexd/commit/5bc540eeffeefd289197283f7ebe491397f18376)) |
| | | - time column in conns ([98f6293](https://github.com/MetaCubeX/metacubexd/commit/98f6293290ecedf2f3fde4baa24f60cedfaa0445)) |
| | | |
| | | ## [1.60.3](https://github.com/MetaCubeX/metacubexd/compare/v1.60.2...v1.60.3) (2023-09-04) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - auto login ([d7f3649](https://github.com/MetaCubeX/metacubexd/commit/d7f3649eba6e2a6475e5b96b9fff51e414ca47f1)) |
| | | - **connections:** close column header translation result not updating ([5689034](https://github.com/MetaCubeX/metacubexd/commit/5689034f942145e35811bb8c2f063ba5632490da)) |
| | | |
| | | ## [1.60.2](https://github.com/MetaCubeX/metacubexd/compare/v1.60.1...v1.60.2) (2023-09-04) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - setup onsubmit secret ([30fd139](https://github.com/MetaCubeX/metacubexd/commit/30fd13946b0a42528330d06994803d811838979e)) |
| | | |
| | | ## [1.60.1](https://github.com/MetaCubeX/metacubexd/compare/v1.60.0...v1.60.1) (2023-09-03) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - **overview:** only enable flex-row layout on large screen ([be8b54a](https://github.com/MetaCubeX/metacubexd/commit/be8b54a0f620acab8afdf9039555d89f7bf74083)) |
| | | |
| | | ## [1.60.0](https://github.com/MetaCubeX/metacubexd/compare/v1.59.1...v1.60.0) (2023-09-03) |
| | | |
| | | ### Features |
| | | |
| | | - **connections:** move close connection button next to search input ([4b8175d](https://github.com/MetaCubeX/metacubexd/commit/4b8175d718211d139b4db17f9412b88608b089fe)) |
| | | |
| | | ## [1.59.1](https://github.com/MetaCubeX/metacubexd/compare/v1.59.0...v1.59.1) (2023-09-03) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - **connections:** disable sorting for close column ([98b5d62](https://github.com/MetaCubeX/metacubexd/commit/98b5d62c0208efbd1cf501e031240d41ab434fc0)) |
| | | |
| | | ## [1.59.0](https://github.com/MetaCubeX/metacubexd/compare/v1.58.0...v1.59.0) (2023-09-03) |
| | | |
| | | ### Features |
| | | |
| | | - display current version ([1765ada](https://github.com/MetaCubeX/metacubexd/commit/1765adacdad295101e3d0479841affcca8a9ec7e)) |
| | | - **overview:** add background color to charts container ([bd83d11](https://github.com/MetaCubeX/metacubexd/commit/bd83d113b169662e369355e655cf179a7704d311)) |
| | | |
| | | ## [1.58.0](https://github.com/MetaCubeX/metacubexd/compare/v1.57.3...v1.58.0) (2023-09-03) |
| | | |
| | | ### Features |
| | | |
| | | - **connections:** copy to clipboard on right click ([58afb5f](https://github.com/MetaCubeX/metacubexd/commit/58afb5fce29173b8041b8a93788bba361f3c43cd)) |
| | | |
| | | ## [1.57.3](https://github.com/MetaCubeX/metacubexd/compare/v1.57.2...v1.57.3) (2023-09-03) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - **font:** adapt to apple fonts ([84c70b0](https://github.com/MetaCubeX/metacubexd/commit/84c70b0550e6e1da5adf2f69ea530c45ff14561a)) |
| | | |
| | | ## [1.57.2](https://github.com/MetaCubeX/metacubexd/compare/v1.57.1...v1.57.2) (2023-09-03) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - bar latency ([197b0c7](https://github.com/MetaCubeX/metacubexd/commit/197b0c7d4b34dafb293ce398c9a59b043b4330eb)) |
| | | |
| | | ## [1.57.1](https://github.com/MetaCubeX/metacubexd/compare/v1.57.0...v1.57.1) (2023-09-02) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - **latency:** invalid 0 latency scenario ([dd5f0f1](https://github.com/MetaCubeX/metacubexd/commit/dd5f0f14a070bb9a042bfab1a544a161d8187b58)) |
| | | |
| | | ## [1.57.0](https://github.com/MetaCubeX/metacubexd/compare/v1.56.0...v1.57.0) (2023-09-02) |
| | | |
| | | ### Features |
| | | |
| | | - **config:** make config option titles more noticeable ([b9eb92f](https://github.com/MetaCubeX/metacubexd/commit/b9eb92fc0bd6ef5ce0aa35fd1c7ee46dd028621a)) |
| | | |
| | | ## [1.56.0](https://github.com/MetaCubeX/metacubexd/compare/v1.55.0...v1.56.0) (2023-09-02) |
| | | |
| | | ### Features |
| | | |
| | | - **proxies:** sortProxiesByOrderingType ([635b8ba](https://github.com/MetaCubeX/metacubexd/commit/635b8ba966da12dfe3879e97472014150deec0df)) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - **proxies:** latency widget ([b511b88](https://github.com/MetaCubeX/metacubexd/commit/b511b88cd1d426ef3252ba0a0d9f28c4fb6ad826)) |
| | | |
| | | ## [1.55.0](https://github.com/MetaCubeX/metacubexd/compare/v1.54.0...v1.55.0) (2023-09-02) |
| | | |
| | | ### Features |
| | | |
| | | - add PROXIES_PREVIEW_TYPE - off ([fec6413](https://github.com/MetaCubeX/metacubexd/commit/fec64133fa8b5a3f14905d7910097d790314471f)) |
| | | - relax https latency test range ([1b09fa3](https://github.com/MetaCubeX/metacubexd/commit/1b09fa39af2c5bfc9fd8374c9b2ec53e59595184)) |
| | | |
| | | ## [1.54.0](https://github.com/MetaCubeX/metacubexd/compare/v1.53.0...v1.54.0) (2023-09-02) |
| | | |
| | | ### Features |
| | | |
| | | - add buttons - updateGEODatabases, restart, upgrade ([8e56543](https://github.com/MetaCubeX/metacubexd/commit/8e5654332ddfce03399e24864b856fed882480c3)) |
| | | |
| | | ## [1.53.0](https://github.com/MetaCubeX/metacubexd/compare/v1.52.1...v1.53.0) (2023-09-02) |
| | | |
| | | ### Features |
| | | |
| | | - config for render in two col ([5d43ea0](https://github.com/MetaCubeX/metacubexd/commit/5d43ea0b5f48b2d84984bdf94dcef87d8e780432)) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - btn animate ([a29e638](https://github.com/MetaCubeX/metacubexd/commit/a29e638c1e02fc8fb25ac1806d9e9e9d9af288f9)) |
| | | |
| | | ## [1.52.1](https://github.com/MetaCubeX/metacubexd/compare/v1.52.0...v1.52.1) (2023-09-02) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - update proxies ([467d0bc](https://github.com/MetaCubeX/metacubexd/commit/467d0bcce2bd7e0764c847ae3a8231a6ddcf034b)) |
| | | |
| | | ## [1.52.0](https://github.com/MetaCubeX/metacubexd/compare/v1.51.1...v1.52.0) (2023-09-02) |
| | | |
| | | ### Features |
| | | |
| | | - auto switch theme ([0bbc661](https://github.com/MetaCubeX/metacubexd/commit/0bbc661e93698312e162e0b8391aea20b553c9a9)) |
| | | |
| | | ## [1.51.1](https://github.com/MetaCubeX/metacubexd/compare/v1.51.0...v1.51.1) (2023-09-02) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - rules page && dns query for mobile ([86fd0d7](https://github.com/MetaCubeX/metacubexd/commit/86fd0d7d9db1aec5841f1b2e4931b98e789e5a93)) |
| | | |
| | | ## [1.51.0](https://github.com/MetaCubeX/metacubexd/compare/v1.50.0...v1.51.0) (2023-09-02) |
| | | |
| | | ### Features |
| | | |
| | | - reset visible ([350fd56](https://github.com/MetaCubeX/metacubexd/commit/350fd56bc98b25dadf5675b4ecdff323565afe97)) |
| | | - rules provider pages ([65346ca](https://github.com/MetaCubeX/metacubexd/commit/65346ca743bf823da841d73c1f31022b1292c53a)) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - update proxy provider api may throw ([ad46745](https://github.com/MetaCubeX/metacubexd/commit/ad46745329343dea2fe69e0b3b3561cab6b54436)) |
| | | |
| | | ## [1.50.0](https://github.com/MetaCubeX/metacubexd/compare/v1.49.0...v1.50.0) (2023-09-02) |
| | | |
| | | ### Features |
| | | |
| | | - separate provider page ([419d3e3](https://github.com/MetaCubeX/metacubexd/commit/419d3e33f31a80c21e2ba16864bcad70d9dd4665)) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - better update provider all ([68ef377](https://github.com/MetaCubeX/metacubexd/commit/68ef377f4bf8690cf3bd371d945e11fd5dcf9b50)) |
| | | |
| | | ## [1.49.0](https://github.com/MetaCubeX/metacubexd/compare/v1.48.0...v1.49.0) (2023-09-02) |
| | | |
| | | ### Features |
| | | |
| | | - auto close all connections before switch proxy ([b79f10f](https://github.com/MetaCubeX/metacubexd/commit/b79f10f528e5a501f8d2b2e3d7e66eae820a00dd)) |
| | | - collapse proxies and provider ([8757924](https://github.com/MetaCubeX/metacubexd/commit/875792432a7d9ea495b0d76c22dd3876bef4c34a)) |
| | | |
| | | ## [1.48.0](https://github.com/MetaCubeX/metacubexd/compare/v1.47.0...v1.48.0) (2023-09-02) |
| | | |
| | | ### Features |
| | | |
| | | - try defalut host and secret && adapt openclash ([fa612a1](https://github.com/MetaCubeX/metacubexd/commit/fa612a12d362a66f9506a7010d209f2bb7967b4b)) |
| | | |
| | | ## [1.47.0](https://github.com/MetaCubeX/metacubexd/compare/v1.46.0...v1.47.0) (2023-09-02) |
| | | |
| | | ### Features |
| | | |
| | | - config for delay test ([e92ce75](https://github.com/MetaCubeX/metacubexd/commit/e92ce752251b107423ff945a10a23abce867c5b3)) |
| | | |
| | | ## [1.46.0](https://github.com/MetaCubeX/metacubexd/compare/v1.45.2...v1.46.0) (2023-09-02) |
| | | |
| | | ### Features |
| | | |
| | | - preview by auto ([3257791](https://github.com/MetaCubeX/metacubexd/commit/3257791dec6851cb02cb627cbba469bf93d9ad90)) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - init scroll from 30 && revert delay map ([c260db1](https://github.com/MetaCubeX/metacubexd/commit/c260db13df031152d02bcd5328da3b373d3b8741)) |
| | | - show option ([1351119](https://github.com/MetaCubeX/metacubexd/commit/135111914d58a5817781b3d657e0803f20e9e5e7)) |
| | | |
| | | ## [1.45.2](https://github.com/MetaCubeX/metacubexd/compare/v1.45.1...v1.45.2) (2023-09-02) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - tool tip for mobile ([b0bac89](https://github.com/MetaCubeX/metacubexd/commit/b0bac89488b6f3a0b9fbe32f74f3ae0f0593cbe7)) |
| | | |
| | | ## [1.45.1](https://github.com/MetaCubeX/metacubexd/compare/v1.45.0...v1.45.1) (2023-09-02) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - dns query component layout ([2464896](https://github.com/MetaCubeX/metacubexd/commit/2464896a4635cf04377485316695807519104cd7)) |
| | | |
| | | ## [1.45.0](https://github.com/MetaCubeX/metacubexd/compare/v1.44.1...v1.45.0) (2023-09-02) |
| | | |
| | | ### Features |
| | | |
| | | - **connections:** reset the ordering of columns ([9d3706f](https://github.com/MetaCubeX/metacubexd/commit/9d3706f39013b0c58860872a95b0c9266628e93c)) |
| | | - i18n for conn table ([33fa191](https://github.com/MetaCubeX/metacubexd/commit/33fa191cfed4e05aeb5bb94aaa7ead792ec8a736)) |
| | | |
| | | ## [1.44.1](https://github.com/MetaCubeX/metacubexd/compare/v1.44.0...v1.44.1) (2023-09-02) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - bar and delay reactive ([9ed0540](https://github.com/MetaCubeX/metacubexd/commit/9ed05401a891bf02af90541e08ec9358ada73cff)) |
| | | |
| | | ## [1.44.0](https://github.com/MetaCubeX/metacubexd/compare/v1.43.0...v1.44.0) (2023-09-02) |
| | | |
| | | ### Features |
| | | |
| | | - preview by bar ([4d29e7c](https://github.com/MetaCubeX/metacubexd/commit/4d29e7c16e64278f2172af4e00b34cf4b10a50ee)) |
| | | |
| | | ## [1.43.0](https://github.com/MetaCubeX/metacubexd/compare/v1.42.1...v1.43.0) (2023-09-02) |
| | | |
| | | ### Features |
| | | |
| | | - choose halloween as default theme ([a1dee05](https://github.com/MetaCubeX/metacubexd/commit/a1dee058255946a5d9e45f72773e9a1260f8bc51)) |
| | | |
| | | ## [1.42.1](https://github.com/MetaCubeX/metacubexd/compare/v1.42.0...v1.42.1) (2023-09-02) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - bytesize in subs info ([4aa7c24](https://github.com/MetaCubeX/metacubexd/commit/4aa7c24ba4d56c49d25d55e69389c966872f5356)) |
| | | |
| | | ## [1.42.0](https://github.com/MetaCubeX/metacubexd/compare/v1.41.2...v1.42.0) (2023-09-01) |
| | | |
| | | ### Features |
| | | |
| | | - adjust table border radius ([9a9b1b4](https://github.com/MetaCubeX/metacubexd/commit/9a9b1b4abef693d48dfa79c5cccd6f808d98e7ac)) |
| | | |
| | | ## [1.41.2](https://github.com/MetaCubeX/metacubexd/compare/v1.41.1...v1.41.2) (2023-09-01) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - header tooltip on menu items instead of inner links ([2619b4f](https://github.com/MetaCubeX/metacubexd/commit/2619b4fd2f2e1f2921e5503196c11644d6e21708)) |
| | | |
| | | ## [1.41.1](https://github.com/MetaCubeX/metacubexd/compare/v1.41.0...v1.41.1) (2023-09-01) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - some themes is overriding global font-family ([c8a41bd](https://github.com/MetaCubeX/metacubexd/commit/c8a41bd29f5b60dc79c6fc623c27d1fbba700408)) |
| | | |
| | | ## [1.41.0](https://github.com/MetaCubeX/metacubexd/compare/v1.40.0...v1.41.0) (2023-09-01) |
| | | |
| | | ### Features |
| | | |
| | | - publish gh-pages to a custom domain ([4b548fb](https://github.com/MetaCubeX/metacubexd/commit/4b548fb219581ad7fe7eea56eb52aeb082ab5bb9)) |
| | | |
| | | ## [1.40.0](https://github.com/MetaCubeX/metacubexd/compare/v1.39.0...v1.40.0) (2023-09-01) |
| | | |
| | | ### Features |
| | | |
| | | - **i18n:** setup page i18n support ([07ee55d](https://github.com/MetaCubeX/metacubexd/commit/07ee55d8d9a9551d1de3da5836436673433d4ba2)) |
| | | |
| | | ## [1.39.0](https://github.com/MetaCubeX/metacubexd/compare/v1.38.0...v1.39.0) (2023-09-01) |
| | | |
| | | ### Features |
| | | |
| | | - **i18n:** add i18n support for navs ([3c7f01a](https://github.com/MetaCubeX/metacubexd/commit/3c7f01aefc4567886c4610aaf0753d037eeaf838)) |
| | | - **i18n:** add more i18n resources ([b3172da](https://github.com/MetaCubeX/metacubexd/commit/b3172da302ddf53a33f6ca092f11eac153401feb)) |
| | | - **i18n:** add toggle i18n button ([5620011](https://github.com/MetaCubeX/metacubexd/commit/56200111bd3dcb147e41dbcec2be6a7481b90f67)) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - default lang ([508e19a](https://github.com/MetaCubeX/metacubexd/commit/508e19a247e90a7a83d1f386851eb13d19e2b58a)) |
| | | |
| | | ## [1.38.0](https://github.com/MetaCubeX/metacubexd/compare/v1.37.0...v1.38.0) (2023-09-01) |
| | | |
| | | ### Features |
| | | |
| | | - **logs:** render logs in table ([7eee024](https://github.com/MetaCubeX/metacubexd/commit/7eee024cdc2c831a75710d62e842ae648818dec8)) |
| | | |
| | | ## [1.37.0](https://github.com/MetaCubeX/metacubexd/compare/v1.36.1...v1.37.0) (2023-09-01) |
| | | |
| | | ### Features |
| | | |
| | | - render in two line ([1807340](https://github.com/MetaCubeX/metacubexd/commit/18073407a6a096ef03433e5f8e4fafd2ebfc27c4)) |
| | | |
| | | ## [1.36.1](https://github.com/MetaCubeX/metacubexd/compare/v1.36.0...v1.36.1) (2023-09-01) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - card click ([25cbdad](https://github.com/MetaCubeX/metacubexd/commit/25cbdad8bc28021131dcadd4339b1b77942e6f71)) |
| | | |
| | | ## [1.36.0](https://github.com/MetaCubeX/metacubexd/compare/v1.35.1...v1.36.0) (2023-09-01) |
| | | |
| | | ### Features |
| | | |
| | | - infinite scroll for proxy card ([2584223](https://github.com/MetaCubeX/metacubexd/commit/258422353f340ae93cab791802a7ae54ae6c8046)) |
| | | |
| | | ## [1.35.1](https://github.com/MetaCubeX/metacubexd/compare/v1.35.0...v1.35.1) (2023-09-01) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - collapse render ([b26e033](https://github.com/MetaCubeX/metacubexd/commit/b26e03373fcc2516cf21acc51756dc2237406000)) |
| | | |
| | | ## [1.35.0](https://github.com/MetaCubeX/metacubexd/compare/v1.34.0...v1.35.0) (2023-09-01) |
| | | |
| | | ### Features |
| | | |
| | | - add PWA support ([a8c9c21](https://github.com/MetaCubeX/metacubexd/commit/a8c9c218985cd3cc470589aa3699a47bf280fd5e)) |
| | | |
| | | ## [1.34.0](https://github.com/MetaCubeX/metacubexd/compare/v1.33.1...v1.34.0) (2023-09-01) |
| | | |
| | | ### Features |
| | | |
| | | - **stats:** flexible size stat on large screen ([f5866dc](https://github.com/MetaCubeX/metacubexd/commit/f5866dcd9dfcdf7d5377d34070d0315500f65742)) |
| | | |
| | | ## [1.33.1](https://github.com/MetaCubeX/metacubexd/compare/v1.33.0...v1.33.1) (2023-09-01) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - cannot import module from connection ([3b774b9](https://github.com/MetaCubeX/metacubexd/commit/3b774b942358ffb2afa78e119093c2e696fda578)) |
| | | - **connections:** destination and host ([bbfd514](https://github.com/MetaCubeX/metacubexd/commit/bbfd5142a3bb8ac2ea1888befaff64c7b522010e)) |
| | | - import path ([13ff26c](https://github.com/MetaCubeX/metacubexd/commit/13ff26c9d3340c68483423d3adfa5dac31f25aed)) |
| | | |
| | | ## [1.33.0](https://github.com/MetaCubeX/metacubexd/compare/v1.32.0...v1.33.0) (2023-09-01) |
| | | |
| | | ### Features |
| | | |
| | | - speed test dot for proxy ([e9c6666](https://github.com/MetaCubeX/metacubexd/commit/e9c6666606c511be4f54582bb73d9bdcb3afd417)) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - delay dot class import ([09a8ede](https://github.com/MetaCubeX/metacubexd/commit/09a8ede9690b0159e5becb294db51379ad21bc90)) |
| | | |
| | | ## [1.32.0](https://github.com/MetaCubeX/metacubexd/compare/v1.31.0...v1.32.0) (2023-09-01) |
| | | |
| | | ### Features |
| | | |
| | | - add a dns query form ([b42bba7](https://github.com/MetaCubeX/metacubexd/commit/b42bba784a11156cb7964f25de840197dedceee7)) |
| | | |
| | | ## [1.31.0](https://github.com/MetaCubeX/metacubexd/compare/v1.30.0...v1.31.0) (2023-09-01) |
| | | |
| | | ### Features |
| | | |
| | | - show subscription usage in a progress bar ([24bfadd](https://github.com/MetaCubeX/metacubexd/commit/24bfaddfc80f0373c181ea873fdc39885f67fe58)) |
| | | - subs info ([45da766](https://github.com/MetaCubeX/metacubexd/commit/45da766ed006ef51312206c17c9d65b9aa4a274b)) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - process not shown ([f5d65eb](https://github.com/MetaCubeX/metacubexd/commit/f5d65eb018f8d2b706d99118e828cf47822cd435)) |
| | | |
| | | ## [1.30.0](https://github.com/MetaCubeX/metacubexd/compare/v1.29.0...v1.30.0) (2023-09-01) |
| | | |
| | | ### Features |
| | | |
| | | - fira sans font ([a00c28e](https://github.com/MetaCubeX/metacubexd/commit/a00c28e601ac587a68500084a76f28e503bde706)) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - default listen port ([75e7c64](https://github.com/MetaCubeX/metacubexd/commit/75e7c64007e006ab1b502de4940bae1a4f3cf048)) |
| | | |
| | | ## [1.29.0](https://github.com/MetaCubeX/metacubexd/compare/v1.28.3...v1.29.0) (2023-09-01) |
| | | |
| | | ### Features |
| | | |
| | | - infinite scroll for rules ([a05c548](https://github.com/MetaCubeX/metacubexd/commit/a05c54854a3ffb1b9d079e14f9d48c0d2e79d2f3)) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - set proxy both from proxies and provider ([2549bfe](https://github.com/MetaCubeX/metacubexd/commit/2549bfe89cc5dd97ff2eb17d2d66ec659f5f046c)) |
| | | |
| | | ## [1.28.3](https://github.com/MetaCubeX/metacubexd/compare/v1.28.2...v1.28.3) (2023-09-01) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - proxy from proxies ([42d9e7b](https://github.com/MetaCubeX/metacubexd/commit/42d9e7bc9f19169d9928157b97a948205ca9cff6)) |
| | | |
| | | ## [1.28.2](https://github.com/MetaCubeX/metacubexd/compare/v1.28.1...v1.28.2) (2023-09-01) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - card info color theme ([80d5138](https://github.com/MetaCubeX/metacubexd/commit/80d5138423312f4c11cd864a01fa353526cde127)) |
| | | |
| | | ## [1.28.1](https://github.com/MetaCubeX/metacubexd/compare/v1.28.0...v1.28.1) (2023-09-01) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - card color theme ([4e5c8bf](https://github.com/MetaCubeX/metacubexd/commit/4e5c8bfe8d525c0857b113447b690d48c953a56b)) |
| | | |
| | | ## [1.28.0](https://github.com/MetaCubeX/metacubexd/compare/v1.27.1...v1.28.0) (2023-09-01) |
| | | |
| | | ### Features |
| | | |
| | | - add twemoji font ([e4a89fc](https://github.com/MetaCubeX/metacubexd/commit/e4a89fc89ecf17420f15c88ea4c30d6ed62ba92a)) |
| | | |
| | | ## [1.27.1](https://github.com/MetaCubeX/metacubexd/compare/v1.27.0...v1.27.1) (2023-08-31) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - border color ([c2cb607](https://github.com/MetaCubeX/metacubexd/commit/c2cb607a452b15da3fbfe9c1d2f5007f8b9fbcb6)) |
| | | |
| | | ## [1.27.0](https://github.com/MetaCubeX/metacubexd/compare/v1.26.0...v1.27.0) (2023-08-31) |
| | | |
| | | ### Features |
| | | |
| | | - sortable column ([4c4011a](https://github.com/MetaCubeX/metacubexd/commit/4c4011a617a5589da13dafcf357b9caef7bf5b32)) |
| | | |
| | | ## [1.26.0](https://github.com/MetaCubeX/metacubexd/compare/v1.25.1...v1.26.0) (2023-08-31) |
| | | |
| | | ### Features |
| | | |
| | | - columns hide ([cb9970c](https://github.com/MetaCubeX/metacubexd/commit/cb9970c29eae947d85e2ba196f92db360fd5add9)) |
| | | |
| | | ## [1.25.1](https://github.com/MetaCubeX/metacubexd/compare/v1.25.0...v1.25.1) (2023-08-31) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - **connections:** update the look of close button ([eee81e6](https://github.com/MetaCubeX/metacubexd/commit/eee81e62585605794c6f77b99b5c82b0e3d03774)) |
| | | |
| | | ## [1.25.0](https://github.com/MetaCubeX/metacubexd/compare/v1.24.0...v1.25.0) (2023-08-31) |
| | | |
| | | ### Features |
| | | |
| | | - update secret when changed ([#48](https://github.com/MetaCubeX/metacubexd/issues/48)) ([935908e](https://github.com/MetaCubeX/metacubexd/commit/935908e0c1f4885a89a27008d5d7f6cd5e52ff68)) |
| | | |
| | | ## [1.24.0](https://github.com/MetaCubeX/metacubexd/compare/v1.23.0...v1.24.0) (2023-08-31) |
| | | |
| | | ### Features |
| | | |
| | | - dayjs ([b4fc1b4](https://github.com/MetaCubeX/metacubexd/commit/b4fc1b4c983cebe7a52dd08f4b2b49dd6eb6a17d)) |
| | | - health check for provider ([03dda3c](https://github.com/MetaCubeX/metacubexd/commit/03dda3ccad684a326022022c7c83155ee9d32410)) |
| | | |
| | | ## [1.23.0](https://github.com/MetaCubeX/metacubexd/compare/v1.22.0...v1.23.0) (2023-08-31) |
| | | |
| | | ### Features |
| | | |
| | | - config port && hide id in table by default ([e0f5c46](https://github.com/MetaCubeX/metacubexd/commit/e0f5c46a2eb1876b7d412b773139c752414888e4)) |
| | | - update provider && rule pages style ([c0fee9d](https://github.com/MetaCubeX/metacubexd/commit/c0fee9d9298f807fb6380e72d49166609e9ac262)) |
| | | |
| | | ## [1.22.0](https://github.com/MetaCubeX/metacubexd/compare/v1.21.0...v1.22.0) (2023-08-30) |
| | | |
| | | ### Features |
| | | |
| | | - hide drawer on menu click ([7ce001c](https://github.com/MetaCubeX/metacubexd/commit/7ce001caa11c7aa5c177957777708316e731231d)) |
| | | |
| | | ## [1.21.0](https://github.com/MetaCubeX/metacubexd/compare/v1.20.0...v1.21.0) (2023-08-30) |
| | | |
| | | ### Features |
| | | |
| | | - hide scrollbar, update overlay menu ([9919cca](https://github.com/MetaCubeX/metacubexd/commit/9919cca00e4315d0efd4dc0e8dcba9287de79f9b)) |
| | | - update header ([7a32ec4](https://github.com/MetaCubeX/metacubexd/commit/7a32ec4fe37fca26642e44402ef4309453c252c4)) |
| | | - update proxy layout ([b60c855](https://github.com/MetaCubeX/metacubexd/commit/b60c85566cbe4704dffe065a9e41c9c5c83c9365)) |
| | | |
| | | ## [1.20.0](https://github.com/MetaCubeX/metacubexd/compare/v1.19.0...v1.20.0) (2023-08-30) |
| | | |
| | | ### Features |
| | | |
| | | - render protocl and udp ([44df379](https://github.com/MetaCubeX/metacubexd/commit/44df379fd6a18ef7d855dac91897480421e494e5)) |
| | | - truncate proxy node name ([6db7ced](https://github.com/MetaCubeX/metacubexd/commit/6db7ceddac226e81e4009d4e333f73bac985bd67)) |
| | | - update proxy layout ([83b0de8](https://github.com/MetaCubeX/metacubexd/commit/83b0de831a6d6eedc1c2b8ad74de1416752dd835)) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - card protocol text style ([4a03662](https://github.com/MetaCubeX/metacubexd/commit/4a03662bbd3a64a481bca96f0c81c78efd8bab7e)) |
| | | |
| | | ## [1.19.0](https://github.com/MetaCubeX/metacubexd/compare/v1.18.0...v1.19.0) (2023-08-30) |
| | | |
| | | ### Features |
| | | |
| | | - proxy page style ([#47](https://github.com/MetaCubeX/metacubexd/issues/47)) ([51f7121](https://github.com/MetaCubeX/metacubexd/commit/51f712157fac4d727d7dedbb6f67428f05515e06)) |
| | | |
| | | ## [1.18.0](https://github.com/MetaCubeX/metacubexd/compare/v1.17.0...v1.18.0) (2023-08-30) |
| | | |
| | | ### Features |
| | | |
| | | - **connections:** reverse chains ([43edb25](https://github.com/MetaCubeX/metacubexd/commit/43edb25c10e7ead51591b62ffe8e128275803c16)) |
| | | - prevent duplicate endpoints from being stored ([#45](https://github.com/MetaCubeX/metacubexd/issues/45)) ([e966288](https://github.com/MetaCubeX/metacubexd/commit/e96628845b2efc32d978f273c9e245a4e83c2246)) |
| | | |
| | | ## [1.17.0](https://github.com/MetaCubeX/metacubexd/compare/v1.16.0...v1.17.0) (2023-08-30) |
| | | |
| | | ### Features |
| | | |
| | | - redirect illegal path to overview ([#41](https://github.com/MetaCubeX/metacubexd/issues/41)) ([a114b35](https://github.com/MetaCubeX/metacubexd/commit/a114b35c1abcc5eb3b53c497ab689f9eb138deb7)) |
| | | |
| | | ## [1.16.0](https://github.com/MetaCubeX/metacubexd/compare/v1.15.0...v1.16.0) (2023-08-30) |
| | | |
| | | ### Features |
| | | |
| | | - route to override immediately after checking ([#39](https://github.com/MetaCubeX/metacubexd/issues/39)) ([b178161](https://github.com/MetaCubeX/metacubexd/commit/b178161dbd108f4ae9ed59352d0d0450113051f0)) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - proxies title height ([b64cfa3](https://github.com/MetaCubeX/metacubexd/commit/b64cfa3f9a774143ceab492c26c67215dad6b195)) |
| | | |
| | | ## [1.15.0](https://github.com/MetaCubeX/metacubexd/compare/v1.14.2...v1.15.0) (2023-08-30) |
| | | |
| | | ### Features |
| | | |
| | | - speed test ([#38](https://github.com/MetaCubeX/metacubexd/issues/38)) ([a57741b](https://github.com/MetaCubeX/metacubexd/commit/a57741baf6a2b435c5349fa64adcec7210910a78)) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - support sing-box ([#36](https://github.com/MetaCubeX/metacubexd/issues/36)) ([4eeb345](https://github.com/MetaCubeX/metacubexd/commit/4eeb3458ccf389dda2a6e883877355da55149b66)) |
| | | |
| | | ## [1.14.2](https://github.com/MetaCubeX/metacubexd/compare/v1.14.1...v1.14.2) (2023-08-30) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - drop linux/arm/v7 ([f03c6fd](https://github.com/MetaCubeX/metacubexd/commit/f03c6fdf4cb5ff0916165c75c66ba54abb68c2d8)) |
| | | |
| | | ## [1.14.1](https://github.com/MetaCubeX/metacubexd/compare/v1.14.0...v1.14.1) (2023-08-30) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - support child path to fix router issue ([1091234](https://github.com/MetaCubeX/metacubexd/commit/10912347032f02e87c6754bb109510e1873a429f)) |
| | | |
| | | ## [1.14.0](https://github.com/MetaCubeX/metacubexd/compare/v1.13.2...v1.14.0) (2023-08-29) |
| | | |
| | | ### Features |
| | | |
| | | - **docker:** multi-platform support ([c7a8af4](https://github.com/MetaCubeX/metacubexd/commit/c7a8af420f35b1681680df48a4713e0639b586a5)) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - **dependabot:** package-ecosystem field is not a valid value ([567ffa8](https://github.com/MetaCubeX/metacubexd/commit/567ffa889463183a6a849af8fc03dd6de06fe81e)) |
| | | |
| | | ## [1.13.2](https://github.com/MetaCubeX/metacubexd/compare/v1.13.1...v1.13.2) (2023-08-29) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - remove permissions definition ([154d780](https://github.com/MetaCubeX/metacubexd/commit/154d7809f65f1b9794c4586219b4566161573538)) |
| | | |
| | | ## [1.13.1](https://github.com/MetaCubeX/metacubexd/compare/v1.13.0...v1.13.1) (2023-08-29) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - repository name should be lowercase ([5d5b9f2](https://github.com/MetaCubeX/metacubexd/commit/5d5b9f290e8dbc236cd70ba476a5769c0fbda25e)) |
| | | |
| | | ## [1.13.0](https://github.com/MetaCubeX/metacubexd/compare/v1.12.0...v1.13.0) (2023-08-29) |
| | | |
| | | ### Features |
| | | |
| | | - publish docker image ([7d5b3cd](https://github.com/MetaCubeX/metacubexd/commit/7d5b3cddccbb3eba86af2367d163664d2c8e8ba4)) |
| | | |
| | | ## [1.12.0](https://github.com/MetaCubeX/metacubexd/compare/v1.11.0...v1.12.0) (2023-08-29) |
| | | |
| | | ### Features |
| | | |
| | | - **footer:** drop footer ([fc0fbf9](https://github.com/MetaCubeX/metacubexd/commit/fc0fbf9daef45054a3f5e13e64e472823a6fe072)) |
| | | - **header:** responsive layout ([2d9daa9](https://github.com/MetaCubeX/metacubexd/commit/2d9daa96cc3bd0b947cd7b34dafcb56eca91cfbe)) |
| | | |
| | | ## [1.11.0](https://github.com/MetaCubeX/metacubexd/compare/v1.10.0...v1.11.0) (2023-08-29) |
| | | |
| | | ### Features |
| | | |
| | | - **connections:** fallback host column to destinationIP ([4e5f8d2](https://github.com/MetaCubeX/metacubexd/commit/4e5f8d2b3d6c6dc7519f8a12b1060ecf2bb32771)) |
| | | - show downlaod and upload speed in per-second form ([49bf6c7](https://github.com/MetaCubeX/metacubexd/commit/49bf6c7e2d76c7de0bf9cb6f538a369460080628)) |
| | | |
| | | ## [1.10.0](https://github.com/MetaCubeX/metacubexd/compare/v1.9.0...v1.10.0) (2023-08-29) |
| | | |
| | | ### Features |
| | | |
| | | - calculate download speed and upload speed ([6a1c1ab](https://github.com/MetaCubeX/metacubexd/commit/6a1c1abcbceffbc91c70a18e689a778be51c951c)) |
| | | - update connection columns ([7336948](https://github.com/MetaCubeX/metacubexd/commit/7336948ad3a5c422bd20b46e99ae056f8b4b53d9)) |
| | | |
| | | ## [1.9.0](https://github.com/MetaCubeX/metacubexd/compare/v1.8.1...v1.9.0) (2023-08-29) |
| | | |
| | | ### Features |
| | | |
| | | - proxy selectable ([#19](https://github.com/MetaCubeX/metacubexd/issues/19)) ([6b08020](https://github.com/MetaCubeX/metacubexd/commit/6b0802023d3eeb41a76e7a3ee1087c5321a19100)) |
| | | |
| | | ## [1.8.1](https://github.com/MetaCubeX/metacubexd/compare/v1.8.0...v1.8.1) (2023-08-29) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - charts data slice ([#17](https://github.com/MetaCubeX/metacubexd/issues/17)) ([35f8042](https://github.com/MetaCubeX/metacubexd/commit/35f80426ce083265ef469bf0d20b0ba219a37495)) |
| | | |
| | | ## [1.8.0](https://github.com/MetaCubeX/metacubexd/compare/v1.7.0...v1.8.0) (2023-08-29) |
| | | |
| | | ### Features |
| | | |
| | | - proxy point && delay ([#15](https://github.com/MetaCubeX/metacubexd/issues/15)) ([341cdd0](https://github.com/MetaCubeX/metacubexd/commit/341cdd0113e80c21c267fd3f7286e3a90a1cc414)) |
| | | |
| | | ## [1.7.0](https://github.com/MetaCubeX/metacubexd/compare/v1.6.0...v1.7.0) (2023-08-28) |
| | | |
| | | ### Features |
| | | |
| | | - add footer ([e4bfb3f](https://github.com/MetaCubeX/metacubexd/commit/e4bfb3f2a8e22ce2e32f4cc5a4493a40e81b6f5a)) |
| | | - small size table ([9b56558](https://github.com/MetaCubeX/metacubexd/commit/9b56558f84a644fa38c87916c5761b380518c910)) |
| | | |
| | | ## [1.6.0](https://github.com/MetaCubeX/metacubexd/compare/v1.5.0...v1.6.0) (2023-08-28) |
| | | |
| | | ### Features |
| | | |
| | | - update header layout ([07c82e8](https://github.com/MetaCubeX/metacubexd/commit/07c82e87d83b8117ad603857723b995f37a058f6)) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - theme icon height ([#13](https://github.com/MetaCubeX/metacubexd/issues/13)) ([2889de2](https://github.com/MetaCubeX/metacubexd/commit/2889de205f1f9f862021fed96a808bb69b207f5d)) |
| | | |
| | | ## [1.5.0](https://github.com/MetaCubeX/metacubexd/compare/v1.4.0...v1.5.0) (2023-08-28) |
| | | |
| | | ### Features |
| | | |
| | | - rules page ([#10](https://github.com/MetaCubeX/metacubexd/issues/10)) ([457b498](https://github.com/MetaCubeX/metacubexd/commit/457b49827dd38b61e63fd73e90a3736c956b4157)) |
| | | |
| | | ## [1.4.0](https://github.com/MetaCubeX/metacubexd/compare/v1.3.0...v1.4.0) (2023-08-27) |
| | | |
| | | ### Features |
| | | |
| | | - **connections:** add sorting and close connection support ([45c9740](https://github.com/MetaCubeX/metacubexd/commit/45c9740d3bee08e8cf874c3d866b9c9c3249a26e)) |
| | | |
| | | ## [1.3.0](https://github.com/MetaCubeX/metacubexd/compare/v1.2.0...v1.3.0) (2023-08-27) |
| | | |
| | | ### Features |
| | | |
| | | - show vertical stats on small screen devices ([841161a](https://github.com/MetaCubeX/metacubexd/commit/841161a0da9f753bff3d9754b29fc0a653c97af9)) |
| | | |
| | | ## [1.2.0](https://github.com/MetaCubeX/metacubexd/compare/v1.1.0...v1.2.0) (2023-08-27) |
| | | |
| | | ### Features |
| | | |
| | | - update proxies page layout ([54e5e1f](https://github.com/MetaCubeX/metacubexd/commit/54e5e1fc2407eff607b6225a957a13258164584c)) |
| | | |
| | | ## [1.1.0](https://github.com/MetaCubeX/metacubexd/compare/v1.0.3...v1.1.0) (2023-08-27) |
| | | |
| | | ### Features |
| | | |
| | | - add bg color and text color for stats ([c273169](https://github.com/MetaCubeX/metacubexd/commit/c27316967b8685e600a3af9dd7698498f838bdda)) |
| | | |
| | | ## [1.0.3](https://github.com/MetaCubeX/metacubexd/compare/v1.0.2...v1.0.3) (2023-08-27) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - publish zip instead of assets ([f7ee4f7](https://github.com/MetaCubeX/metacubexd/commit/f7ee4f7faa362afa60487f90b2ddae051f85b535)) |
| | | |
| | | ## [1.0.2](https://github.com/MetaCubeX/metacubexd/compare/v1.0.1...v1.0.2) (2023-08-27) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - release artifacts ([3bab638](https://github.com/MetaCubeX/metacubexd/commit/3bab638f502efb6aedd1372feed829d95c3fd806)) |
| | | |
| | | ## [1.0.1](https://github.com/MetaCubeX/metacubexd/compare/v1.0.0...v1.0.1) (2023-08-27) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - publish issue ([a3897da](https://github.com/MetaCubeX/metacubexd/commit/a3897da0f631c12612ff8eb945cad5c9ab6eef24)) |
| | | |
| | | ## 1.0.0 (2023-08-27) |
| | | |
| | | ### Features |
| | | |
| | | - use hash router instead of browser ([#1](https://github.com/MetaCubeX/metacubexd/issues/1)) ([b9ab6a9](https://github.com/MetaCubeX/metacubexd/commit/b9ab6a924487a241719928ae00c3965efcbcc71b)) |
| | | |
| | | ### Bug Fixes |
| | | |
| | | - layout issue on mobile platform ([35a02e4](https://github.com/MetaCubeX/metacubexd/commit/35a02e4174194efd09a3d1ac98dfd1817828e0f7)) |
| | | - setup failed when secret is required ([b660847](https://github.com/MetaCubeX/metacubexd/commit/b660847efee6e00461017244adaa63b1e7c2230a)) |
| New file |
| | |
| | | :80 { |
| | | file_server |
| | | |
| | | root * . |
| | | |
| | | try_files {path} /index.html |
| | | } |
| New file |
| | |
| | | FROM docker.io/node:alpine as builder |
| | | |
| | | WORKDIR /build |
| | | |
| | | COPY . . |
| | | |
| | | RUN corepack enable |
| | | RUN corepack prepare pnpm@latest --activate |
| | | RUN pnpm install |
| | | RUN pnpm build |
| | | |
| | | FROM docker.io/caddy:alpine |
| | | |
| | | EXPOSE 80 |
| | | |
| | | WORKDIR /srv |
| | | |
| | | COPY --from=builder /build/dist/. . |
| | | COPY Caddyfile . |
| | | |
| | | CMD ["caddy", "run"] |
| New file |
| | |
| | | MIT License Copyright (c) 2023 MetaCubeX |
| | | |
| | | Permission is hereby granted, free of |
| | | charge, to any person obtaining a copy of this software and associated |
| | | documentation files (the "Software"), to deal in the Software without |
| | | restriction, including without limitation the rights to use, copy, modify, merge, |
| | | publish, distribute, sublicense, and/or sell copies of the Software, and to |
| | | permit persons to whom the Software is furnished to do so, subject to the |
| | | following conditions: |
| | | |
| | | The above copyright notice and this permission notice |
| | | (including the next paragraph) shall be included in all copies or substantial |
| | | portions of the Software. |
| | | |
| | | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF |
| | | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
| | | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO |
| | | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR |
| | | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
| | | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| | | THE SOFTWARE. |
| New file |
| | |
| | | mixed-port: 7890 |
| | | geodata-mode: true |
| | | tcp-concurrent: true |
| | | allow-lan: true |
| | | bind-address: '*' |
| | | mode: rule |
| | | log-level: info |
| | | ipv6: true |
| | | external-controller: 0.0.0.0:9090 |
| | | global-client-fingerprint: random |
| | | |
| | | profile: |
| | | store-selected: true |
| | | |
| | | tun: |
| | | enable: true |
| | | device: meta |
| | | stack: system |
| | | auto-route: true |
| | | auto-redir: true |
| | | |
| | | sniffer: |
| | | enable: true |
| | | force-dns-mapping: true |
| | | parse-pure-ip: true |
| | | override-destination: true |
| | | sniff: |
| | | TLS: |
| | | HTTP: |
| | | |
| | | dns: |
| | | enable: true |
| | | listen: :53 |
| | | enhanced-mode: redir-host |
| | | proxy-server-nameserver: |
| | | - https://dns.alidns.com/dns-query |
| | | - https://doh.pub/dns-query |
| | | |
| | | rule-providers: |
| | | reject: |
| | | type: http |
| | | behavior: domain |
| | | url: 'https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/reject.txt' |
| | | path: ./ruleset/reject.yaml |
| | | interval: 86400 |
| | | |
| | | icloud: |
| | | type: http |
| | | behavior: domain |
| | | url: 'https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/icloud.txt' |
| | | path: ./ruleset/icloud.yaml |
| | | interval: 86400 |
| | | |
| | | apple: |
| | | type: http |
| | | behavior: domain |
| | | url: 'https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/apple.txt' |
| | | path: ./ruleset/apple.yaml |
| | | interval: 86400 |
| | | |
| | | google: |
| | | type: http |
| | | behavior: domain |
| | | url: 'https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/google.txt' |
| | | path: ./ruleset/google.yaml |
| | | interval: 86400 |
| | | |
| | | proxy: |
| | | type: http |
| | | behavior: domain |
| | | url: 'https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/proxy.txt' |
| | | path: ./ruleset/proxy.yaml |
| | | interval: 86400 |
| | | |
| | | direct: |
| | | type: http |
| | | behavior: domain |
| | | url: 'https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/direct.txt' |
| | | path: ./ruleset/direct.yaml |
| | | interval: 86400 |
| | | |
| | | private: |
| | | type: http |
| | | behavior: domain |
| | | url: 'https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/private.txt' |
| | | |
| | | gfw: |
| | | type: http |
| | | behavior: domain |
| | | url: 'https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/gfw.txt' |
| | | path: ./ruleset/gfw.yaml |
| | | interval: 86400 |
| | | |
| | | tld-not-cn: |
| | | type: http |
| | | behavior: domain |
| | | url: 'https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/tld-not-cn.txt' |
| | | path: ./ruleset/tld-not-cn.yaml |
| | | interval: 86400 |
| | | |
| | | telegramcidr: |
| | | type: http |
| | | behavior: ipcidr |
| | | url: 'https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/telegramcidr.txt' |
| | | path: ./ruleset/telegramcidr.yaml |
| | | interval: 86400 |
| | | |
| | | cncidr: |
| | | type: http |
| | | behavior: ipcidr |
| | | url: 'https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/cncidr.txt' |
| | | path: ./ruleset/cncidr.yaml |
| | | interval: 86400 |
| | | |
| | | lancidr: |
| | | type: http |
| | | behavior: ipcidr |
| | | url: 'https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/lancidr.txt' |
| | | path: ./ruleset/lancidr.yaml |
| | | interval: 86400 |
| | | |
| | | applications: |
| | | type: http |
| | | behavior: classical |
| | | url: 'https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/applications.txt' |
| | | path: ./ruleset/applications.yaml |
| | | interval: 86400 |
| | | |
| | | rules: |
| | | - RULE-SET,private,DIRECT |
| | | - RULE-SET,reject,REJECT |
| | | - RULE-SET,icloud,DIRECT |
| | | - RULE-SET,apple,DIRECT |
| | | - RULE-SET,direct,DIRECT |
| | | - RULE-SET,lancidr,DIRECT |
| | | - RULE-SET,cncidr,DIRECT |
| | | - GEOIP,LAN,DIRECT |
| | | - GEOIP,CN,DIRECT |
| | | |
| | | - MATCH,PROXY |
| New file |
| | |
| | | version: '3' |
| | | |
| | | services: |
| | | metacubexd: |
| | | container_name: metacubexd |
| | | image: ghcr.io/metacubex/metacubexd |
| | | restart: always |
| | | ports: |
| | | - '80:80' |
| | | |
| | | # optional |
| | | meta: |
| | | container_name: meta |
| | | image: docker.io/metacubex/clash-meta:Alpha |
| | | restart: always |
| | | network_mode: host |
| | | cap_add: |
| | | - NET_ADMIN |
| | | volumes: |
| | | - ./config.yaml:/root/.config/clash |
| New file |
| | |
| | | <!doctype html> |
| | | <html lang="en"> |
| | | |
| | | <head> |
| | | <meta charset="utf-8" /> |
| | | <meta name="viewport" |
| | | content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" /> |
| | | <meta name="theme-color" content="#000000" /> |
| | | <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> |
| | | <link rel="apple-touch-icon" href="/pwa-192x192.png" /> |
| | | |
| | | <title>metacubexd</title> |
| | | </head> |
| | | |
| | | <body> |
| | | <noscript>You need to enable JavaScript to run this app.</noscript> |
| | | <button id="btn_play" onclick="javascript:callCSharpMethod()">测试(From JS to C#)</button> |
| | | |
| | | <div id="root" style="display: contents"></div> |
| | | <script> |
| | | // 测试在Web中调用C#的方法 |
| | | function callCSharpMethod() { |
| | | console.log("a test from web page js:") |
| | | window.backendSharp.showAlertMsg("来自JS的调用"); |
| | | } |
| | | |
| | | |
| | | // 测试C#调用JS的方法,只传递一个普通的字符串 |
| | | function displayMessage(message) { |
| | | alert(message); |
| | | } |
| | | </script> |
| | | <script src="/src/main.tsx" type="module"></script> |
| | | </body> |
| | | |
| | | </html> |
| New file |
| | |
| | | { |
| | | "name": "metacubexd", |
| | | "version": "1.124.2", |
| | | "description": "Clash.Meta Dashboard, The Official One, XD", |
| | | "license": "MIT", |
| | | "type": "module", |
| | | "scripts": { |
| | | "build": "vite build", |
| | | "dev": "vite --host 0.0.0.0", |
| | | "format": "prettier -w .", |
| | | "lint": "eslint --fix .", |
| | | "prepare": "husky install", |
| | | "pwa-assets-generator": "pwa-assets-generator", |
| | | "serve": "vite preview" |
| | | }, |
| | | "dependencies": { |
| | | "@commitlint/config-conventional": "^17.7.0", |
| | | "@felte/solid": "^1.2.11", |
| | | "@felte/validator-zod": "^1.0.17", |
| | | "@fontsource/fira-sans": "^5.0.15", |
| | | "@solid-primitives/clipboard": "^1.5.7", |
| | | "@solid-primitives/context": "^0.2.1", |
| | | "@solid-primitives/event-listener": "^2.3.0", |
| | | "@solid-primitives/i18n": "^2.0.0", |
| | | "@solid-primitives/keyed": "^1.2.0", |
| | | "@solid-primitives/media": "^2.2.5", |
| | | "@solid-primitives/resize-observer": "^2.0.22", |
| | | "@solid-primitives/storage": "^2.1.1", |
| | | "@solid-primitives/timer": "^1.3.7", |
| | | "@solid-primitives/websocket": "^1.1.0", |
| | | "@solidjs/router": "^0.8.3", |
| | | "@tabler/icons-solidjs": "^2.39.0", |
| | | "@tanstack/match-sorter-utils": "^8.8.4", |
| | | "@tanstack/solid-table": "^8.10.7", |
| | | "@tanstack/solid-virtual": "3.0.0-beta.65", |
| | | "@tanstack/virtual-core": "3.0.0-beta.65", |
| | | "@thisbeyond/solid-dnd": "^0.7.4", |
| | | "@types/byte-size": "^8.1.0", |
| | | "@types/lodash": "^4.14.199", |
| | | "@types/node": "^20.8.6", |
| | | "@types/uuid": "^9.0.5", |
| | | "@typescript-eslint/eslint-plugin": "^6.7.5", |
| | | "@typescript-eslint/parser": "^6.7.5", |
| | | "@vite-pwa/assets-generator": "^0.0.10", |
| | | "apexcharts": "^3.43.0", |
| | | "autoprefixer": "^10.4.16", |
| | | "byte-size": "^8.1.1", |
| | | "commitlint": "^17.7.2", |
| | | "daisyui": "^3.9.2", |
| | | "dayjs": "^1.11.10", |
| | | "eslint": "^8.51.0", |
| | | "eslint-config-prettier": "^9.0.0", |
| | | "eslint-plugin-prettier": "^5.0.1", |
| | | "husky": "^8.0.3", |
| | | "is-ip": "^5.0.1", |
| | | "ky": "^1.0.1", |
| | | "lint-staged": "^14.0.1", |
| | | "lodash": "^4.17.21", |
| | | "match-sorter": "^6.3.1", |
| | | "prettier": "^3.0.3", |
| | | "prettier-plugin-organize-imports": "^3.2.3", |
| | | "prettier-plugin-tailwindcss": "^0.5.6", |
| | | "solid-apexcharts": "^0.3.2", |
| | | "solid-js": "^1.8.1", |
| | | "solid-toast": "^0.5.0", |
| | | "sort-package-json": "^2.6.0", |
| | | "tailwind-merge": "^1.14.0", |
| | | "tailwindcss": "^3.3.3", |
| | | "tailwindcss-safe-area": "^0.4.1", |
| | | "typescript": "^5.2.2", |
| | | "uuid": "^9.0.1", |
| | | "vite": "^4.4.11", |
| | | "vite-plugin-pwa": "^0.16.5", |
| | | "vite-plugin-solid": "^2.7.1", |
| | | "zod": "^3.22.4" |
| | | } |
| | | } |
| New file |
| | |
| | | export default { |
| | | plugins: { |
| | | tailwindcss: {}, |
| | | autoprefixer: {}, |
| | | }, |
| | | } |
| New file |
| | |
| | | <?xml version="1.0" encoding="UTF-8" standalone="no"?> |
| | | <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> |
| | | <svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0.00 0.00 200.00 200.00"> |
| | | <g stroke-width="2.00" fill="none" stroke-linecap="butt"> |
| | | <path stroke="#babcbd" vector-effect="non-scaling-stroke" d=" |
| | | M 40.95 140.48 |
| | | Q 68.55 157.30 96.63 173.25 |
| | | C 102.24 176.44 107.25 175.99 112.94 172.72 |
| | | Q 137.94 158.32 162.95 143.97 |
| | | Q 165.51 142.50 167.13 140.70 |
| | | A 1.36 1.36 0.0 0 0 167.49 139.78 |
| | | C 167.62 114.80 168.11 94.50 166.94 76.07 |
| | | C 166.62 71.04 164.79 67.55 160.95 64.72" |
| | | /> |
| | | <path stroke="#edefef" vector-effect="non-scaling-stroke" d=" |
| | | M 160.95 64.72 |
| | | C 158.30 59.86 148.11 55.99 143.18 53.08 |
| | | C 131.17 46.00 119.07 39.12 106.93 32.27 |
| | | C 102.78 29.92 100.95 27.45 95.80 30.54 |
| | | Q 86.26 36.27 72.74 43.77 |
| | | C 65.00 48.06 55.94 53.83 47.28 58.44 |
| | | C 36.47 64.19 37.03 68.23 37.02 79.49 |
| | | Q 36.99 107.48 37.00 135.54 |
| | | A 1.33 1.30 24.1 0 0 37.35 136.43 |
| | | L 40.95 140.48" |
| | | /> |
| | | <path stroke="#cdcdcd" vector-effect="non-scaling-stroke" d=" |
| | | M 160.95 64.72 |
| | | C 162.26 67.10 163.25 68.73 163.25 71.58 |
| | | Q 163.26 102.51 163.09 133.43 |
| | | Q 163.07 137.05 160.83 138.33 |
| | | Q 133.44 154.04 106.07 169.70 |
| | | C 102.53 171.72 96.73 171.58 93.59 169.67 |
| | | C 81.84 162.53 70.33 156.39 57.98 148.94 |
| | | Q 49.77 143.99 40.95 140.48" |
| | | /> |
| | | <path stroke="#848484" vector-effect="non-scaling-stroke" d=" |
| | | M 155.05 66.47 |
| | | A 0.30 0.30 0.0 0 0 155.05 65.95 |
| | | L 100.11 34.53 |
| | | A 0.30 0.30 0.0 0 0 99.81 34.53 |
| | | L 44.98 65.96 |
| | | A 0.30 0.30 0.0 0 0 44.98 66.48 |
| | | L 99.82 97.91 |
| | | A 0.30 0.30 0.0 0 0 100.12 97.91 |
| | | L 155.05 66.47" |
| | | /> |
| | | <path stroke="#848484" vector-effect="non-scaling-stroke" d=" |
| | | M 97.83 102.22 |
| | | A 0.32 0.32 0.0 0 0 97.67 101.94 |
| | | L 42.48 70.37 |
| | | A 0.32 0.32 0.0 0 0 42.00 70.65 |
| | | L 42.00 133.51 |
| | | A 0.32 0.32 0.0 0 0 42.16 133.79 |
| | | L 97.04 165.13 |
| | | A 0.32 0.32 0.0 0 0 97.52 164.85 |
| | | L 97.83 102.22" |
| | | /> |
| | | <path stroke="#848484" vector-effect="non-scaling-stroke" d=" |
| | | M 102.35 101.92 |
| | | A 0.32 0.32 0.0 0 0 102.19 102.20 |
| | | L 102.47 164.86 |
| | | A 0.32 0.32 0.0 0 0 102.95 165.14 |
| | | L 157.78 133.85 |
| | | A 0.32 0.32 0.0 0 0 157.94 133.57 |
| | | L 157.72 70.85 |
| | | A 0.32 0.32 0.0 0 0 157.24 70.57 |
| | | L 102.35 101.92" |
| | | /> |
| | | <path stroke="#848484" vector-effect="non-scaling-stroke" d=" |
| | | M 101.2539 41.9921 |
| | | A 1.17 1.17 0.0 0 0 100.0859 40.8201 |
| | | L 100.0459 40.8200 |
| | | A 1.17 1.17 0.0 0 0 98.8739 41.9880 |
| | | L 98.7861 92.2679 |
| | | A 1.17 1.17 0.0 0 0 99.9541 93.4399 |
| | | L 99.9941 93.4400 |
| | | A 1.17 1.17 0.0 0 0 101.1661 92.2720 |
| | | L 101.2539 41.9921" |
| | | /> |
| | | <path stroke="#848484" vector-effect="non-scaling-stroke" d=" |
| | | M 93.8121 104.2576 |
| | | A 1.09 1.09 0.0 0 0 92.3238 103.8561 |
| | | L 48.3895 129.1194 |
| | | A 1.09 1.09 0.0 0 0 47.9879 130.6077 |
| | | L 48.0079 130.6424 |
| | | A 1.09 1.09 0.0 0 0 49.4962 131.0439 |
| | | L 93.4305 105.7806 |
| | | A 1.09 1.09 0.0 0 0 93.8321 104.2923 |
| | | L 93.8121 104.2576" |
| | | /> |
| | | <path stroke="#848484" vector-effect="non-scaling-stroke" d=" |
| | | M 152.0194 130.6077 |
| | | A 1.09 1.09 0.0 0 0 151.6178 129.1194 |
| | | L 107.6488 103.8361 |
| | | A 1.09 1.09 0.0 0 0 106.1606 104.2377 |
| | | L 106.1406 104.2723 |
| | | A 1.09 1.09 0.0 0 0 106.5422 105.7606 |
| | | L 150.5112 131.0439 |
| | | A 1.09 1.09 0.0 0 0 151.9994 130.6423 |
| | | L 152.0194 130.6077" |
| | | /> |
| | | </g> |
| | | <path fill="#dadedf" d=" |
| | | M 0.00 0.00 |
| | | L 200.00 0.00 |
| | | L 200.00 200.00 |
| | | L 0.00 200.00 |
| | | L 0.00 0.00 |
| | | Z |
| | | M 40.95 140.48 |
| | | Q 68.55 157.30 96.63 173.25 |
| | | C 102.24 176.44 107.25 175.99 112.94 172.72 |
| | | Q 137.94 158.32 162.95 143.97 |
| | | Q 165.51 142.50 167.13 140.70 |
| | | A 1.36 1.36 0.0 0 0 167.49 139.78 |
| | | C 167.62 114.80 168.11 94.50 166.94 76.07 |
| | | C 166.62 71.04 164.79 67.55 160.95 64.72 |
| | | C 158.30 59.86 148.11 55.99 143.18 53.08 |
| | | C 131.17 46.00 119.07 39.12 106.93 32.27 |
| | | C 102.78 29.92 100.95 27.45 95.80 30.54 |
| | | Q 86.26 36.27 72.74 43.77 |
| | | C 65.00 48.06 55.94 53.83 47.28 58.44 |
| | | C 36.47 64.19 37.03 68.23 37.02 79.49 |
| | | Q 36.99 107.48 37.00 135.54 |
| | | A 1.33 1.30 24.1 0 0 37.35 136.43 |
| | | L 40.95 140.48 |
| | | Z" |
| | | /> |
| | | <path fill="#ffffff" d=" |
| | | M 160.95 64.72 |
| | | C 162.26 67.10 163.25 68.73 163.25 71.58 |
| | | Q 163.26 102.51 163.09 133.43 |
| | | Q 163.07 137.05 160.83 138.33 |
| | | Q 133.44 154.04 106.07 169.70 |
| | | C 102.53 171.72 96.73 171.58 93.59 169.67 |
| | | C 81.84 162.53 70.33 156.39 57.98 148.94 |
| | | Q 49.77 143.99 40.95 140.48 |
| | | L 37.35 136.43 |
| | | A 1.33 1.30 24.1 0 1 37.00 135.54 |
| | | Q 36.99 107.48 37.02 79.49 |
| | | C 37.03 68.23 36.47 64.19 47.28 58.44 |
| | | C 55.94 53.83 65.00 48.06 72.74 43.77 |
| | | Q 86.26 36.27 95.80 30.54 |
| | | C 100.95 27.45 102.78 29.92 106.93 32.27 |
| | | C 119.07 39.12 131.17 46.00 143.18 53.08 |
| | | C 148.11 55.99 158.30 59.86 160.95 64.72 |
| | | Z |
| | | M 155.05 66.47 |
| | | A 0.30 0.30 0.0 0 0 155.05 65.95 |
| | | L 100.11 34.53 |
| | | A 0.30 0.30 0.0 0 0 99.81 34.53 |
| | | L 44.98 65.96 |
| | | A 0.30 0.30 0.0 0 0 44.98 66.48 |
| | | L 99.82 97.91 |
| | | A 0.30 0.30 0.0 0 0 100.12 97.91 |
| | | L 155.05 66.47 |
| | | Z |
| | | M 97.83 102.22 |
| | | A 0.32 0.32 0.0 0 0 97.67 101.94 |
| | | L 42.48 70.37 |
| | | A 0.32 0.32 0.0 0 0 42.00 70.65 |
| | | L 42.00 133.51 |
| | | A 0.32 0.32 0.0 0 0 42.16 133.79 |
| | | L 97.04 165.13 |
| | | A 0.32 0.32 0.0 0 0 97.52 164.85 |
| | | L 97.83 102.22 |
| | | Z |
| | | M 102.35 101.92 |
| | | A 0.32 0.32 0.0 0 0 102.19 102.20 |
| | | L 102.47 164.86 |
| | | A 0.32 0.32 0.0 0 0 102.95 165.14 |
| | | L 157.78 133.85 |
| | | A 0.32 0.32 0.0 0 0 157.94 133.57 |
| | | L 157.72 70.85 |
| | | A 0.32 0.32 0.0 0 0 157.24 70.57 |
| | | L 102.35 101.92 |
| | | Z" |
| | | /> |
| | | <path fill="#080808" d=" |
| | | M 155.05 66.47 |
| | | L 100.12 97.91 |
| | | A 0.30 0.30 0.0 0 1 99.82 97.91 |
| | | L 44.98 66.48 |
| | | A 0.30 0.30 0.0 0 1 44.98 65.96 |
| | | L 99.81 34.53 |
| | | A 0.30 0.30 0.0 0 1 100.11 34.53 |
| | | L 155.05 65.95 |
| | | A 0.30 0.30 0.0 0 1 155.05 66.47 |
| | | Z |
| | | M 101.2539 41.9921 |
| | | A 1.17 1.17 0.0 0 0 100.0859 40.8201 |
| | | L 100.0459 40.8200 |
| | | A 1.17 1.17 0.0 0 0 98.8739 41.9880 |
| | | L 98.7861 92.2679 |
| | | A 1.17 1.17 0.0 0 0 99.9541 93.4399 |
| | | L 99.9941 93.4400 |
| | | A 1.17 1.17 0.0 0 0 101.1661 92.2720 |
| | | L 101.2539 41.9921 |
| | | Z" |
| | | /> |
| | | <rect fill="#ffffff" x="-1.19" y="-26.31" transform="translate(100.02,67.13) rotate(0.1)" width="2.38" |
| | | height="52.62" rx="1.17"/> |
| | | <path fill="#9a9a9b" d=" |
| | | M 160.95 64.72 |
| | | C 164.79 67.55 166.62 71.04 166.94 76.07 |
| | | C 168.11 94.50 167.62 114.80 167.49 139.78 |
| | | A 1.36 1.36 0.0 0 1 167.13 140.70 |
| | | Q 165.51 142.50 162.95 143.97 |
| | | Q 137.94 158.32 112.94 172.72 |
| | | C 107.25 175.99 102.24 176.44 96.63 173.25 |
| | | Q 68.55 157.30 40.95 140.48 |
| | | Q 49.77 143.99 57.98 148.94 |
| | | C 70.33 156.39 81.84 162.53 93.59 169.67 |
| | | C 96.73 171.58 102.53 171.72 106.07 169.70 |
| | | Q 133.44 154.04 160.83 138.33 |
| | | Q 163.07 137.05 163.09 133.43 |
| | | Q 163.26 102.51 163.25 71.58 |
| | | C 163.25 68.73 162.26 67.10 160.95 64.72 |
| | | Z" |
| | | /> |
| | | <path fill="#080808" d=" |
| | | M 97.83 102.22 |
| | | L 97.52 164.85 |
| | | A 0.32 0.32 0.0 0 1 97.04 165.13 |
| | | L 42.16 133.79 |
| | | A 0.32 0.32 0.0 0 1 42.00 133.51 |
| | | L 42.00 70.65 |
| | | A 0.32 0.32 0.0 0 1 42.48 70.37 |
| | | L 97.67 101.94 |
| | | A 0.32 0.32 0.0 0 1 97.83 102.22 |
| | | Z |
| | | M 93.8121 104.2576 |
| | | A 1.09 1.09 0.0 0 0 92.3238 103.8561 |
| | | L 48.3895 129.1194 |
| | | A 1.09 1.09 0.0 0 0 47.9879 130.6077 |
| | | L 48.0079 130.6424 |
| | | A 1.09 1.09 0.0 0 0 49.4962 131.0439 |
| | | L 93.4305 105.7806 |
| | | A 1.09 1.09 0.0 0 0 93.8321 104.2923 |
| | | L 93.8121 104.2576 |
| | | Z" |
| | | /> |
| | | <path fill="#080808" d=" |
| | | M 102.35 101.92 |
| | | L 157.24 70.57 |
| | | A 0.32 0.32 0.0 0 1 157.72 70.85 |
| | | L 157.94 133.57 |
| | | A 0.32 0.32 0.0 0 1 157.78 133.85 |
| | | L 102.95 165.14 |
| | | A 0.32 0.32 0.0 0 1 102.47 164.86 |
| | | L 102.19 102.20 |
| | | A 0.32 0.32 0.0 0 1 102.35 101.92 |
| | | Z |
| | | M 152.0194 130.6077 |
| | | A 1.09 1.09 0.0 0 0 151.6178 129.1194 |
| | | L 107.6488 103.8361 |
| | | A 1.09 1.09 0.0 0 0 106.1606 104.2377 |
| | | L 106.1406 104.2723 |
| | | A 1.09 1.09 0.0 0 0 106.5422 105.7606 |
| | | L 150.5112 131.0439 |
| | | A 1.09 1.09 0.0 0 0 151.9994 130.6423 |
| | | L 152.0194 130.6077 |
| | | Z" |
| | | /> |
| | | <rect fill="#ffffff" x="-26.43" y="-1.11" transform="translate(70.91,117.45) rotate(-29.9)" width="52.86" |
| | | height="2.22" rx="1.09"/> |
| | | <rect fill="#ffffff" x="-26.45" y="-1.11" transform="translate(129.08,117.44) rotate(29.9)" width="52.90" |
| | | height="2.22" rx="1.09"/> |
| | | </svg> |
| New file |
| | |
| | | import { |
| | | defineConfig, |
| | | minimalPreset as preset, |
| | | } from '@vite-pwa/assets-generator/config' |
| | | |
| | | export default defineConfig({ |
| | | preset, |
| | | images: ['public/favicon.svg'], |
| | | }) |
| New file |
| | |
| | | import { usePrefersDark } from '@solid-primitives/media' |
| | | import { Navigate, Route, Routes } from '@solidjs/router' |
| | | import { Show, createEffect, lazy } from 'solid-js' |
| | | import { Toaster } from 'solid-toast' |
| | | import { twMerge } from 'tailwind-merge' |
| | | import { Header } from '~/components' |
| | | import { ROUTES } from '~/constants' |
| | | import { I18nProvider, locale } from '~/i18n' |
| | | import { |
| | | WsMsg, |
| | | autoSwitchTheme, |
| | | curTheme, |
| | | endpoint, |
| | | favDayTheme, |
| | | favNightTheme, |
| | | setCurTheme, |
| | | setLatestConnectionMsg, |
| | | useTwemoji, |
| | | useWsRequest, |
| | | } from '~/signals' |
| | | |
| | | const Setup = lazy(() => import('~/pages/Setup')) |
| | | const Overview = lazy(() => import('~/pages/Overview')) |
| | | const Connections = lazy(() => import('~/pages/Connections')) |
| | | const Logs = lazy(() => import('~/pages/Logs')) |
| | | const Proxies = lazy(() => import('~/pages/Proxies')) |
| | | const Rules = lazy(() => import('~/pages/Rules')) |
| | | const Config = lazy(() => import('~/pages/Config')) |
| | | |
| | | const ProtectedResources = () => { |
| | | const latestConnectionMsg = useWsRequest<WsMsg>('connections') |
| | | |
| | | createEffect(() => setLatestConnectionMsg(latestConnectionMsg())) |
| | | |
| | | return null |
| | | } |
| | | |
| | | export const App = () => { |
| | | const prefersDark = usePrefersDark() |
| | | |
| | | createEffect(() => { |
| | | if (autoSwitchTheme()) |
| | | setCurTheme(prefersDark() ? favNightTheme() : favDayTheme()) |
| | | }) |
| | | |
| | | return ( |
| | | <I18nProvider locale={locale()}> |
| | | <div |
| | | class={twMerge( |
| | | 'relative flex h-screen flex-col overscroll-y-none subpixel-antialiased p-safe', |
| | | useTwemoji() ? 'font-twemoji' : 'font-no-twemoji', |
| | | )} |
| | | data-theme={curTheme()} |
| | | > |
| | | <Header /> |
| | | |
| | | <div class="flex-1 overflow-y-auto p-2 sm:p-4"> |
| | | <Routes> |
| | | <Show when={endpoint()}> |
| | | <Route path={ROUTES.Overview} component={Overview} /> |
| | | <Route path={ROUTES.Proxies} component={Proxies} /> |
| | | <Route path={ROUTES.Rules} component={Rules} /> |
| | | <Route path={ROUTES.Conns} component={Connections} /> |
| | | <Route path={ROUTES.Log} component={Logs} /> |
| | | <Route path={ROUTES.Config} component={Config} /> |
| | | <Route path="*" element={<Navigate href={ROUTES.Overview} />} /> |
| | | </Show> |
| | | |
| | | <Route path={endpoint() ? ROUTES.Setup : '*'} component={Setup} /> |
| | | </Routes> |
| | | |
| | | <Show when={endpoint()}> |
| | | <ProtectedResources /> |
| | | </Show> |
| | | </div> |
| | | |
| | | <Toaster position="bottom-center" /> |
| | | </div> |
| | | </I18nProvider> |
| | | ) |
| | | } |
| New file |
| | |
| | | import ky from 'ky' |
| | | import { ResourceActions, createSignal } from 'solid-js' |
| | | import { toast } from 'solid-toast' |
| | | import { useRequest } from '~/signals' |
| | | import { |
| | | BackendVersion, |
| | | Config, |
| | | Proxy, |
| | | ProxyProvider, |
| | | Rule, |
| | | RuleProvider, |
| | | } from '~/types' |
| | | |
| | | export const closeAllConnectionsAPI = () => { |
| | | const request = useRequest() |
| | | |
| | | return request.delete('connections') |
| | | } |
| | | |
| | | export const closeSingleConnectionAPI = (id: string) => { |
| | | const request = useRequest() |
| | | |
| | | return request.delete(`connections/${id}`) |
| | | } |
| | | |
| | | export const [reloadingConfigFile, setReloadingConfigFile] = createSignal(false) |
| | | export const [updatingGEODatabases, setUpdatingGEODatabases] = |
| | | createSignal(false) |
| | | export const [flushingFakeIPData, setFlushingFakeIPData] = createSignal(false) |
| | | export const [upgradingBackend, setUpgradingBackend] = createSignal(false) |
| | | export const [restartingBackend, setRestartingBackend] = createSignal(false) |
| | | |
| | | export const reloadConfigFileAPI = async () => { |
| | | const request = useRequest() |
| | | setReloadingConfigFile(true) |
| | | try { |
| | | await request.put('configs', { |
| | | searchParams: { force: true }, |
| | | json: { path: '', payload: '' }, |
| | | }) |
| | | } catch {} |
| | | setReloadingConfigFile(false) |
| | | } |
| | | |
| | | export const flushFakeIPDataAPI = async () => { |
| | | const request = useRequest() |
| | | setFlushingFakeIPData(true) |
| | | try { |
| | | await request.post('cache/fakeip/flush') |
| | | } catch {} |
| | | setFlushingFakeIPData(false) |
| | | } |
| | | |
| | | export const updateGEODatabasesAPI = async () => { |
| | | const request = useRequest() |
| | | setUpdatingGEODatabases(true) |
| | | try { |
| | | await request.post('configs/geo') |
| | | } catch {} |
| | | setUpdatingGEODatabases(false) |
| | | } |
| | | |
| | | export const upgradeBackendAPI = async () => { |
| | | const request = useRequest() |
| | | setUpgradingBackend(true) |
| | | try { |
| | | await request.post('upgrade') |
| | | } catch {} |
| | | setUpgradingBackend(false) |
| | | } |
| | | |
| | | export const restartBackendAPI = async () => { |
| | | const request = useRequest() |
| | | setRestartingBackend(true) |
| | | try { |
| | | await request.post('restart') |
| | | } catch {} |
| | | setRestartingBackend(false) |
| | | } |
| | | |
| | | export const fetchBackendConfigAPI = () => { |
| | | const request = useRequest() |
| | | |
| | | return request.get('configs').json<Config>() |
| | | } |
| | | |
| | | export const updateBackendConfigAPI = async ( |
| | | key: keyof Config, |
| | | value: Partial<Config[keyof Config]>, |
| | | refetch: ResourceActions<Config | undefined>['refetch'], |
| | | ) => { |
| | | try { |
| | | const request = useRequest() |
| | | |
| | | await request.patch('configs', { json: { [key]: value } }).json<Config>() |
| | | |
| | | await refetch() |
| | | } catch (err) { |
| | | toast.error((err as Error).message) |
| | | } |
| | | } |
| | | |
| | | export const fetchBackendVersionAPI = async () => { |
| | | const request = useRequest() |
| | | |
| | | const { version } = await request.get('version').json<BackendVersion>() |
| | | |
| | | return version |
| | | } |
| | | |
| | | export const fetchProxyProvidersAPI = () => { |
| | | const request = useRequest() |
| | | |
| | | return request |
| | | .get('providers/proxies') |
| | | .json<{ providers: Record<string, ProxyProvider> }>() |
| | | } |
| | | |
| | | export const fetchProxiesAPI = () => { |
| | | const request = useRequest() |
| | | |
| | | return request.get('proxies').json<{ proxies: Record<string, Proxy> }>() |
| | | } |
| | | |
| | | export const updateProxyProviderAPI = (providerName: string) => { |
| | | const request = useRequest() |
| | | |
| | | return request.put(`providers/proxies/${providerName}`) |
| | | } |
| | | |
| | | export const proxyProviderHealthCheckAPI = (providerName: string) => { |
| | | const request = useRequest() |
| | | |
| | | return request |
| | | .get(`providers/proxies/${providerName}/healthcheck`, { |
| | | timeout: 5 * 1000, |
| | | }) |
| | | .json<Record<string, number>>() |
| | | } |
| | | |
| | | export const selectProxyInGroupAPI = (groupName: string, proxyName: string) => { |
| | | const request = useRequest() |
| | | |
| | | return request.put(`proxies/${groupName}`, { |
| | | body: JSON.stringify({ |
| | | name: proxyName, |
| | | }), |
| | | }) |
| | | } |
| | | |
| | | export const proxyLatencyTestAPI = ( |
| | | proxyName: string, |
| | | provider: string, |
| | | url: string, |
| | | timeout: number, |
| | | ) => { |
| | | const request = useRequest() |
| | | |
| | | if (provider !== '') { |
| | | return proxyProviderHealthCheckAPI(provider).then((latencyMap) => ({ |
| | | delay: latencyMap[proxyName], |
| | | })) |
| | | } |
| | | |
| | | return request |
| | | .get(`proxies/${proxyName}/delay`, { |
| | | searchParams: { |
| | | url, |
| | | timeout, |
| | | }, |
| | | }) |
| | | .json<{ delay: number }>() |
| | | } |
| | | |
| | | export const proxyGroupLatencyTestAPI = ( |
| | | groupName: string, |
| | | url: string, |
| | | timeout: number, |
| | | ) => { |
| | | const request = useRequest() |
| | | |
| | | return request |
| | | .get(`group/${groupName}/delay`, { |
| | | searchParams: { |
| | | url, |
| | | timeout, |
| | | }, |
| | | }) |
| | | .json<Record<string, number>>() |
| | | } |
| | | |
| | | export const fetchRulesAPI = () => { |
| | | const request = useRequest() |
| | | |
| | | return request.get('rules').json<{ rules: Record<string, Rule> }>() |
| | | } |
| | | |
| | | export const fetchRuleProvidersAPI = () => { |
| | | const request = useRequest() |
| | | |
| | | return request |
| | | .get('providers/rules') |
| | | .json<{ providers: Record<string, RuleProvider> }>() |
| | | } |
| | | |
| | | export const updateRuleProviderAPI = (providerName: string) => { |
| | | const request = useRequest() |
| | | |
| | | return request.put(`providers/rules/${providerName}`) |
| | | } |
| | | |
| | | export const isUpdateAvailableAPI = async (versionResponse: string) => { |
| | | const match = /(alpha|beta|meta)-?(\w+)/.exec(versionResponse) |
| | | |
| | | if (!match) { |
| | | return false |
| | | } |
| | | |
| | | const channel = match[1], |
| | | version = match[2] |
| | | |
| | | if (channel === 'meta') { |
| | | const { assets } = await ky |
| | | .get('https://api.github.com/repos/MetaCubeX/Clash.Meta/releases/latest') |
| | | .json<{ |
| | | assets: { |
| | | name: string |
| | | }[] |
| | | }>() |
| | | |
| | | const alreadyLatest = assets.some(({ name }) => name.includes(version)) |
| | | |
| | | return !alreadyLatest |
| | | } |
| | | |
| | | if (channel === 'alpha') { |
| | | const { assets } = await ky |
| | | .get( |
| | | 'https://api.github.com/repos/MetaCubeX/Clash.Meta/releases/tags/Prerelease-Alpha', |
| | | ) |
| | | .json<{ |
| | | assets: { |
| | | name: string |
| | | }[] |
| | | }>() |
| | | |
| | | const alreadyLatest = assets.some(({ name }) => name.includes(version)) |
| | | |
| | | return !alreadyLatest |
| | | } |
| | | |
| | | return false |
| | | } |
| New file |
| | |
| | | import { JSX, ParentComponent, Show, splitProps } from 'solid-js' |
| | | import { twMerge } from 'tailwind-merge' |
| | | |
| | | interface ButtonBaseProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> { |
| | | loading?: boolean |
| | | disabled?: boolean |
| | | } |
| | | |
| | | interface ButtonWithIconProps extends ButtonBaseProps { |
| | | icon: JSX.Element |
| | | children?: JSX.Element |
| | | } |
| | | |
| | | interface ButtonWithoutIconProps extends ButtonBaseProps { |
| | | icon?: JSX.Element |
| | | children: JSX.Element |
| | | } |
| | | |
| | | export const Button: ParentComponent< |
| | | ButtonWithIconProps | ButtonWithoutIconProps |
| | | > = (props) => { |
| | | const [local, others] = splitProps(props, ['class', 'loading', 'icon']) |
| | | |
| | | return ( |
| | | <button |
| | | class={twMerge( |
| | | 'btn flex items-center', |
| | | local.loading ? 'btn-disabled' : local.class, |
| | | )} |
| | | {...others} |
| | | > |
| | | <Show when={local.loading}> |
| | | <div class="loading loading-spinner" /> |
| | | </Show> |
| | | |
| | | <span |
| | | class="truncate rounded-none" |
| | | classList={{ |
| | | 'flex-1': !local.icon, |
| | | }} |
| | | > |
| | | {props.icon || props.children} |
| | | </span> |
| | | </button> |
| | | ) |
| | | } |
| New file |
| | |
| | | import { children, JSX, ParentComponent, Show } from 'solid-js' |
| | | import { twMerge } from 'tailwind-merge' |
| | | |
| | | type Props = { |
| | | title: JSX.Element |
| | | isOpen?: boolean |
| | | onCollapse: (collapsed: boolean) => void |
| | | } |
| | | |
| | | export const Collapse: ParentComponent<Props> = (props) => { |
| | | const { title, onCollapse } = props |
| | | |
| | | const getCollapseClassName = () => { |
| | | const openedClassName = 'collapse-open' |
| | | const closedClassName = 'collapse-close' |
| | | |
| | | return props.isOpen ? openedClassName : closedClassName |
| | | } |
| | | |
| | | const getCollapseContentClassName = () => { |
| | | const openedClassName = 'opacity-100' |
| | | const closedClassName = 'opacity-0' |
| | | |
| | | return props.isOpen ? openedClassName : closedClassName |
| | | } |
| | | |
| | | return ( |
| | | <div |
| | | class={twMerge( |
| | | getCollapseClassName(), |
| | | 'collapse collapse-arrow select-none border-secondary bg-base-200 shadow-md', |
| | | )} |
| | | > |
| | | <div |
| | | class="collapse-title pr-4 text-xl font-medium after:!top-8" |
| | | onClick={() => onCollapse(!props.isOpen)} |
| | | > |
| | | {title} |
| | | </div> |
| | | |
| | | <div |
| | | class={twMerge( |
| | | getCollapseContentClassName(), |
| | | 'collapse-content grid grid-cols-2 gap-2 transition-opacity duration-1000 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5', |
| | | )} |
| | | > |
| | | <Show when={props.isOpen}>{children(() => props.children)()}</Show> |
| | | </div> |
| | | </div> |
| | | ) |
| | | } |
| New file |
| | |
| | | import { children, ParentComponent } from 'solid-js' |
| | | |
| | | export const ConfigTitle: ParentComponent<{ withDivider?: boolean }> = ( |
| | | props, |
| | | ) => ( |
| | | <div |
| | | class="py-2 text-center text-lg font-semibold" |
| | | classList={{ |
| | | divider: props.withDivider, |
| | | }} |
| | | > |
| | | {children(() => props.children)()} |
| | | </div> |
| | | ) |
| New file |
| | |
| | | import { createForm } from '@felte/solid' |
| | | import { validator } from '@felte/validator-zod' |
| | | import { IconNetwork, IconX } from '@tabler/icons-solidjs' |
| | | import type { |
| | | DragEventHandler, |
| | | Draggable, |
| | | Droppable, |
| | | } from '@thisbeyond/solid-dnd' |
| | | import { |
| | | DragDropProvider, |
| | | DragDropSensors, |
| | | DragOverlay, |
| | | SortableProvider, |
| | | closestCenter, |
| | | createSortable, |
| | | useDragDropContext, |
| | | } from '@thisbeyond/solid-dnd' |
| | | import { uniq } from 'lodash' |
| | | import { Component, For, Index, Show, createSignal } from 'solid-js' |
| | | import { z } from 'zod' |
| | | import { Button, ConfigTitle, Modal } from '~/components' |
| | | import { |
| | | CONNECTIONS_TABLE_ACCESSOR_KEY, |
| | | CONNECTIONS_TABLE_INITIAL_COLUMN_ORDER, |
| | | CONNECTIONS_TABLE_INITIAL_COLUMN_VISIBILITY, |
| | | TAILWINDCSS_SIZE, |
| | | } from '~/constants' |
| | | import { useI18n } from '~/i18n' |
| | | import { |
| | | allConnections, |
| | | clientSourceIPTags, |
| | | connectionsTableSize, |
| | | setClientSourceIPTags, |
| | | setConnectionsTableSize, |
| | | } from '~/signals' |
| | | import { |
| | | ConnectionsTableColumnOrder, |
| | | ConnectionsTableColumnVisibility, |
| | | } from '~/types' |
| | | |
| | | const TagClientSourceIPWithNameForm: Component = () => { |
| | | const schema = z.object({ |
| | | tagName: z.string().nonempty(), |
| | | sourceIP: z.string().nonempty(), |
| | | }) |
| | | |
| | | const [t] = useI18n() |
| | | |
| | | const { form, reset } = createForm<z.infer<typeof schema>>({ |
| | | extend: validator({ schema }), |
| | | onSubmit: ({ tagName, sourceIP }) => { |
| | | setClientSourceIPTags((tags) => { |
| | | if ( |
| | | tags.some( |
| | | (tag) => tag.tagName === tagName || tag.sourceIP === sourceIP, |
| | | ) |
| | | ) { |
| | | return tags |
| | | } |
| | | |
| | | return [...tags, { tagName, sourceIP }] |
| | | }) |
| | | |
| | | reset() |
| | | }, |
| | | }) |
| | | |
| | | return ( |
| | | <form use:form={form}> |
| | | <div class="join flex"> |
| | | <select name="sourceIP" class="select join-item select-bordered"> |
| | | <option /> |
| | | |
| | | <Index |
| | | each={uniq( |
| | | allConnections().map(({ metadata: { sourceIP } }) => sourceIP), |
| | | ) |
| | | .sort() |
| | | .filter( |
| | | (sourceIP) => |
| | | !clientSourceIPTags().some( |
| | | ({ sourceIP: tagSourceIP }) => tagSourceIP === sourceIP, |
| | | ), |
| | | )} |
| | | > |
| | | {(sourceIP) => ( |
| | | <option class="badge" value={sourceIP()}> |
| | | {sourceIP()} |
| | | </option> |
| | | )} |
| | | </Index> |
| | | </select> |
| | | |
| | | <input |
| | | name="tagName" |
| | | class="input join-item input-bordered min-w-0 flex-1" |
| | | placeholder="name" |
| | | /> |
| | | |
| | | <Button type="submit" class="join-item"> |
| | | {t('tag')} |
| | | </Button> |
| | | </div> |
| | | </form> |
| | | ) |
| | | } |
| | | |
| | | export const ConnectionsSettingsModal = (props: { |
| | | ref?: (el: HTMLDialogElement) => void |
| | | order: ConnectionsTableColumnOrder |
| | | visible: ConnectionsTableColumnVisibility |
| | | onOrderChange: (value: ConnectionsTableColumnOrder) => void |
| | | onVisibleChange: (value: ConnectionsTableColumnVisibility) => void |
| | | }) => { |
| | | const [t] = useI18n() |
| | | |
| | | const [activeKey, setActiveKey] = |
| | | createSignal<CONNECTIONS_TABLE_ACCESSOR_KEY | null>(null) |
| | | |
| | | const onDragStart = ({ draggable }: { draggable: Draggable }) => |
| | | setActiveKey(draggable.id as CONNECTIONS_TABLE_ACCESSOR_KEY) |
| | | const onDragEnd = ({ |
| | | draggable, |
| | | droppable, |
| | | }: { |
| | | draggable: Draggable |
| | | droppable: Droppable |
| | | }) => { |
| | | if (draggable && droppable) { |
| | | const currentItems = props.order |
| | | const fromIndex = currentItems.indexOf( |
| | | draggable.id as CONNECTIONS_TABLE_ACCESSOR_KEY, |
| | | ) |
| | | const toIndex = currentItems.indexOf( |
| | | droppable.id as CONNECTIONS_TABLE_ACCESSOR_KEY, |
| | | ) |
| | | |
| | | if (fromIndex !== toIndex) { |
| | | const updatedItems = currentItems.slice() |
| | | |
| | | updatedItems.splice(toIndex, 0, ...updatedItems.splice(fromIndex, 1)) |
| | | props.onOrderChange(updatedItems) |
| | | } |
| | | } |
| | | } |
| | | |
| | | const FormRow: Component<{ |
| | | key: CONNECTIONS_TABLE_ACCESSOR_KEY |
| | | }> = ({ key }) => { |
| | | const sortable = createSortable(key) |
| | | const [state] = useDragDropContext()! |
| | | |
| | | return ( |
| | | <div |
| | | use:sortable |
| | | class="sortable" |
| | | classList={{ |
| | | 'opacity-25': sortable.isActiveDraggable, |
| | | 'transition-transform': !!state.active.draggable, |
| | | }} |
| | | > |
| | | <div class="flex cursor-grab justify-between py-2"> |
| | | <span class="select-none">{t(key)}</span> |
| | | |
| | | <input |
| | | type="checkbox" |
| | | class="toggle" |
| | | checked={props.visible[key]} |
| | | onChange={(e) => { |
| | | props.onVisibleChange({ |
| | | ...props.visible, |
| | | [key]: e.target.checked, |
| | | }) |
| | | }} |
| | | /> |
| | | </div> |
| | | </div> |
| | | ) |
| | | } |
| | | |
| | | return ( |
| | | <Modal |
| | | ref={(el) => props.ref?.(el)} |
| | | icon={<IconNetwork size={24} />} |
| | | title={t('connectionsSettings')} |
| | | action={ |
| | | <Button |
| | | class="btn-neutral btn-sm" |
| | | onClick={() => { |
| | | props.onOrderChange(CONNECTIONS_TABLE_INITIAL_COLUMN_ORDER) |
| | | props.onVisibleChange(CONNECTIONS_TABLE_INITIAL_COLUMN_VISIBILITY) |
| | | }} |
| | | > |
| | | {t('reset')} |
| | | </Button> |
| | | } |
| | | > |
| | | <div class="flex flex-col gap-4"> |
| | | <div> |
| | | <ConfigTitle withDivider>{t('tableSize')}</ConfigTitle> |
| | | |
| | | <select |
| | | class="select select-bordered w-full" |
| | | value={connectionsTableSize()} |
| | | onChange={(e) => |
| | | setConnectionsTableSize(e.target.value as TAILWINDCSS_SIZE) |
| | | } |
| | | > |
| | | <For each={Object.values(TAILWINDCSS_SIZE)}> |
| | | {(value) => <option value={value}>{t(value)}</option>} |
| | | </For> |
| | | </select> |
| | | </div> |
| | | |
| | | <div> |
| | | <ConfigTitle withDivider> |
| | | {t('tagClientSourceIPWithName')} |
| | | </ConfigTitle> |
| | | |
| | | <div class="flex flex-col gap-4"> |
| | | <TagClientSourceIPWithNameForm /> |
| | | |
| | | <div class="flex flex-col gap-2"> |
| | | <For each={clientSourceIPTags()}> |
| | | {({ tagName, sourceIP }) => ( |
| | | <div class="badge badge-primary w-full items-center justify-between gap-2 py-4"> |
| | | <span class="truncate"> |
| | | {tagName} ({sourceIP}) |
| | | </span> |
| | | |
| | | <Button |
| | | class="btn-circle btn-ghost btn-xs" |
| | | onClick={() => |
| | | setClientSourceIPTags((tags) => |
| | | tags.filter((tag) => tag.tagName !== tagName), |
| | | ) |
| | | } |
| | | icon={<IconX size={12} />} |
| | | /> |
| | | </div> |
| | | )} |
| | | </For> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div> |
| | | <ConfigTitle withDivider>{t('sort')}</ConfigTitle> |
| | | |
| | | <DragDropProvider |
| | | onDragStart={onDragStart} |
| | | onDragEnd={onDragEnd as DragEventHandler} |
| | | collisionDetector={closestCenter} |
| | | > |
| | | <DragDropSensors /> |
| | | |
| | | <SortableProvider ids={props.order}> |
| | | <For each={props.order}>{(key) => <FormRow key={key} />}</For> |
| | | </SortableProvider> |
| | | |
| | | <DragOverlay> |
| | | <Show when={activeKey()}> |
| | | <div>{t(activeKey()!)}</div> |
| | | </Show> |
| | | </DragOverlay> |
| | | </DragDropProvider> |
| | | </div> |
| | | </div> |
| | | </Modal> |
| | | ) |
| | | } |
| New file |
| | |
| | | import { IconNetwork } from '@tabler/icons-solidjs' |
| | | import { Component, Show } from 'solid-js' |
| | | import { Modal } from '~/components' |
| | | import { useI18n } from '~/i18n' |
| | | import { allConnections } from '~/signals' |
| | | |
| | | export const ConnectionsTableDetailsModal: Component<{ |
| | | ref?: (el: HTMLDialogElement) => void |
| | | selectedConnectionID?: string |
| | | }> = (props) => { |
| | | const [t] = useI18n() |
| | | |
| | | return ( |
| | | <Modal |
| | | ref={(el) => props.ref?.(el)} |
| | | icon={<IconNetwork size={24} />} |
| | | title={t('connectionsDetails')} |
| | | > |
| | | <Show when={props.selectedConnectionID}> |
| | | <pre> |
| | | <code> |
| | | {JSON.stringify( |
| | | allConnections().find( |
| | | ({ id }) => id === props.selectedConnectionID, |
| | | ), |
| | | null, |
| | | 2, |
| | | )} |
| | | </code> |
| | | </pre> |
| | | </Show> |
| | | </Modal> |
| | | ) |
| | | } |
| New file |
| | |
| | | import { A, useLocation } from '@solidjs/router' |
| | | import { |
| | | IconFileStack, |
| | | IconGlobe, |
| | | IconHome, |
| | | IconMenu, |
| | | IconNetwork, |
| | | IconPalette, |
| | | IconRuler, |
| | | IconSettings, |
| | | } from '@tabler/icons-solidjs' |
| | | import { For, ParentComponent, Show, createSignal } from 'solid-js' |
| | | import { LogoText } from '~/components' |
| | | import { ROUTES, themes } from '~/constants' |
| | | import { useI18n } from '~/i18n' |
| | | import { setCurTheme } from '~/signals' |
| | | |
| | | const Nav: ParentComponent<{ href: string; tooltip: string }> = ({ |
| | | href, |
| | | tooltip, |
| | | children, |
| | | }) => ( |
| | | <li class="tooltip tooltip-bottom" data-tip={tooltip}> |
| | | <A class="rounded-box" href={href}> |
| | | {children} |
| | | </A> |
| | | </li> |
| | | ) |
| | | |
| | | const ThemeSwitcher = () => ( |
| | | <div class="drawer drawer-end w-auto sm:ml-auto"> |
| | | <input id="themes" type="checkbox" class="drawer-toggle" /> |
| | | |
| | | <div class="drawer-content flex items-center"> |
| | | <label |
| | | for="themes" |
| | | class="btn btn-circle btn-primary drawer-button btn-sm" |
| | | > |
| | | <IconPalette /> |
| | | </label> |
| | | </div> |
| | | |
| | | <div class="drawer-side"> |
| | | <label for="themes" class="drawer-overlay" /> |
| | | |
| | | <ul class="menu rounded-l-box gap-2 bg-base-300 p-2"> |
| | | <For each={themes}> |
| | | {(theme) => ( |
| | | <li |
| | | data-theme={theme} |
| | | class="btn btn-xs" |
| | | onClick={() => setCurTheme(theme)} |
| | | > |
| | | {theme} |
| | | </li> |
| | | )} |
| | | </For> |
| | | </ul> |
| | | </div> |
| | | </div> |
| | | ) |
| | | |
| | | export const Header = () => { |
| | | const [t] = useI18n() |
| | | const navs = () => [ |
| | | { |
| | | href: ROUTES.Overview, |
| | | name: t('overview'), |
| | | icon: <IconHome />, |
| | | }, |
| | | { |
| | | href: ROUTES.Proxies, |
| | | name: t('proxies'), |
| | | icon: <IconGlobe />, |
| | | }, |
| | | { |
| | | href: ROUTES.Rules, |
| | | name: t('rules'), |
| | | icon: <IconRuler />, |
| | | }, |
| | | { |
| | | href: ROUTES.Conns, |
| | | name: t('connections'), |
| | | icon: <IconNetwork />, |
| | | }, |
| | | { |
| | | href: ROUTES.Log, |
| | | name: t('logs'), |
| | | icon: <IconFileStack />, |
| | | }, |
| | | { |
| | | href: ROUTES.Config, |
| | | name: t('config'), |
| | | icon: <IconSettings />, |
| | | }, |
| | | ] |
| | | |
| | | const location = useLocation() |
| | | |
| | | const [openedDrawer, setOpenedDrawer] = createSignal(false) |
| | | |
| | | return ( |
| | | <ul class="navbar sticky inset-x-0 top-0 z-50 flex w-auto items-center justify-center bg-base-300 px-4 shadow-lg"> |
| | | <div class="navbar-start gap-4"> |
| | | <div class="drawer w-auto lg:hidden"> |
| | | <input |
| | | id="navs" |
| | | type="checkbox" |
| | | class="drawer-toggle" |
| | | onChange={(e) => setOpenedDrawer(e.target.checked)} |
| | | checked={openedDrawer()} |
| | | /> |
| | | |
| | | <div class="drawer-content flex items-center"> |
| | | <label for="navs" class="btn btn-circle drawer-button btn-sm"> |
| | | <IconMenu /> |
| | | </label> |
| | | </div> |
| | | |
| | | <div class="drawer-side"> |
| | | <label for="navs" class="drawer-overlay" /> |
| | | |
| | | <ul class="menu rounded-r-box min-h-full w-2/5 gap-2 bg-base-300 pt-20"> |
| | | <For each={navs()}> |
| | | {({ href, name }) => ( |
| | | <li onClick={() => setOpenedDrawer(false)}> |
| | | <A href={href}>{name}</A> |
| | | </li> |
| | | )} |
| | | </For> |
| | | </ul> |
| | | </div> |
| | | </div> |
| | | |
| | | <LogoText /> |
| | | </div> |
| | | |
| | | <Show when={location.pathname !== ROUTES.Setup}> |
| | | <div class="navbar-center hidden lg:flex"> |
| | | <ul class="menu menu-horizontal menu-lg gap-2 p-0"> |
| | | <For each={navs()}> |
| | | {({ href, name, icon }) => ( |
| | | <Nav href={href} tooltip={name}> |
| | | {icon} |
| | | </Nav> |
| | | )} |
| | | </For> |
| | | </ul> |
| | | </div> |
| | | </Show> |
| | | |
| | | <div class="navbar-end"> |
| | | <div class="flex items-center gap-2"> |
| | | <ThemeSwitcher /> |
| | | </div> |
| | | </div> |
| | | </ul> |
| | | ) |
| | | } |
| New file |
| | |
| | | import { Show, createEffect, createMemo, createSignal } from 'solid-js' |
| | | import { LATENCY_QUALITY_MAP_HTTP } from '~/constants' |
| | | import { useI18n } from '~/i18n' |
| | | import { latencyQualityMap, useProxies } from '~/signals' |
| | | |
| | | export const Latency = (props: { name?: string }) => { |
| | | const [t] = useI18n() |
| | | const { latencyMap } = useProxies() |
| | | const [textClassName, setTextClassName] = createSignal('') |
| | | const latency = createMemo(() => { |
| | | return latencyMap()[props.name!] |
| | | }) |
| | | |
| | | createEffect(() => { |
| | | setTextClassName('text-success') |
| | | |
| | | if (latency() > latencyQualityMap().HIGH) { |
| | | setTextClassName('text-error') |
| | | } else if (latency() > latencyQualityMap().MEDIUM) { |
| | | setTextClassName('text-warning') |
| | | } |
| | | }) |
| | | |
| | | return ( |
| | | <Show |
| | | when={ |
| | | typeof latency() === 'number' && |
| | | latency() !== LATENCY_QUALITY_MAP_HTTP.NOT_CONNECTED |
| | | } |
| | | > |
| | | <span class={`whitespace-nowrap text-xs ${textClassName()}`}> |
| | | {latency()} |
| | | {t('ms')} |
| | | </span> |
| | | </Show> |
| | | ) |
| | | } |
| New file |
| | |
| | | import { A } from '@solidjs/router' |
| | | import { endpoint } from '~/signals' |
| | | |
| | | export const LogoText = () => ( |
| | | <div class="text-md flex items-center gap-1 whitespace-nowrap font-bold uppercase sm:text-xl"> |
| | | <A |
| | | class="bg-gradient-to-br from-primary to-secondary bg-clip-text text-transparent" |
| | | href={endpoint() ? '/' : '/setup'} |
| | | > |
| | | metacube |
| | | </A> |
| | | <span>(</span> |
| | | <a |
| | | class="text-primary transition-transform hover:rotate-90 hover:scale-125" |
| | | href="https://github.com/metacubex/metacubexd" |
| | | target="_blank" |
| | | > |
| | | xd |
| | | </a> |
| | | <span>)</span> |
| | | </div> |
| | | ) |
| New file |
| | |
| | | import { IconFileStack } from '@tabler/icons-solidjs' |
| | | import { Component, For } from 'solid-js' |
| | | import { ConfigTitle, Modal } from '~/components' |
| | | import { |
| | | LOGS_TABLE_MAX_ROWS_LIST, |
| | | LOG_LEVEL, |
| | | TAILWINDCSS_SIZE, |
| | | } from '~/constants' |
| | | import { useI18n } from '~/i18n' |
| | | import { |
| | | logLevel, |
| | | logMaxRows, |
| | | logsTableSize, |
| | | setLogLevel, |
| | | setLogMaxRows, |
| | | setLogsTableSize, |
| | | } from '~/signals' |
| | | |
| | | export const LogsSettingsModal: Component<{ |
| | | ref?: (el: HTMLDialogElement) => void |
| | | }> = (props) => { |
| | | const [t] = useI18n() |
| | | |
| | | return ( |
| | | <Modal |
| | | ref={(el) => props.ref?.(el)} |
| | | icon={<IconFileStack size={24} />} |
| | | title={t('logsSettings')} |
| | | > |
| | | <div class="flex flex-col gap-4"> |
| | | <div> |
| | | <ConfigTitle withDivider>{t('tableSize')}</ConfigTitle> |
| | | |
| | | <select |
| | | class="select select-bordered w-full" |
| | | value={logsTableSize()} |
| | | onChange={(e) => |
| | | setLogsTableSize(e.target.value as TAILWINDCSS_SIZE) |
| | | } |
| | | > |
| | | <For each={Object.values(TAILWINDCSS_SIZE)}> |
| | | {(value) => <option value={value}>{t(value)}</option>} |
| | | </For> |
| | | </select> |
| | | </div> |
| | | |
| | | <div> |
| | | <ConfigTitle withDivider>{t('logLevel')}</ConfigTitle> |
| | | |
| | | <select |
| | | class="select select-bordered w-full" |
| | | value={logLevel()} |
| | | onChange={(e) => setLogLevel(e.target.value as LOG_LEVEL)} |
| | | > |
| | | <For |
| | | each={[ |
| | | LOG_LEVEL.Info, |
| | | LOG_LEVEL.Error, |
| | | LOG_LEVEL.Warning, |
| | | LOG_LEVEL.Debug, |
| | | LOG_LEVEL.Silent, |
| | | ]} |
| | | > |
| | | {(level) => <option value={level}>{t(level)}</option>} |
| | | </For> |
| | | </select> |
| | | </div> |
| | | |
| | | <div> |
| | | <ConfigTitle withDivider>{t('logMaxRows')}</ConfigTitle> |
| | | |
| | | <select |
| | | class="select select-bordered w-full" |
| | | value={logMaxRows()} |
| | | onChange={(e) => setLogMaxRows(parseInt(e.target.value))} |
| | | > |
| | | <For each={LOGS_TABLE_MAX_ROWS_LIST}> |
| | | {(rows) => <option value={rows}>{rows}</option>} |
| | | </For> |
| | | </select> |
| | | </div> |
| | | </div> |
| | | </Modal> |
| | | ) |
| | | } |
| New file |
| | |
| | | import { IconX } from '@tabler/icons-solidjs' |
| | | import { JSX, ParentComponent, Show, children } from 'solid-js' |
| | | import { twMerge } from 'tailwind-merge' |
| | | import { Button } from '~/components' |
| | | |
| | | type Props = { |
| | | ref?: (el: HTMLDialogElement) => void |
| | | icon?: JSX.Element |
| | | title?: JSX.Element |
| | | action?: JSX.Element |
| | | } |
| | | |
| | | const actionClassName = |
| | | 'sticky bottom-0 z-50 flex items-center justify-end bg-base-100 bg-opacity-80 p-4 backdrop-blur' |
| | | |
| | | export const Modal: ParentComponent<Props> = (props) => { |
| | | let dialogRef: HTMLDialogElement | undefined |
| | | |
| | | return ( |
| | | <dialog |
| | | ref={(el) => (dialogRef = el) && props.ref?.(el)} |
| | | class="modal modal-bottom sm:modal-middle" |
| | | > |
| | | <div class="modal-box p-0" onContextMenu={(e) => e.preventDefault()}> |
| | | <div class={twMerge(actionClassName, 'top-0 justify-between')}> |
| | | <div class="flex items-center gap-4 text-xl font-bold"> |
| | | {props.icon} |
| | | |
| | | <span>{props.title}</span> |
| | | </div> |
| | | |
| | | <Button |
| | | class="btn-circle btn-sm" |
| | | onClick={() => dialogRef?.close()} |
| | | icon={<IconX size={20} />} |
| | | /> |
| | | </div> |
| | | |
| | | <div class="p-4">{children(() => props.children)()}</div> |
| | | |
| | | <Show when={props.action}> |
| | | <div class={actionClassName}> |
| | | <div class="flex justify-end gap-2">{props.action}</div> |
| | | </div> |
| | | </Show> |
| | | </div> |
| | | |
| | | <form method="dialog" class="modal-backdrop"> |
| | | <button /> |
| | | </form> |
| | | </dialog> |
| | | ) |
| | | } |
| New file |
| | |
| | | import { IconGlobe } from '@tabler/icons-solidjs' |
| | | import { Component, For } from 'solid-js' |
| | | import { ConfigTitle, Modal } from '~/components' |
| | | import { PROXIES_ORDERING_TYPE, PROXIES_PREVIEW_TYPE } from '~/constants' |
| | | import { useI18n } from '~/i18n' |
| | | import { |
| | | autoCloseConns, |
| | | hideUnAvailableProxies, |
| | | latencyTestTimeoutDuration, |
| | | proxiesOrderingType, |
| | | proxiesPreviewType, |
| | | setAutoCloseConns, |
| | | setHideUnAvailableProxies, |
| | | setLatencyTestTimeoutDuration, |
| | | setProxiesOrderingType, |
| | | setProxiesPreviewType, |
| | | setUrlForLatencyTest, |
| | | urlForLatencyTest, |
| | | } from '~/signals' |
| | | |
| | | export const ProxiesSettingsModal: Component<{ |
| | | ref?: (el: HTMLDialogElement) => void |
| | | }> = (props) => { |
| | | const [t] = useI18n() |
| | | |
| | | return ( |
| | | <Modal |
| | | ref={(el) => props.ref?.(el)} |
| | | icon={<IconGlobe size={24} />} |
| | | title={t('proxiesSettings')} |
| | | > |
| | | <div class="flex flex-col gap-4"> |
| | | <div> |
| | | <ConfigTitle withDivider>{t('autoCloseConns')}</ConfigTitle> |
| | | |
| | | <div class="flex w-full justify-center"> |
| | | <input |
| | | class="toggle" |
| | | type="checkbox" |
| | | checked={autoCloseConns()} |
| | | onChange={(e) => setAutoCloseConns(e.target.checked)} |
| | | /> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="flex flex-col"> |
| | | <ConfigTitle withDivider>{t('urlForLatencyTest')}</ConfigTitle> |
| | | |
| | | <input |
| | | class="input input-bordered w-full" |
| | | value={urlForLatencyTest()} |
| | | onChange={(e) => setUrlForLatencyTest(e.target.value)} |
| | | /> |
| | | </div> |
| | | |
| | | <div> |
| | | <ConfigTitle withDivider> |
| | | {t('latencyTestTimeoutDuration')} ({t('ms')}) |
| | | </ConfigTitle> |
| | | |
| | | <input |
| | | type="number" |
| | | class="input input-bordered w-full" |
| | | value={latencyTestTimeoutDuration()} |
| | | onChange={(e) => |
| | | setLatencyTestTimeoutDuration(Number(e.target.value)) |
| | | } |
| | | /> |
| | | </div> |
| | | |
| | | <div> |
| | | <ConfigTitle withDivider>{t('proxiesSorting')}</ConfigTitle> |
| | | |
| | | <select |
| | | class="select select-bordered w-full" |
| | | value={proxiesOrderingType()} |
| | | onChange={(e) => |
| | | setProxiesOrderingType(e.target.value as PROXIES_ORDERING_TYPE) |
| | | } |
| | | > |
| | | <For each={Object.values(PROXIES_ORDERING_TYPE)}> |
| | | {(value) => ( |
| | | <option class="flex items-center gap-2" value={value}> |
| | | {t(value)} |
| | | </option> |
| | | )} |
| | | </For> |
| | | </select> |
| | | </div> |
| | | |
| | | <div> |
| | | <ConfigTitle withDivider>{t('hideUnAvailableProxies')}</ConfigTitle> |
| | | |
| | | <div class="flex w-full justify-center"> |
| | | <input |
| | | class="toggle" |
| | | type="checkbox" |
| | | checked={hideUnAvailableProxies()} |
| | | onChange={(e) => setHideUnAvailableProxies(e.target.checked)} |
| | | /> |
| | | </div> |
| | | </div> |
| | | |
| | | <div> |
| | | <ConfigTitle withDivider>{t('proxiesPreviewType')}</ConfigTitle> |
| | | |
| | | <select |
| | | class="select select-bordered w-full" |
| | | value={proxiesPreviewType()} |
| | | onChange={(e) => |
| | | setProxiesPreviewType(e.target.value as PROXIES_PREVIEW_TYPE) |
| | | } |
| | | > |
| | | <For each={Object.values(PROXIES_PREVIEW_TYPE)}> |
| | | {(value) => <option value={value}>{t(value)}</option>} |
| | | </For> |
| | | </select> |
| | | </div> |
| | | </div> |
| | | </Modal> |
| | | ) |
| | | } |
| New file |
| | |
| | | import { IconBrandSpeedtest } from '@tabler/icons-solidjs' |
| | | import { createMemo, Show } from 'solid-js' |
| | | import { twMerge } from 'tailwind-merge' |
| | | import { Button, Latency } from '~/components' |
| | | import { filterSpecialProxyType, formatProxyType } from '~/helpers' |
| | | import { useProxies } from '~/signals' |
| | | |
| | | export const ProxyNodeCard = (props: { |
| | | proxyName: string |
| | | isSelected?: boolean |
| | | onClick?: () => void |
| | | }) => { |
| | | const { proxyLatencyTest, proxyLatencyTestingMap } = useProxies() |
| | | const { proxyName, isSelected, onClick } = props |
| | | const { proxyNodeMap } = useProxies() |
| | | const proxyNode = createMemo(() => proxyNodeMap()[proxyName]) |
| | | const specialType = () => |
| | | filterSpecialProxyType(proxyNode()?.type) |
| | | ? proxyNode()?.xudp |
| | | ? 'xudp' |
| | | : proxyNode()?.udp |
| | | ? 'udp' |
| | | : null |
| | | : null |
| | | |
| | | return ( |
| | | <div |
| | | class={twMerge( |
| | | 'card card-bordered tooltip-bottom flex flex-col gap-1 border-neutral-focus bg-neutral p-2 text-neutral-content shadow-lg', |
| | | isSelected && 'border-primary bg-primary-content text-primary', |
| | | onClick && 'cursor-pointer', |
| | | )} |
| | | onClick={onClick} |
| | | title={proxyName} |
| | | > |
| | | <div class="flex items-center justify-between gap-2"> |
| | | <span class="truncate text-left text-sm">{proxyName}</span> |
| | | |
| | | <Button |
| | | class="btn-circle btn-ghost h-auto min-h-0 w-auto" |
| | | icon={ |
| | | <IconBrandSpeedtest |
| | | size={20} |
| | | class={twMerge( |
| | | proxyLatencyTestingMap()[proxyName] && |
| | | 'animate-pulse text-success', |
| | | )} |
| | | /> |
| | | } |
| | | onClick={(e) => { |
| | | e.stopPropagation() |
| | | |
| | | void proxyLatencyTest(proxyName, proxyNode().provider) |
| | | }} |
| | | /> |
| | | </div> |
| | | |
| | | <div class="flex items-center justify-between gap-1"> |
| | | <div |
| | | class={twMerge( |
| | | 'truncate text-xs text-slate-500', |
| | | isSelected && 'text-primary', |
| | | )} |
| | | > |
| | | {formatProxyType(proxyNode()?.type)} |
| | | |
| | | <Show when={specialType()}>{` :: ${specialType()}`}</Show> |
| | | </div> |
| | | |
| | | <div class="text-xs"> |
| | | <Latency name={props.proxyName} /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | ) |
| | | } |
| New file |
| | |
| | | import { createMemo, Match, Show, Switch } from 'solid-js' |
| | | import { ProxyPreviewBar, ProxyPreviewDots } from '~/components' |
| | | import { PROXIES_PREVIEW_TYPE } from '~/constants' |
| | | import { proxiesPreviewType } from '~/signals' |
| | | |
| | | export const ProxyNodePreview = (props: { |
| | | proxyNameList: string[] |
| | | now?: string |
| | | }) => { |
| | | const off = () => proxiesPreviewType() === PROXIES_PREVIEW_TYPE.OFF |
| | | |
| | | const isSmallGroup = createMemo(() => props.proxyNameList.length <= 10) |
| | | |
| | | const isShowBar = createMemo(() => { |
| | | const type = proxiesPreviewType() |
| | | |
| | | return ( |
| | | type === PROXIES_PREVIEW_TYPE.BAR || |
| | | (type === PROXIES_PREVIEW_TYPE.Auto && !isSmallGroup()) |
| | | ) |
| | | }) |
| | | |
| | | const isShowDots = createMemo(() => { |
| | | const type = proxiesPreviewType() |
| | | |
| | | return ( |
| | | type === PROXIES_PREVIEW_TYPE.DOTS || |
| | | (type === PROXIES_PREVIEW_TYPE.Auto && isSmallGroup()) |
| | | ) |
| | | }) |
| | | |
| | | return ( |
| | | <Show when={!off()}> |
| | | <Switch> |
| | | <Match when={isShowBar()}> |
| | | <ProxyPreviewBar |
| | | proxyNameList={props.proxyNameList} |
| | | now={props.now} |
| | | /> |
| | | </Match> |
| | | |
| | | <Match when={isShowDots()}> |
| | | <ProxyPreviewDots |
| | | proxyNameList={props.proxyNameList} |
| | | now={props.now} |
| | | /> |
| | | </Match> |
| | | </Switch> |
| | | </Show> |
| | | ) |
| | | } |
| New file |
| | |
| | | import { createMemo } from 'solid-js' |
| | | import { Latency } from '~/components' |
| | | import { latencyQualityMap, useProxies } from '~/signals' |
| | | |
| | | export const ProxyPreviewBar = (props: { |
| | | proxyNameList: string[] |
| | | now?: string |
| | | }) => { |
| | | const { latencyMap } = useProxies() |
| | | const latencyList = createMemo(() => |
| | | props.proxyNameList.map((name) => latencyMap()[name]), |
| | | ) |
| | | |
| | | const all = createMemo(() => latencyList().length) |
| | | const good = createMemo( |
| | | () => |
| | | latencyList().filter( |
| | | (latency) => |
| | | latency > latencyQualityMap().NOT_CONNECTED && |
| | | latency <= latencyQualityMap().MEDIUM, |
| | | ).length, |
| | | ) |
| | | const middle = createMemo( |
| | | () => |
| | | latencyList().filter( |
| | | (latency) => |
| | | latency > latencyQualityMap().MEDIUM && |
| | | latency <= latencyQualityMap().HIGH, |
| | | ).length, |
| | | ) |
| | | const slow = createMemo( |
| | | () => |
| | | latencyList().filter((latency) => latency > latencyQualityMap().HIGH) |
| | | .length, |
| | | ) |
| | | const notConnected = createMemo( |
| | | () => |
| | | latencyList().filter( |
| | | (latency) => latency === latencyQualityMap().NOT_CONNECTED, |
| | | ).length, |
| | | ) |
| | | |
| | | return ( |
| | | <div class="flex items-center gap-2 py-2"> |
| | | <div class="flex flex-1 overflow-hidden rounded-2xl"> |
| | | <div |
| | | class="h-2 bg-success" |
| | | style={{ |
| | | width: `${(good() * 100) / all()}%`, // cant use tw class, otherwise dynamic classname won't be generated |
| | | }} |
| | | /> |
| | | <div |
| | | class="h-2 bg-warning" |
| | | style={{ |
| | | width: `${(middle() * 100) / all()}%`, |
| | | }} |
| | | /> |
| | | <div |
| | | class="h-2 bg-error" |
| | | style={{ |
| | | width: `${(slow() * 100) / all()}%`, |
| | | }} |
| | | /> |
| | | <div |
| | | class="h-2 bg-neutral" |
| | | style={{ |
| | | width: `${(notConnected() * 100) / all()}%`, |
| | | }} |
| | | /> |
| | | </div> |
| | | |
| | | <Latency name={props.now} /> |
| | | </div> |
| | | ) |
| | | } |
| New file |
| | |
| | | import { For } from 'solid-js' |
| | | import { twMerge } from 'tailwind-merge' |
| | | import { Latency } from '~/components' |
| | | import { latencyQualityMap, useProxies } from '~/signals' |
| | | |
| | | const LatencyDots = (props: { |
| | | name: string |
| | | latency?: number |
| | | selected: boolean |
| | | }) => { |
| | | let dotClassName = props.selected |
| | | ? 'bg-white border-4 border-success' |
| | | : 'bg-success' |
| | | |
| | | if ( |
| | | typeof props.latency !== 'number' || |
| | | props.latency === latencyQualityMap().NOT_CONNECTED |
| | | ) { |
| | | dotClassName = props.selected |
| | | ? 'bg-white border-4 border-neutral' |
| | | : 'bg-neutral' |
| | | } else if (props.latency > latencyQualityMap().HIGH) { |
| | | dotClassName = props.selected |
| | | ? 'bg-white border-4 border-error' |
| | | : 'bg-error' |
| | | } else if (props.latency > latencyQualityMap().MEDIUM) { |
| | | dotClassName = props.selected |
| | | ? 'bg-white border-4 border-warning' |
| | | : 'bg-warning' |
| | | } |
| | | |
| | | return ( |
| | | <div |
| | | class={twMerge('h-4 w-4 rounded-full', dotClassName)} |
| | | title={props.name} |
| | | /> |
| | | ) |
| | | } |
| | | |
| | | export const ProxyPreviewDots = (props: { |
| | | proxyNameList: string[] |
| | | now?: string |
| | | }) => { |
| | | const { latencyMap } = useProxies() |
| | | |
| | | return ( |
| | | <div class="flex items-center gap-2 py-2"> |
| | | <div class="flex flex-1 flex-wrap items-center gap-1"> |
| | | <For |
| | | each={props.proxyNameList.map((name): [string, number] => [ |
| | | name, |
| | | latencyMap()[name], |
| | | ])} |
| | | > |
| | | {([name, latency]) => { |
| | | const isSelected = props.now === name |
| | | |
| | | return ( |
| | | <LatencyDots |
| | | name={name} |
| | | latency={latency} |
| | | selected={isSelected} |
| | | /> |
| | | ) |
| | | }} |
| | | </For> |
| | | </div> |
| | | |
| | | <Latency name={props.now} /> |
| | | </div> |
| | | ) |
| | | } |
| New file |
| | |
| | | import { createWindowSize } from '@solid-primitives/resize-observer' |
| | | import { ParentComponent, Show, children, createMemo } from 'solid-js' |
| | | |
| | | export const RenderInTwoColumns: ParentComponent = (props) => { |
| | | const resolvedChildren = children(() => props.children) |
| | | const windowSize = createWindowSize() |
| | | // 640 is sm size in default tailwindcss breakpoint |
| | | const showTwoColumns = createMemo(() => windowSize.width >= 640) |
| | | |
| | | const leftColumns = createMemo(() => |
| | | resolvedChildren.toArray().filter((_, index) => index % 2 === 0), |
| | | ) |
| | | |
| | | const rightColumns = createMemo(() => |
| | | resolvedChildren.toArray().filter((_, index) => index % 2 === 1), |
| | | ) |
| | | |
| | | return ( |
| | | <div class="flex flex-col gap-2 sm:flex-row"> |
| | | <Show when={showTwoColumns()} fallback={props.children}> |
| | | <div class="flex flex-1 flex-col gap-2">{leftColumns()}</div> |
| | | <div class="flex flex-1 flex-col gap-2">{rightColumns()}</div> |
| | | </Show> |
| | | </div> |
| | | ) |
| | | } |
| New file |
| | |
| | | import byteSize from 'byte-size' |
| | | import dayjs from 'dayjs' |
| | | import { toFinite } from 'lodash' |
| | | import { useI18n } from '~/i18n' |
| | | import type { SubscriptionInfo as ISubscriptionInfo } from '~/types' |
| | | |
| | | const getSubscriptionsInfo = (subscriptionInfo: ISubscriptionInfo) => { |
| | | const { Download = 0, Upload = 0, Total = 0, Expire = 0 } = subscriptionInfo |
| | | |
| | | const total = byteSize(Total, { units: 'iec' }) |
| | | const used = byteSize(Download + Upload, { |
| | | units: 'iec', |
| | | }) |
| | | const percentage = toFinite((((Download + Upload) / Total) * 100).toFixed(2)) |
| | | |
| | | const expirePrefix = () => { |
| | | const [t] = useI18n() |
| | | |
| | | return t('expire') |
| | | } |
| | | |
| | | const expireStr = () => { |
| | | const [t] = useI18n() |
| | | |
| | | if (Expire === 0) { |
| | | return t('noExpire') |
| | | } |
| | | |
| | | return dayjs(Expire * 1000).format('YYYY-MM-DD') |
| | | } |
| | | |
| | | return { |
| | | total, |
| | | used, |
| | | percentage, |
| | | expirePrefix, |
| | | expireStr, |
| | | } |
| | | } |
| | | |
| | | export const SubscriptionInfo = (props: { |
| | | subscriptionInfo?: ISubscriptionInfo |
| | | }) => { |
| | | if (!props.subscriptionInfo) { |
| | | return |
| | | } |
| | | |
| | | const info = getSubscriptionsInfo(props.subscriptionInfo) |
| | | |
| | | return ( |
| | | <> |
| | | <progress class="progress" value={info.percentage} max="100" /> |
| | | |
| | | <div class="text-sm text-slate-500"> |
| | | {`${info.used}`} / {`${info.total}`} ( {info.percentage}% ) |
| | | </div> |
| | | |
| | | <div class="text-sm text-slate-500"> |
| | | {info.expirePrefix()}: {info.expireStr()} |
| | | </div> |
| | | </> |
| | | ) |
| | | } |
| New file |
| | |
| | | export * from './Button' |
| | | export * from './Collapse' |
| | | export * from './ConfigTitle' |
| | | export * from './ConnectionsSettingsModal' |
| | | export * from './ConnectionsTableDetailsModal' |
| | | export * from './Header' |
| | | export * from './Latency' |
| | | export * from './LogoText' |
| | | export * from './LogsSettingsModal' |
| | | export * from './Modal' |
| | | export * from './ProxiesSettingsModal' |
| | | export * from './ProxyNodeCard' |
| | | export * from './ProxyNodePreview' |
| | | export * from './ProxyPreviewBar' |
| | | export * from './ProxyPreviewDots' |
| | | export * from './RenderInTwoColumns' |
| | | export * from './SubscriptionInfo' |
| New file |
| | |
| | | import { ApexOptions } from 'apexcharts' |
| | | import byteSize from 'byte-size' |
| | | |
| | | export const themes = [ |
| | | 'light', |
| | | 'dark', |
| | | 'cupcake', |
| | | 'bumblebee', |
| | | 'emerald', |
| | | 'corporate', |
| | | 'synthwave', |
| | | 'retro', |
| | | 'cyberpunk', |
| | | 'valentine', |
| | | 'halloween', |
| | | 'garden', |
| | | 'forest', |
| | | 'aqua', |
| | | 'lofi', |
| | | 'pastel', |
| | | 'fantasy', |
| | | 'wireframe', |
| | | 'black', |
| | | 'luxury', |
| | | 'dracula', |
| | | 'cmyk', |
| | | 'autumn', |
| | | 'business', |
| | | 'acid', |
| | | 'lemonade', |
| | | 'night', |
| | | 'coffee', |
| | | 'winter', |
| | | ] as const |
| | | |
| | | export enum ROUTES { |
| | | Overview = '/overview', |
| | | Proxies = '/proxies', |
| | | Rules = '/rules', |
| | | Conns = '/conns', |
| | | Log = '/logs', |
| | | Config = '/config', |
| | | Setup = '/setup', |
| | | } |
| | | |
| | | export const CHART_MAX_XAXIS = 10 |
| | | |
| | | export const DEFAULT_CHART_OPTIONS: ApexOptions = { |
| | | title: { align: 'center', style: { color: 'gray', fontSize: '16px' } }, |
| | | chart: { |
| | | toolbar: { show: false }, |
| | | zoom: { enabled: false }, |
| | | animations: { easing: 'linear' }, |
| | | }, |
| | | noData: { text: 'Loading...' }, |
| | | legend: { |
| | | showForSingleSeries: true, |
| | | fontSize: '16px', |
| | | labels: { colors: 'gray' }, |
| | | itemMargin: { horizontal: 32 }, |
| | | }, |
| | | dataLabels: { enabled: false }, |
| | | grid: { yaxis: { lines: { show: false } } }, |
| | | stroke: { curve: 'smooth' }, |
| | | tooltip: { enabled: false }, |
| | | xaxis: { |
| | | range: CHART_MAX_XAXIS, |
| | | labels: { show: false }, |
| | | axisTicks: { show: false }, |
| | | }, |
| | | yaxis: { |
| | | labels: { |
| | | style: { colors: 'gray', fontSize: '13px' }, |
| | | formatter: (val) => byteSize(val).toString(), |
| | | }, |
| | | }, |
| | | } |
| | | |
| | | export enum LATENCY_QUALITY_MAP_HTTP { |
| | | NOT_CONNECTED = -1, |
| | | MEDIUM = 200, |
| | | HIGH = 500, |
| | | } |
| | | |
| | | export enum LATENCY_QUALITY_MAP_HTTPS { |
| | | NOT_CONNECTED = -1, |
| | | MEDIUM = 800, |
| | | HIGH = 1500, |
| | | } |
| | | |
| | | export enum PROXIES_PREVIEW_TYPE { |
| | | OFF = 'off', |
| | | DOTS = 'dots', |
| | | BAR = 'bar', |
| | | Auto = 'auto', |
| | | } |
| | | |
| | | export enum PROXIES_ORDERING_TYPE { |
| | | NATURAL = 'orderNatural', |
| | | LATENCY_ASC = 'orderLatency_asc', |
| | | LATENCY_DESC = 'orderLatency_desc', |
| | | NAME_ASC = 'orderName_asc', |
| | | NAME_DESC = 'orderName_desc', |
| | | } |
| | | |
| | | export enum LANG { |
| | | EN = 'en-US', |
| | | ZH = 'zh-CN', |
| | | } |
| | | |
| | | export enum CONNECTIONS_TABLE_ACCESSOR_KEY { |
| | | Details = 'details', |
| | | Close = 'close', |
| | | ID = 'ID', |
| | | Type = 'type', |
| | | Process = 'process', |
| | | Host = 'host', |
| | | SniffHost = 'sniffHost', |
| | | Rule = 'rules', |
| | | Chains = 'chains', |
| | | DlSpeed = 'dlSpeed', |
| | | ULSpeed = 'ulSpeed', |
| | | Download = 'dl', |
| | | Upload = 'ul', |
| | | ConnectTime = 'connectTime', |
| | | SourceIP = 'sourceIP', |
| | | SourcePort = 'sourcePort', |
| | | Destination = 'destination', |
| | | } |
| | | |
| | | export const CONNECTIONS_TABLE_MAX_CLOSED_ROWS = 200 |
| | | |
| | | export const CONNECTIONS_TABLE_INITIAL_COLUMN_ORDER = Object.values( |
| | | CONNECTIONS_TABLE_ACCESSOR_KEY, |
| | | ) |
| | | export const CONNECTIONS_TABLE_INITIAL_COLUMN_VISIBILITY = { |
| | | ...Object.fromEntries( |
| | | CONNECTIONS_TABLE_INITIAL_COLUMN_ORDER.map((i) => [i, true]), |
| | | ), |
| | | [CONNECTIONS_TABLE_ACCESSOR_KEY.ID]: false, |
| | | } |
| | | |
| | | export enum TAILWINDCSS_SIZE { |
| | | XS = 'xs', |
| | | SM = 'sm', |
| | | MD = 'md', |
| | | LG = 'lg', |
| | | } |
| | | |
| | | export enum MODE_OPTIONS { |
| | | Global = 'global', |
| | | Rule = 'rule', |
| | | Direct = 'direct', |
| | | } |
| | | |
| | | export enum LOG_LEVEL { |
| | | Info = 'info', |
| | | Error = 'error', |
| | | Warning = 'warning', |
| | | Debug = 'debug', |
| | | Silent = 'silent', |
| | | } |
| | | |
| | | export const LOGS_TABLE_MAX_ROWS_LIST = [200, 300, 500, 800, 1000] |
| | | export const DEFAULT_LOGS_TABLE_MAX_ROWS = LOGS_TABLE_MAX_ROWS_LIST[0] |
| New file |
| | |
| | | import { createSignal } from 'solid-js' |
| | | |
| | | export const transformEndpointURL = (url: string) => |
| | | /^https?/.test(url) ? url : `${window.location.protocol}//${url}` |
| | | |
| | | export const useStringBooleanMap = () => { |
| | | const [map, setMap] = createSignal<Record<string, boolean>>({}) |
| | | const set = (name: string, value: boolean) => { |
| | | setMap({ |
| | | ...map(), |
| | | [name]: value, |
| | | }) |
| | | } |
| | | |
| | | const setWithCallback = async ( |
| | | name: string, |
| | | callback: () => Promise<void>, |
| | | ) => { |
| | | set(name, true) |
| | | try { |
| | | await callback() |
| | | } catch {} |
| | | set(name, false) |
| | | } |
| | | |
| | | return { |
| | | map, |
| | | set, |
| | | setWithCallback, |
| | | } |
| | | } |
| New file |
| | |
| | | export * from './global' |
| | | export * from './proxies' |
| New file |
| | |
| | | import { PROXIES_ORDERING_TYPE } from '~/constants' |
| | | import { latencyQualityMap } from '~/signals' |
| | | |
| | | export const formatProxyType = (type = '') => { |
| | | const t = type.toLowerCase() |
| | | |
| | | if (t.includes('shadowsocks')) { |
| | | return t.replace('shadowsocks', 'ss') // for both ss and ssr |
| | | } |
| | | |
| | | if (t === 'hysteria') { |
| | | return 'hy' |
| | | } |
| | | |
| | | if (t === 'wireguard') { |
| | | return 'wg' |
| | | } |
| | | |
| | | return t |
| | | } |
| | | |
| | | export const filterSpecialProxyType = (type = '') => { |
| | | const t = type.toLowerCase() |
| | | const conditions = [ |
| | | 'selector', |
| | | 'direct', |
| | | 'reject', |
| | | 'urltest', |
| | | 'loadbalance', |
| | | 'fallback', |
| | | 'relay', |
| | | ] |
| | | |
| | | return !conditions.includes(t) |
| | | } |
| | | |
| | | export const sortProxiesByOrderingType = ( |
| | | proxyNames: string[], |
| | | proxyLatencyMap: Record<string, number>, |
| | | orderingType: PROXIES_ORDERING_TYPE, |
| | | ) => { |
| | | if (orderingType === PROXIES_ORDERING_TYPE.NATURAL) { |
| | | return proxyNames |
| | | } |
| | | |
| | | return proxyNames.sort((a, b) => { |
| | | const prevLatency = proxyLatencyMap[a] |
| | | const nextLatency = proxyLatencyMap[b] |
| | | |
| | | switch (orderingType) { |
| | | case PROXIES_ORDERING_TYPE.LATENCY_ASC: |
| | | if (prevLatency === latencyQualityMap().NOT_CONNECTED) return 1 |
| | | |
| | | if (nextLatency === latencyQualityMap().NOT_CONNECTED) return -1 |
| | | |
| | | return prevLatency - nextLatency |
| | | |
| | | case PROXIES_ORDERING_TYPE.LATENCY_DESC: |
| | | if (prevLatency === latencyQualityMap().NOT_CONNECTED) return 1 |
| | | |
| | | if (nextLatency === latencyQualityMap().NOT_CONNECTED) return -1 |
| | | |
| | | return nextLatency - prevLatency |
| | | |
| | | case PROXIES_ORDERING_TYPE.NAME_ASC: |
| | | return a.localeCompare(b) |
| | | |
| | | case PROXIES_ORDERING_TYPE.NAME_DESC: |
| | | return b.localeCompare(a) |
| | | |
| | | default: |
| | | return 0 |
| | | } |
| | | }) |
| | | } |
| | | |
| | | export const filterProxiesByAvailability = ( |
| | | proxyNames: string[], |
| | | proxyLatencyMap: Record<string, number>, |
| | | enabled?: boolean, |
| | | ) => |
| | | enabled |
| | | ? proxyNames.filter( |
| | | (name) => proxyLatencyMap[name] !== latencyQualityMap().NOT_CONNECTED, |
| | | ) |
| | | : proxyNames |
| New file |
| | |
| | | import { Flatten } from '@solid-primitives/i18n' |
| | | import { LANG } from '~/constants' |
| | | import en from './en' |
| | | import zh from './zh' |
| | | |
| | | export type Dict = Flatten<typeof en> |
| | | |
| | | export default { |
| | | [LANG.EN]: en, |
| | | [LANG.ZH]: zh, |
| | | } |
| New file |
| | |
| | | export default { |
| | | add: 'Add', |
| | | overview: 'Overview', |
| | | proxies: 'Proxies', |
| | | proxiesSettings: 'Proxies Settings', |
| | | rules: 'Rules', |
| | | connections: 'Connections', |
| | | connectionsSettings: 'Connections Settings', |
| | | connectionsDetails: 'Connections Details', |
| | | logs: 'Logs', |
| | | logsSettings: 'Logs Settings', |
| | | config: 'Config', |
| | | upload: 'Upload', |
| | | download: 'Download', |
| | | uploadTotal: 'Upload Total', |
| | | downloadTotal: 'Download Total', |
| | | activeConnections: 'Active Connections', |
| | | memoryUsage: 'Memory Usage', |
| | | traffic: 'Traffic', |
| | | memory: 'Memory', |
| | | down: 'Down', |
| | | up: 'Up', |
| | | proxyProviders: 'Proxy Providers', |
| | | ruleProviders: 'Rule Providers', |
| | | search: 'Search', |
| | | ID: 'ID', |
| | | type: 'Type', |
| | | name: 'Name', |
| | | process: 'Process', |
| | | host: 'Host', |
| | | sniffHost: 'Sniff Host', |
| | | chains: 'Chains', |
| | | connectTime: 'Time', |
| | | dlSpeed: 'DL Speed', |
| | | ulSpeed: 'UL Speed', |
| | | dl: 'DL', |
| | | ul: 'UL', |
| | | sourceIP: 'Source IP', |
| | | sourcePort: 'Source Port', |
| | | destination: 'Destination', |
| | | close: 'Close', |
| | | reset: 'Reset', |
| | | dnsQuery: 'DNS Query', |
| | | dots: 'Dots', |
| | | bar: 'Bar', |
| | | auto: 'Auto', |
| | | off: 'Off', |
| | | proxiesPreviewType: 'Proxies preview type', |
| | | urlForLatencyTest: 'URL for latency test', |
| | | autoCloseConns: 'Automatically Close connections', |
| | | useTwemoji: 'Use Twemoji Mozilla Font', |
| | | autoSwitchTheme: 'Automatically switch theme', |
| | | favDayTheme: 'Favorite light theme', |
| | | favNightTheme: 'Favorite dark theme', |
| | | renderInTwoColumns: 'Render in two columns', |
| | | updateGEODatabases: 'Update GEO Databases', |
| | | restartCore: 'Restart Core', |
| | | upgradeCore: 'Upgrade Core', |
| | | proxiesSorting: 'Proxies Sorting', |
| | | orderNatural: 'Original order in config file', |
| | | orderLatency_asc: 'By latency from low to high', |
| | | orderLatency_desc: 'By latency from high to low', |
| | | orderName_asc: 'By name alphabetically (A-Z)', |
| | | orderName_desc: 'By name alphabetically (Z-A)', |
| | | ms: 'ms', |
| | | updated: 'Updated', |
| | | tableSize: 'Table size', |
| | | logLevel: 'Log Level', |
| | | info: 'info', |
| | | silent: 'silent', |
| | | debug: 'debug', |
| | | warning: 'warning', |
| | | error: 'error', |
| | | logMaxRows: 'Log Maximum Reserved Rows', |
| | | xs: 'Extra small size', |
| | | sm: 'Small size', |
| | | md: 'Normal size', |
| | | lg: 'Large size', |
| | | switchEndpoint: 'Switch Endpoint', |
| | | switchLanguage: 'Switch Language', |
| | | latencyTestTimeoutDuration: 'Latency Test Timeout Duration', |
| | | all: 'All', |
| | | sequence: 'Sequence', |
| | | payload: 'Payload', |
| | | details: 'Details', |
| | | endpointURL: 'Endpoint URL', |
| | | secret: 'Secret', |
| | | global: 'Global', |
| | | rule: 'Rule', |
| | | direct: 'Direct', |
| | | active: 'Active', |
| | | closed: 'Closed', |
| | | sort: 'Sort', |
| | | hideUnAvailableProxies: 'Hide UnAvailable Proxies', |
| | | reloadConfig: 'Reload Config', |
| | | flushFakeIP: 'Flush Fake-IP', |
| | | tagClientSourceIPWithName: 'Tag Client Source IP With Name', |
| | | tag: 'Tag', |
| | | coreConfig: 'Core Config', |
| | | xdConfig: 'XD Config', |
| | | version: 'Version', |
| | | expire: 'Expire', |
| | | noExpire: 'Null', |
| | | enableTunDevice: 'Enable TUN Device', |
| | | tunModeStack: 'TUN Mode Stack', |
| | | tunDeviceName: 'TUN Device Name', |
| | | interfaceName: 'Interface Name', |
| | | en: 'English', |
| | | zh: 'Chinese', |
| | | port: '{{ name }} Port', |
| | | } |
| New file |
| | |
| | | import { createContextProvider } from '@solid-primitives/context' |
| | | import * as i18n from '@solid-primitives/i18n' |
| | | import { makePersisted } from '@solid-primitives/storage' |
| | | import { createSignal } from 'solid-js' |
| | | import { LANG } from '~/constants' |
| | | import dict, { Dict } from './dict' |
| | | |
| | | export const [locale, setLocale] = makePersisted( |
| | | createSignal<LANG>( |
| | | Reflect.has(dict, navigator.language) |
| | | ? (navigator.language as LANG) |
| | | : LANG.EN, |
| | | ), |
| | | { |
| | | name: 'lang', |
| | | storage: localStorage, |
| | | }, |
| | | ) |
| | | |
| | | export const [I18nProvider, useMaybeI18n] = createContextProvider< |
| | | [i18n.Translator<Dict>], |
| | | { locale: LANG } |
| | | >((props) => [ |
| | | i18n.translator(() => i18n.flatten(dict[props.locale]), i18n.resolveTemplate), |
| | | ]) |
| | | |
| | | export const useI18n = () => useMaybeI18n()! |
| New file |
| | |
| | | import { Dict } from '~/i18n/dict' |
| | | |
| | | export default { |
| | | add: '添加', |
| | | overview: '概览', |
| | | proxies: '代理', |
| | | proxiesSettings: '代理设置', |
| | | rules: '规则', |
| | | connections: '连接', |
| | | connectionsSettings: '连接设置', |
| | | connectionsDetails: '连接详情', |
| | | logs: '日志', |
| | | logsSettings: '日志设置', |
| | | config: '配置', |
| | | upload: '上传', |
| | | download: '下载', |
| | | uploadTotal: '上传总量', |
| | | downloadTotal: '下载总量', |
| | | activeConnections: '活动连接', |
| | | memoryUsage: '内存使用情况', |
| | | traffic: '流量', |
| | | memory: '内存', |
| | | down: '下载', |
| | | up: '上传', |
| | | proxyProviders: '代理提供者', |
| | | ruleProviders: '规则提供者', |
| | | search: '搜索', |
| | | ID: 'ID', |
| | | type: '类型', |
| | | name: '名字', |
| | | process: '进程', |
| | | host: '主机', |
| | | sniffHost: '嗅探域名', |
| | | chains: '链路', |
| | | connectTime: '连接时间', |
| | | dlSpeed: '下载速度', |
| | | ulSpeed: '上传速度', |
| | | dl: '下载量', |
| | | ul: '上传量', |
| | | sourceIP: '源地址', |
| | | sourcePort: '源端口', |
| | | destination: '目标地址', |
| | | close: '关闭', |
| | | reset: '重置', |
| | | dnsQuery: 'DNS 查询', |
| | | dots: '点阵', |
| | | bar: '条形', |
| | | auto: '自适应', |
| | | off: '关闭', |
| | | proxiesPreviewType: '节点组预览样式', |
| | | urlForLatencyTest: '测速链接', |
| | | autoCloseConns: '自动断开连接', |
| | | useTwemoji: '使用 Twemoji Mozilla 字体', |
| | | autoSwitchTheme: '自动切换主题', |
| | | favDayTheme: '浅色主题偏好', |
| | | favNightTheme: '深色主题偏好', |
| | | renderInTwoColumns: '双列渲染', |
| | | updateGEODatabases: '更新 GEO 数据库', |
| | | restartCore: '重启核心', |
| | | upgradeCore: '更新核心', |
| | | proxiesSorting: '节点排序', |
| | | orderNatural: '原配置文件中的排序', |
| | | orderLatency_asc: '按延迟从低到高', |
| | | orderLatency_desc: '按延迟从高到低', |
| | | orderName_asc: '按名称字母排序 (A-Z)', |
| | | orderName_desc: '按名称字母排序 (Z-A)', |
| | | ms: '毫秒', |
| | | updated: '更新于', |
| | | tableSize: '表格大小', |
| | | logLevel: '日志等级', |
| | | info: '信息', |
| | | silent: '静默', |
| | | debug: '调试', |
| | | warning: '警告', |
| | | error: '错误', |
| | | logMaxRows: '日志最大保留行数', |
| | | xs: '超小尺寸', |
| | | sm: '小尺寸', |
| | | md: '正常尺寸', |
| | | lg: '超大尺寸', |
| | | switchEndpoint: '切换后端', |
| | | switchLanguage: '切换语言', |
| | | latencyTestTimeoutDuration: '测速超时时间', |
| | | all: '全部', |
| | | sequence: '序列号', |
| | | payload: '内容', |
| | | details: '详情', |
| | | endpointURL: '后端地址', |
| | | secret: '密钥', |
| | | global: '全局', |
| | | rule: '规则', |
| | | direct: '直连', |
| | | active: '活动', |
| | | closed: '已关闭', |
| | | sort: '排序', |
| | | hideUnAvailableProxies: '隐藏不可用节点', |
| | | reloadConfig: '重载配置', |
| | | flushFakeIP: '清空 Fake-IP', |
| | | tagClientSourceIPWithName: '为客户端源 IP 地址添加名称标记', |
| | | tag: '标记', |
| | | coreConfig: '核心配置', |
| | | xdConfig: 'XD 配置', |
| | | version: '版本', |
| | | expire: '到期时间', |
| | | noExpire: '不限时', |
| | | enableTunDevice: '开启 TUN 转发', |
| | | tunModeStack: 'TUN 模式堆栈', |
| | | tunDeviceName: 'TUN 设备名称', |
| | | interfaceName: '接口名称', |
| | | en: '英文', |
| | | zh: '中文', |
| | | port: '{{ name }} 端口', |
| | | } satisfies Dict |
| New file |
| | |
| | | @import '@fontsource/fira-sans'; |
| | | |
| | | @tailwind base; |
| | | @tailwind components; |
| | | @tailwind utilities; |
| | | @tailwind variants; |
| | | |
| | | @font-face { |
| | | font-family: 'Twemoji Mozilla'; |
| | | src: url('assets/Twemoji.Mozilla.ttf') format('truetype'); |
| | | } |
| | | |
| | | :root { |
| | | scrollbar-width: thin; |
| | | scrollbar-color: transparent transparent; |
| | | } |
| | | |
| | | ::-webkit-scrollbar { |
| | | background: transparent; |
| | | display: none; |
| | | } |
| | | |
| | | ::-webkit-scrollbar:vertical { |
| | | width: 6px; |
| | | } |
| | | |
| | | ::-webkit-scrollbar:horizontal { |
| | | height: 6px; |
| | | } |
| | | |
| | | @media (width >= 640px) { |
| | | :root { |
| | | scrollbar-color: hsl(var(--p)) transparent; |
| | | } |
| | | |
| | | ::-webkit-scrollbar { |
| | | display: block; |
| | | } |
| | | |
| | | ::-webkit-scrollbar-thumb { |
| | | @apply rounded-box bg-primary; |
| | | } |
| | | } |
| New file |
| | |
| | | /* @refresh reload */ |
| | | import '~/index.css' |
| | | |
| | | import { Router, hashIntegration } from '@solidjs/router' |
| | | import dayjs from 'dayjs' |
| | | import 'dayjs/locale/zh-cn' |
| | | import relativeTime from 'dayjs/plugin/relativeTime' |
| | | import { render } from 'solid-js/web' |
| | | import { App } from '~/App' |
| | | |
| | | dayjs.extend(relativeTime) |
| | | |
| | | render( |
| | | () => ( |
| | | <Router source={hashIntegration()}> |
| | | <App /> |
| | | </Router> |
| | | ), |
| | | document.getElementById('root')!, |
| | | ) |
| New file |
| | |
| | | import { createForm } from '@felte/solid' |
| | | import { validator } from '@felte/validator-zod' |
| | | import { useNavigate } from '@solidjs/router' |
| | | import { |
| | | For, |
| | | Show, |
| | | createEffect, |
| | | createResource, |
| | | createSignal, |
| | | onMount, |
| | | } from 'solid-js' |
| | | import { toast } from 'solid-toast' |
| | | import { z } from 'zod' |
| | | import { |
| | | fetchBackendConfigAPI, |
| | | fetchBackendVersionAPI, |
| | | flushFakeIPDataAPI, |
| | | flushingFakeIPData, |
| | | isUpdateAvailableAPI, |
| | | reloadConfigFileAPI, |
| | | reloadingConfigFile, |
| | | restartBackendAPI, |
| | | restartingBackend, |
| | | updateBackendConfigAPI, |
| | | updateGEODatabasesAPI, |
| | | updatingGEODatabases, |
| | | upgradeBackendAPI, |
| | | upgradingBackend, |
| | | } from '~/apis' |
| | | import { Button, ConfigTitle } from '~/components' |
| | | import { LANG, MODE_OPTIONS, ROUTES, themes } from '~/constants' |
| | | import { locale, setLocale, useI18n } from '~/i18n' |
| | | import { |
| | | autoSwitchTheme, |
| | | endpoint, |
| | | favDayTheme, |
| | | favNightTheme, |
| | | setAutoSwitchTheme, |
| | | setFavDayTheme, |
| | | setFavNightTheme, |
| | | setSelectedEndpoint, |
| | | setUseTwemoji, |
| | | useRequest, |
| | | useTwemoji, |
| | | } from '~/signals' |
| | | import type { DNSQuery } from '~/types' |
| | | |
| | | const dnsQueryFormSchema = z.object({ |
| | | name: z.string(), |
| | | type: z.string(), |
| | | }) |
| | | |
| | | const DNSQueryForm = () => { |
| | | const [t] = useI18n() |
| | | const request = useRequest() |
| | | |
| | | const { form, isSubmitting } = createForm<z.infer<typeof dnsQueryFormSchema>>( |
| | | { |
| | | extend: validator({ schema: dnsQueryFormSchema }), |
| | | onSubmit: (values) => |
| | | request |
| | | .get('dns/query', { |
| | | searchParams: { name: values.name, type: values.type }, |
| | | }) |
| | | .json<DNSQuery>() |
| | | .then(({ Answer }) => |
| | | setDNSQueryResult(Answer?.map(({ data }) => data) || []), |
| | | ) |
| | | .catch((err) => toast.error(err.message)), |
| | | }, |
| | | ) |
| | | |
| | | const [DNSQueryResult, setDNSQueryResult] = createSignal<string[]>([]) |
| | | |
| | | return ( |
| | | <div class="flex flex-col"> |
| | | <form use:form={form} class="flex gap-2 sm:flex-row"> |
| | | <input |
| | | type="search" |
| | | name="name" |
| | | class="input input-bordered min-w-0 flex-1" |
| | | placeholder="google.com" |
| | | /> |
| | | |
| | | <div class="flex items-center gap-2"> |
| | | <select name="type" class="select select-bordered"> |
| | | <option>A</option> |
| | | <option>AAAA</option> |
| | | <option>MX</option> |
| | | </select> |
| | | |
| | | <Button type="submit" class="btn-primary" loading={isSubmitting()}> |
| | | {t('dnsQuery')} |
| | | </Button> |
| | | </div> |
| | | </form> |
| | | |
| | | <Show when={DNSQueryResult().length > 0}> |
| | | <div class="flex flex-col p-4"> |
| | | <For each={DNSQueryResult()}> |
| | | {(item) => <div class="py-2">{item}</div>} |
| | | </For> |
| | | </div> |
| | | </Show> |
| | | </div> |
| | | ) |
| | | } |
| | | |
| | | const configFormSchema = z.object({ |
| | | port: z.number(), |
| | | 'socks-port': z.number(), |
| | | 'redir-port': z.number(), |
| | | 'tproxy-port': z.number(), |
| | | 'mixed-port': z.number(), |
| | | }) |
| | | |
| | | const ConfigForm = () => { |
| | | const [t] = useI18n() |
| | | const navigate = useNavigate() |
| | | |
| | | const portList = [ |
| | | { |
| | | label: () => t('port', { name: 'HTTP' }), |
| | | key: 'port', |
| | | onChange: (e: Event & { target: HTMLInputElement }) => |
| | | void updateBackendConfigAPI('port', Number(e.target.value), refetch), |
| | | }, |
| | | { |
| | | label: () => t('port', { name: 'Socks' }), |
| | | key: 'socks-port', |
| | | onChange: (e: Event & { target: HTMLInputElement }) => |
| | | void updateBackendConfigAPI( |
| | | 'socks-port', |
| | | Number(e.target.value), |
| | | refetch, |
| | | ), |
| | | }, |
| | | { |
| | | label: () => t('port', { name: 'Redir' }), |
| | | key: 'redir-port', |
| | | onChange: (e: Event & { target: HTMLInputElement }) => |
| | | void updateBackendConfigAPI( |
| | | 'redir-port', |
| | | Number(e.target.value), |
| | | refetch, |
| | | ), |
| | | }, |
| | | { |
| | | label: () => t('port', { name: 'TProxy' }), |
| | | key: 'tproxy-port', |
| | | onChange: (e: Event & { target: HTMLInputElement }) => |
| | | void updateBackendConfigAPI( |
| | | 'tproxy-port', |
| | | Number(e.target.value), |
| | | refetch, |
| | | ), |
| | | }, |
| | | { |
| | | label: () => t('port', { name: 'Mixed' }), |
| | | key: 'mixed-port', |
| | | onChange: (e: Event & { target: HTMLInputElement }) => |
| | | void updateBackendConfigAPI( |
| | | 'mixed-port', |
| | | Number(e.target.value), |
| | | refetch, |
| | | ), |
| | | }, |
| | | ] |
| | | |
| | | const { form, setInitialValues, reset } = createForm< |
| | | z.infer<typeof configFormSchema> |
| | | >({ extend: validator({ schema: configFormSchema }) }) |
| | | |
| | | const [configsData, { refetch }] = createResource(fetchBackendConfigAPI) |
| | | |
| | | createEffect(() => { |
| | | const configs = configsData() |
| | | |
| | | if (configs) { |
| | | setInitialValues(configs) |
| | | reset() |
| | | } |
| | | }) |
| | | |
| | | return ( |
| | | <div class="flex flex-col gap-4"> |
| | | <select |
| | | class="select select-bordered" |
| | | value={configsData()?.mode} |
| | | onChange={(e) => |
| | | void updateBackendConfigAPI('mode', e.target.value, refetch) |
| | | } |
| | | > |
| | | <option value={MODE_OPTIONS.Global}>{t('global')}</option> |
| | | <option value={MODE_OPTIONS.Rule}>{t('rule')}</option> |
| | | <option value={MODE_OPTIONS.Direct}>{t('direct')}</option> |
| | | </select> |
| | | |
| | | <form class="grid grid-cols-3 gap-2 sm:grid-cols-5" use:form={form}> |
| | | <For each={portList}> |
| | | {(item) => ( |
| | | <div class="form-control"> |
| | | <label for={item.key} class="label"> |
| | | <span class="label-text">{item.label()}</span> |
| | | </label> |
| | | |
| | | <input |
| | | id={item.key} |
| | | name={item.key} |
| | | type="number" |
| | | class="input input-bordered" |
| | | placeholder={item.label()} |
| | | onChange={item.onChange} |
| | | /> |
| | | </div> |
| | | )} |
| | | </For> |
| | | </form> |
| | | |
| | | <div class="grid grid-cols-2 gap-2 sm:grid-cols-4"> |
| | | <div class="form-control"> |
| | | <label for="enable-tun-device" class="label gap-2"> |
| | | <span class="label-text">{t('enableTunDevice')}</span> |
| | | </label> |
| | | |
| | | <input |
| | | id="enable-tun-device" |
| | | type="checkbox" |
| | | class="toggle" |
| | | checked={configsData()?.tun.enable} |
| | | onChange={(e) => |
| | | void updateBackendConfigAPI( |
| | | 'tun', |
| | | { enable: e.target.checked }, |
| | | refetch, |
| | | ) |
| | | } |
| | | /> |
| | | </div> |
| | | |
| | | <div class="form-control"> |
| | | <label for="tun-ip-stack" class="label gap-2"> |
| | | <span class="label-text">{t('tunModeStack')}</span> |
| | | </label> |
| | | |
| | | <select |
| | | id="tun-ip-stack" |
| | | class="select select-bordered flex-1" |
| | | value={configsData()?.tun.stack} |
| | | onChange={(e) => |
| | | void updateBackendConfigAPI( |
| | | 'tun', |
| | | { stack: e.target.value }, |
| | | refetch, |
| | | ) |
| | | } |
| | | > |
| | | <option>gVisor</option> |
| | | <option>System</option> |
| | | <option>LWIP</option> |
| | | </select> |
| | | </div> |
| | | |
| | | <div class="form-control"> |
| | | <label for="device-name" class="label gap-2"> |
| | | <span class="label-text">{t('tunDeviceName')}</span> |
| | | </label> |
| | | |
| | | <input |
| | | id="device-name" |
| | | class="input input-bordered min-w-0" |
| | | value={configsData()?.tun.device} |
| | | onChange={(e) => |
| | | void updateBackendConfigAPI( |
| | | 'tun', |
| | | { device: e.target.value }, |
| | | refetch, |
| | | ) |
| | | } |
| | | /> |
| | | </div> |
| | | |
| | | <div class="form-control"> |
| | | <label for="interface-name" class="label gap-2"> |
| | | <span class="label-text">{t('interfaceName')}</span> |
| | | </label> |
| | | |
| | | <input |
| | | id="interface-name" |
| | | class="input input-bordered min-w-0" |
| | | value={configsData()?.['interface-name']} |
| | | onChange={(e) => |
| | | void updateBackendConfigAPI( |
| | | 'interface-name', |
| | | e.target.value, |
| | | refetch, |
| | | ) |
| | | } |
| | | /> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="grid grid-cols-2 gap-2 sm:grid-cols-3"> |
| | | <Button |
| | | class="btn-primary" |
| | | loading={reloadingConfigFile()} |
| | | onClick={reloadConfigFileAPI} |
| | | > |
| | | {t('reloadConfig')} |
| | | </Button> |
| | | |
| | | <Button |
| | | class="btn-secondary" |
| | | loading={updatingGEODatabases()} |
| | | onClick={updateGEODatabasesAPI} |
| | | > |
| | | {t('updateGEODatabases')} |
| | | </Button> |
| | | |
| | | <Button |
| | | class="btn-accent" |
| | | loading={flushingFakeIPData()} |
| | | onClick={flushFakeIPDataAPI} |
| | | > |
| | | {t('flushFakeIP')} |
| | | </Button> |
| | | |
| | | <Button |
| | | class="btn-error" |
| | | loading={upgradingBackend()} |
| | | onClick={upgradeBackendAPI} |
| | | > |
| | | {t('upgradeCore')} |
| | | </Button> |
| | | |
| | | <Button |
| | | class="btn-warning" |
| | | loading={restartingBackend()} |
| | | onClick={restartBackendAPI} |
| | | > |
| | | {t('restartCore')} |
| | | </Button> |
| | | |
| | | <Button |
| | | class="btn-info" |
| | | onClick={() => { |
| | | setSelectedEndpoint('') |
| | | navigate(ROUTES.Setup) |
| | | }} |
| | | > |
| | | {t('switchEndpoint')} |
| | | </Button> |
| | | </div> |
| | | </div> |
| | | ) |
| | | } |
| | | |
| | | const ConfigForXd = () => { |
| | | const [t] = useI18n() |
| | | |
| | | const languages = [ |
| | | { |
| | | label: () => t('en'), |
| | | value: LANG.EN, |
| | | }, |
| | | { |
| | | label: () => t('zh'), |
| | | value: LANG.ZH, |
| | | }, |
| | | ] |
| | | |
| | | return ( |
| | | <div class="grid grid-cols-1 gap-4 sm:grid-cols-2"> |
| | | <div class="flex flex-col gap-2"> |
| | | <div class="flex flex-col items-center"> |
| | | <ConfigTitle>{t('autoSwitchTheme')}</ConfigTitle> |
| | | |
| | | <input |
| | | type="checkbox" |
| | | class="toggle" |
| | | checked={autoSwitchTheme()} |
| | | onChange={(e) => setAutoSwitchTheme(e.target.checked)} |
| | | /> |
| | | </div> |
| | | |
| | | <Show when={autoSwitchTheme()}> |
| | | <div class="flex flex-col gap-2"> |
| | | <div class="flex flex-col"> |
| | | <ConfigTitle>{t('favDayTheme')}</ConfigTitle> |
| | | |
| | | <select |
| | | class="select select-bordered" |
| | | value={favDayTheme()} |
| | | onChange={(e) => |
| | | setFavDayTheme(e.target.value as (typeof themes)[number]) |
| | | } |
| | | > |
| | | <For each={themes}> |
| | | {(theme) => <option value={theme}>{theme}</option>} |
| | | </For> |
| | | </select> |
| | | </div> |
| | | |
| | | <div class="flex flex-col"> |
| | | <ConfigTitle>{t('favNightTheme')}</ConfigTitle> |
| | | |
| | | <select |
| | | class="select select-bordered" |
| | | value={favNightTheme()} |
| | | onChange={(e) => |
| | | setFavNightTheme(e.target.value as (typeof themes)[number]) |
| | | } |
| | | > |
| | | <For each={themes}> |
| | | {(theme) => <option value={theme}>{theme}</option>} |
| | | </For> |
| | | </select> |
| | | </div> |
| | | </div> |
| | | </Show> |
| | | </div> |
| | | |
| | | <div class="flex flex-col gap-2"> |
| | | <div class="flex flex-col items-center"> |
| | | <ConfigTitle>{t('useTwemoji')}</ConfigTitle> |
| | | |
| | | <input |
| | | type="checkbox" |
| | | class="toggle" |
| | | checked={useTwemoji()} |
| | | onChange={(e) => setUseTwemoji(e.target.checked)} |
| | | /> |
| | | </div> |
| | | |
| | | <div class="flex flex-col"> |
| | | <ConfigTitle>{t('switchLanguage')}</ConfigTitle> |
| | | |
| | | <select |
| | | class="select select-bordered" |
| | | onChange={(e) => setLocale(e.target.value as LANG)} |
| | | > |
| | | <For each={languages}> |
| | | {(lang) => ( |
| | | <option selected={locale() === lang.value} value={lang.value}> |
| | | {lang.label()} |
| | | </option> |
| | | )} |
| | | </For> |
| | | </select> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | ) |
| | | } |
| | | |
| | | const Versions = () => { |
| | | const [backendVersion, setBackendVersion] = createSignal('') |
| | | const [isUpdateAvailable, setIsUpdateAvailable] = createSignal(false) |
| | | |
| | | onMount(async () => { |
| | | const version = await fetchBackendVersionAPI() |
| | | setBackendVersion(version) |
| | | setIsUpdateAvailable(await isUpdateAvailableAPI(version)) |
| | | }) |
| | | |
| | | return ( |
| | | <div class="flex flex-col gap-2"> |
| | | <div class="grid grid-cols-2 gap-4"> |
| | | <kbd class="kbd">{import.meta.env.version}</kbd> |
| | | |
| | | <div class="relative"> |
| | | <Show when={isUpdateAvailable()}> |
| | | <span class="absolute -right-1 -top-1 flex h-3 w-3"> |
| | | <span class="absolute inline-flex h-full w-full animate-ping rounded-full bg-info opacity-75" /> |
| | | <span class="inline-flex h-3 w-3 rounded-full bg-info" /> |
| | | </span> |
| | | </Show> |
| | | |
| | | <kbd class="kbd w-full">{backendVersion()}</kbd> |
| | | </div> |
| | | </div> |
| | | |
| | | <h1 class="text-center text-lg font-bold">{endpoint()?.url}</h1> |
| | | </div> |
| | | ) |
| | | } |
| | | |
| | | export default () => { |
| | | const [t] = useI18n() |
| | | |
| | | return ( |
| | | <div class="mx-auto flex max-w-screen-md flex-col gap-4"> |
| | | <ConfigTitle withDivider>{t('dnsQuery')}</ConfigTitle> |
| | | |
| | | <DNSQueryForm /> |
| | | |
| | | <ConfigTitle withDivider>{t('coreConfig')}</ConfigTitle> |
| | | |
| | | <ConfigForm /> |
| | | |
| | | <ConfigTitle withDivider>{t('xdConfig')}</ConfigTitle> |
| | | |
| | | <ConfigForXd /> |
| | | |
| | | <ConfigTitle withDivider>{t('version')}</ConfigTitle> |
| | | |
| | | <Versions /> |
| | | </div> |
| | | ) |
| | | } |
| New file |
| | |
| | | import { writeClipboard } from '@solid-primitives/clipboard' |
| | | import { makePersisted } from '@solid-primitives/storage' |
| | | import { |
| | | IconInfoSmall, |
| | | IconPlayerPause, |
| | | IconPlayerPlay, |
| | | IconSettings, |
| | | IconSortAscending, |
| | | IconSortDescending, |
| | | IconX, |
| | | IconZoomInFilled, |
| | | IconZoomOutFilled, |
| | | } from '@tabler/icons-solidjs' |
| | | import { rankItem } from '@tanstack/match-sorter-utils' |
| | | import { |
| | | ColumnDef, |
| | | FilterFn, |
| | | GroupingState, |
| | | SortingState, |
| | | createSolidTable, |
| | | flexRender, |
| | | getCoreRowModel, |
| | | getExpandedRowModel, |
| | | getFilteredRowModel, |
| | | getGroupedRowModel, |
| | | getSortedRowModel, |
| | | } from '@tanstack/solid-table' |
| | | import byteSize from 'byte-size' |
| | | import dayjs from 'dayjs' |
| | | import { uniq } from 'lodash' |
| | | import { |
| | | For, |
| | | Index, |
| | | createEffect, |
| | | createMemo, |
| | | createSignal, |
| | | on, |
| | | } from 'solid-js' |
| | | import { twMerge } from 'tailwind-merge' |
| | | import { closeAllConnectionsAPI, closeSingleConnectionAPI } from '~/apis' |
| | | import { |
| | | Button, |
| | | ConnectionsSettingsModal, |
| | | ConnectionsTableDetailsModal, |
| | | } from '~/components' |
| | | import { CONNECTIONS_TABLE_ACCESSOR_KEY } from '~/constants' |
| | | import { useI18n } from '~/i18n' |
| | | import { |
| | | allConnections, |
| | | clientSourceIPTags, |
| | | connectionsTableColumnOrder, |
| | | connectionsTableColumnVisibility, |
| | | connectionsTableSize, |
| | | formatTimeFromNow, |
| | | setConnectionsTableColumnOrder, |
| | | setConnectionsTableColumnVisibility, |
| | | tableSizeClassName, |
| | | useConnections, |
| | | } from '~/signals' |
| | | import type { Connection } from '~/types' |
| | | |
| | | enum ActiveTab { |
| | | activeConnections, |
| | | closedConnections, |
| | | } |
| | | |
| | | const fuzzyFilter: FilterFn<Connection> = (row, columnId, value, addMeta) => { |
| | | // Rank the item |
| | | const itemRank = rankItem(row.getValue(columnId), value) |
| | | |
| | | // Store the itemRank info |
| | | addMeta({ |
| | | itemRank, |
| | | }) |
| | | |
| | | // Return if the item should be filtered in/out |
| | | return itemRank.passed |
| | | } |
| | | |
| | | export default () => { |
| | | let connectionsSettingsModalRef: HTMLDialogElement | undefined |
| | | let connectionsDetailsModalRef: HTMLDialogElement | undefined |
| | | |
| | | const [t] = useI18n() |
| | | |
| | | const [activeTab, setActiveTab] = createSignal(ActiveTab.activeConnections) |
| | | const { activeConnections, closedConnections, paused, setPaused } = |
| | | useConnections() |
| | | |
| | | const [globalFilter, setGlobalFilter] = createSignal('') |
| | | |
| | | const [selectedConnectionID, setSelectedConnectionID] = createSignal<string>() |
| | | |
| | | const columns: ColumnDef<Connection>[] = [ |
| | | { |
| | | header: () => t('details'), |
| | | enableGrouping: false, |
| | | enableSorting: false, |
| | | enableColumnFilter: false, |
| | | enableGlobalFilter: false, |
| | | accessorKey: CONNECTIONS_TABLE_ACCESSOR_KEY.Details, |
| | | cell: ({ row }) => ( |
| | | <div class="flex h-4 items-center"> |
| | | <Button |
| | | class="btn-circle btn-xs" |
| | | onClick={() => { |
| | | setSelectedConnectionID(row.original.id) |
| | | |
| | | connectionsDetailsModalRef?.showModal() |
| | | }} |
| | | icon={<IconInfoSmall size="16" />} |
| | | /> |
| | | </div> |
| | | ), |
| | | }, |
| | | { |
| | | header: () => t('close'), |
| | | enableGrouping: false, |
| | | enableSorting: false, |
| | | enableColumnFilter: false, |
| | | enableGlobalFilter: false, |
| | | accessorKey: CONNECTIONS_TABLE_ACCESSOR_KEY.Close, |
| | | cell: ({ row }) => ( |
| | | <div class="flex h-4 items-center"> |
| | | <Button |
| | | class="btn-circle btn-xs" |
| | | onClick={() => closeSingleConnectionAPI(row.original.id)} |
| | | icon={<IconX size="16" />} |
| | | /> |
| | | </div> |
| | | ), |
| | | }, |
| | | { |
| | | header: () => t('ID'), |
| | | enableGrouping: false, |
| | | accessorKey: CONNECTIONS_TABLE_ACCESSOR_KEY.ID, |
| | | accessorFn: (original) => original.id, |
| | | }, |
| | | { |
| | | header: () => t('type'), |
| | | accessorKey: CONNECTIONS_TABLE_ACCESSOR_KEY.Type, |
| | | accessorFn: (original) => |
| | | `${original.metadata.type}(${original.metadata.network})`, |
| | | }, |
| | | { |
| | | header: () => t('process'), |
| | | accessorKey: CONNECTIONS_TABLE_ACCESSOR_KEY.Process, |
| | | accessorFn: (original) => |
| | | original.metadata.process || |
| | | original.metadata.processPath.replace(/^.*[/\\](.*)$/, '$1') || |
| | | '-', |
| | | }, |
| | | { |
| | | header: () => t('host'), |
| | | accessorKey: CONNECTIONS_TABLE_ACCESSOR_KEY.Host, |
| | | accessorFn: (original) => |
| | | `${ |
| | | original.metadata.host |
| | | ? original.metadata.host |
| | | : original.metadata.destinationIP |
| | | }:${original.metadata.destinationPort}`, |
| | | }, |
| | | { |
| | | header: () => t('sniffHost'), |
| | | accessorKey: CONNECTIONS_TABLE_ACCESSOR_KEY.SniffHost, |
| | | accessorFn: (original) => original.metadata.sniffHost || '-', |
| | | }, |
| | | { |
| | | header: () => t('rules'), |
| | | accessorKey: CONNECTIONS_TABLE_ACCESSOR_KEY.Rule, |
| | | accessorFn: (original) => |
| | | !original.rulePayload |
| | | ? original.rule |
| | | : `${original.rule} :: ${original.rulePayload}`, |
| | | }, |
| | | { |
| | | header: () => t('chains'), |
| | | accessorKey: CONNECTIONS_TABLE_ACCESSOR_KEY.Chains, |
| | | accessorFn: (original) => original.chains.slice().reverse().join(' :: '), |
| | | }, |
| | | { |
| | | header: () => t('connectTime'), |
| | | enableGrouping: false, |
| | | accessorKey: CONNECTIONS_TABLE_ACCESSOR_KEY.ConnectTime, |
| | | accessorFn: (original) => formatTimeFromNow(original.start), |
| | | sortingFn: (prev, next) => |
| | | dayjs(prev.original.start).valueOf() - |
| | | dayjs(next.original.start).valueOf(), |
| | | }, |
| | | { |
| | | header: () => t('dlSpeed'), |
| | | enableGrouping: false, |
| | | accessorKey: CONNECTIONS_TABLE_ACCESSOR_KEY.DlSpeed, |
| | | accessorFn: (original) => `${byteSize(original.downloadSpeed)}/s`, |
| | | sortingFn: (prev, next) => |
| | | prev.original.downloadSpeed - next.original.downloadSpeed, |
| | | }, |
| | | { |
| | | header: () => t('ulSpeed'), |
| | | enableGrouping: false, |
| | | accessorKey: CONNECTIONS_TABLE_ACCESSOR_KEY.ULSpeed, |
| | | accessorFn: (original) => `${byteSize(original.uploadSpeed)}/s`, |
| | | sortingFn: (prev, next) => |
| | | prev.original.uploadSpeed - next.original.uploadSpeed, |
| | | }, |
| | | { |
| | | header: () => t('dl'), |
| | | enableGrouping: false, |
| | | accessorKey: CONNECTIONS_TABLE_ACCESSOR_KEY.Download, |
| | | accessorFn: (original) => byteSize(original.download), |
| | | sortingFn: (prev, next) => |
| | | prev.original.download - next.original.download, |
| | | }, |
| | | { |
| | | header: () => t('ul'), |
| | | enableGrouping: false, |
| | | accessorKey: CONNECTIONS_TABLE_ACCESSOR_KEY.Upload, |
| | | accessorFn: (original) => byteSize(original.upload), |
| | | sortingFn: (prev, next) => prev.original.upload - next.original.upload, |
| | | }, |
| | | { |
| | | header: () => t('sourceIP'), |
| | | accessorKey: CONNECTIONS_TABLE_ACCESSOR_KEY.SourceIP, |
| | | accessorFn: (original) => { |
| | | const tag = clientSourceIPTags().find( |
| | | (tag) => tag.sourceIP === original.metadata.sourceIP, |
| | | ) |
| | | |
| | | return tag ? tag.tagName : original.metadata.sourceIP |
| | | }, |
| | | }, |
| | | { |
| | | header: () => t('sourcePort'), |
| | | accessorKey: CONNECTIONS_TABLE_ACCESSOR_KEY.SourcePort, |
| | | accessorFn: (original) => original.metadata.sourcePort, |
| | | }, |
| | | { |
| | | header: () => t('destination'), |
| | | accessorKey: CONNECTIONS_TABLE_ACCESSOR_KEY.Destination, |
| | | accessorFn: (original) => |
| | | original.metadata.remoteDestination || |
| | | original.metadata.destinationIP || |
| | | original.metadata.host, |
| | | }, |
| | | ] |
| | | |
| | | const [grouping, setGrouping] = createSignal<GroupingState>([]) |
| | | const [sorting, setSorting] = makePersisted( |
| | | createSignal<SortingState>([ |
| | | { id: CONNECTIONS_TABLE_ACCESSOR_KEY.ConnectTime, desc: true }, |
| | | ]), |
| | | { name: 'connectionsTableSorting', storage: localStorage }, |
| | | ) |
| | | |
| | | const table = createSolidTable({ |
| | | filterFns: { |
| | | fuzzy: fuzzyFilter, |
| | | }, |
| | | state: { |
| | | get columnOrder() { |
| | | return connectionsTableColumnOrder() |
| | | }, |
| | | get grouping() { |
| | | return grouping() |
| | | }, |
| | | get sorting() { |
| | | return sorting() |
| | | }, |
| | | get columnVisibility() { |
| | | return connectionsTableColumnVisibility() |
| | | }, |
| | | get globalFilter() { |
| | | return globalFilter() |
| | | }, |
| | | }, |
| | | get data() { |
| | | return activeTab() === ActiveTab.activeConnections |
| | | ? activeConnections() |
| | | : closedConnections() |
| | | }, |
| | | sortDescFirst: true, |
| | | enableHiding: true, |
| | | columns, |
| | | onGlobalFilterChange: setGlobalFilter, |
| | | globalFilterFn: fuzzyFilter, |
| | | onGroupingChange: setGrouping, |
| | | onSortingChange: setSorting, |
| | | getFilteredRowModel: getFilteredRowModel(), |
| | | getSortedRowModel: getSortedRowModel(), |
| | | getExpandedRowModel: getExpandedRowModel(), |
| | | getGroupedRowModel: getGroupedRowModel(), |
| | | getCoreRowModel: getCoreRowModel(), |
| | | }) |
| | | |
| | | const sourceIPHeader = table |
| | | .getFlatHeaders() |
| | | .find(({ id }) => id === CONNECTIONS_TABLE_ACCESSOR_KEY.SourceIP) |
| | | |
| | | const [sourceIPFilter, setSourceIPFilter] = createSignal('') |
| | | |
| | | createEffect( |
| | | on(sourceIPFilter, () => { |
| | | const tagged = clientSourceIPTags().find( |
| | | (tag) => tag.sourceIP === sourceIPFilter(), |
| | | ) |
| | | |
| | | sourceIPHeader?.column.setFilterValue( |
| | | tagged ? tagged.tagName : sourceIPFilter(), |
| | | ) |
| | | }), |
| | | ) |
| | | |
| | | const tabs = createMemo(() => [ |
| | | { |
| | | type: ActiveTab.activeConnections, |
| | | name: t('active'), |
| | | count: activeConnections().length, |
| | | }, |
| | | { |
| | | type: ActiveTab.closedConnections, |
| | | name: t('closed'), |
| | | count: closedConnections().length, |
| | | }, |
| | | ]) |
| | | |
| | | return ( |
| | | <div class="flex h-full flex-col gap-2"> |
| | | <div class="flex w-full flex-wrap items-center gap-2"> |
| | | <div class="flex items-center gap-2"> |
| | | <div class="tabs-boxed tabs gap-2"> |
| | | <Index each={tabs()}> |
| | | {(tab) => ( |
| | | <button |
| | | class={twMerge( |
| | | activeTab() === tab().type && 'tab-active', |
| | | 'tab tab-sm gap-2 sm:tab-md', |
| | | )} |
| | | onClick={() => setActiveTab(tab().type)} |
| | | > |
| | | <span>{tab().name}</span> |
| | | <div class="badge badge-sm">{tab().count}</div> |
| | | </button> |
| | | )} |
| | | </Index> |
| | | </div> |
| | | |
| | | <select |
| | | class="select select-bordered select-primary select-sm w-full max-w-full flex-1 sm:select-md" |
| | | onChange={(e) => setSourceIPFilter(e.target.value)} |
| | | > |
| | | <option value="">{t('all')}</option> |
| | | |
| | | <Index |
| | | each={uniq( |
| | | allConnections().map(({ metadata: { sourceIP } }) => { |
| | | const tagged = clientSourceIPTags().find( |
| | | (tag) => tag.sourceIP === sourceIP, |
| | | ) |
| | | |
| | | return tagged ? tagged.tagName : sourceIP |
| | | }), |
| | | ).sort()} |
| | | > |
| | | {(sourceIP) => <option value={sourceIP()}>{sourceIP()}</option>} |
| | | </Index> |
| | | </select> |
| | | </div> |
| | | |
| | | <div class="join flex flex-1 items-center md:flex-1"> |
| | | <input |
| | | type="search" |
| | | class="input join-item input-primary input-sm min-w-0 flex-1 sm:input-md" |
| | | placeholder={t('search')} |
| | | onInput={(e) => setGlobalFilter(e.target.value)} |
| | | /> |
| | | |
| | | <Button |
| | | class="join-item btn-sm sm:btn-md" |
| | | onClick={() => setPaused((paused) => !paused)} |
| | | icon={paused() ? <IconPlayerPlay /> : <IconPlayerPause />} |
| | | /> |
| | | |
| | | <Button |
| | | class="join-item btn-sm sm:btn-md" |
| | | onClick={() => { |
| | | if (table.getState().globalFilter) { |
| | | table |
| | | .getFilteredRowModel() |
| | | .rows.forEach(({ id }) => closeSingleConnectionAPI(id)) |
| | | } else { |
| | | closeAllConnectionsAPI() |
| | | } |
| | | }} |
| | | icon={<IconX />} |
| | | /> |
| | | |
| | | <Button |
| | | class="btn join-item btn-sm sm:btn-md" |
| | | onClick={() => connectionsSettingsModalRef?.showModal()} |
| | | icon={<IconSettings />} |
| | | /> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="overflow-x-auto whitespace-nowrap rounded-md bg-base-300"> |
| | | <table |
| | | class={twMerge( |
| | | tableSizeClassName(connectionsTableSize()), |
| | | 'table table-zebra relative rounded-none', |
| | | )} |
| | | > |
| | | <thead class="sticky top-0 z-10 h-8"> |
| | | <For each={table.getHeaderGroups()}> |
| | | {(headerGroup) => ( |
| | | <tr> |
| | | <For each={headerGroup.headers}> |
| | | {(header) => ( |
| | | <th class="bg-base-200"> |
| | | <div class={twMerge('flex items-center gap-2')}> |
| | | {header.column.getCanGroup() ? ( |
| | | <button |
| | | class="cursor-pointer" |
| | | onClick={header.column.getToggleGroupingHandler()} |
| | | > |
| | | {header.column.getIsGrouped() ? ( |
| | | <IconZoomOutFilled size={18} /> |
| | | ) : ( |
| | | <IconZoomInFilled size={18} /> |
| | | )} |
| | | </button> |
| | | ) : null} |
| | | |
| | | <div |
| | | class={twMerge( |
| | | header.column.getCanSort() && |
| | | 'cursor-pointer select-none', |
| | | 'flex-1', |
| | | )} |
| | | onClick={header.column.getToggleSortingHandler()} |
| | | > |
| | | {flexRender( |
| | | header.column.columnDef.header, |
| | | header.getContext(), |
| | | )} |
| | | </div> |
| | | |
| | | {{ |
| | | asc: <IconSortAscending />, |
| | | desc: <IconSortDescending />, |
| | | }[header.column.getIsSorted() as string] ?? null} |
| | | </div> |
| | | </th> |
| | | )} |
| | | </For> |
| | | </tr> |
| | | )} |
| | | </For> |
| | | </thead> |
| | | |
| | | <tbody> |
| | | <For each={table.getRowModel().rows}> |
| | | {(row) => ( |
| | | <tr class="hover:!bg-primary hover:text-primary-content"> |
| | | <For each={row.getVisibleCells()}> |
| | | {(cell) => { |
| | | return ( |
| | | <td |
| | | class="py-2" |
| | | onContextMenu={(e) => { |
| | | e.preventDefault() |
| | | |
| | | const value = cell.renderValue() as null | string |
| | | value && writeClipboard(value).catch(() => {}) |
| | | }} |
| | | > |
| | | {cell.getIsGrouped() ? ( |
| | | <button |
| | | class={twMerge( |
| | | row.getCanExpand() |
| | | ? 'cursor-pointer' |
| | | : 'cursor-normal', |
| | | 'flex items-center gap-2', |
| | | )} |
| | | onClick={row.getToggleExpandedHandler()} |
| | | > |
| | | <div> |
| | | {row.getIsExpanded() ? ( |
| | | <IconZoomOutFilled size={18} /> |
| | | ) : ( |
| | | <IconZoomInFilled size={18} /> |
| | | )} |
| | | </div> |
| | | |
| | | <div> |
| | | {flexRender( |
| | | cell.column.columnDef.cell, |
| | | cell.getContext(), |
| | | )} |
| | | </div> |
| | | |
| | | <div>({row.subRows.length})</div> |
| | | </button> |
| | | ) : cell.getIsAggregated() ? ( |
| | | flexRender( |
| | | cell.column.columnDef.aggregatedCell ?? |
| | | cell.column.columnDef.cell, |
| | | cell.getContext(), |
| | | ) |
| | | ) : cell.getIsPlaceholder() ? null : ( |
| | | flexRender( |
| | | cell.column.columnDef.cell, |
| | | cell.getContext(), |
| | | ) |
| | | )} |
| | | </td> |
| | | ) |
| | | }} |
| | | </For> |
| | | </tr> |
| | | )} |
| | | </For> |
| | | </tbody> |
| | | </table> |
| | | </div> |
| | | |
| | | <ConnectionsSettingsModal |
| | | ref={(el) => (connectionsSettingsModalRef = el)} |
| | | order={connectionsTableColumnOrder()} |
| | | visible={connectionsTableColumnVisibility()} |
| | | onOrderChange={(data) => setConnectionsTableColumnOrder(data)} |
| | | onVisibleChange={(data) => |
| | | setConnectionsTableColumnVisibility({ ...data }) |
| | | } |
| | | /> |
| | | |
| | | <ConnectionsTableDetailsModal |
| | | ref={(el) => (connectionsDetailsModalRef = el)} |
| | | selectedConnectionID={selectedConnectionID()} |
| | | /> |
| | | </div> |
| | | ) |
| | | } |
| New file |
| | |
| | | import { makePersisted } from '@solid-primitives/storage' |
| | | import { |
| | | IconSettings, |
| | | IconSortAscending, |
| | | IconSortDescending, |
| | | } from '@tabler/icons-solidjs' |
| | | import { rankItem } from '@tanstack/match-sorter-utils' |
| | | import { |
| | | ColumnDef, |
| | | FilterFn, |
| | | SortingState, |
| | | createSolidTable, |
| | | flexRender, |
| | | getCoreRowModel, |
| | | getFilteredRowModel, |
| | | getSortedRowModel, |
| | | } from '@tanstack/solid-table' |
| | | import { For, Index, createEffect, createSignal } from 'solid-js' |
| | | import { twMerge } from 'tailwind-merge' |
| | | import { Button, LogsSettingsModal } from '~/components' |
| | | import { LOG_LEVEL } from '~/constants' |
| | | import { useI18n } from '~/i18n' |
| | | import { logsTableSize, tableSizeClassName, useWsRequest } from '~/signals' |
| | | import { logLevel, logMaxRows } from '~/signals/config' |
| | | import { Log } from '~/types' |
| | | |
| | | type LogWithSeq = Log & { seq: number } |
| | | |
| | | const fuzzyFilter: FilterFn<LogWithSeq> = (row, columnId, value, addMeta) => { |
| | | // Rank the item |
| | | const itemRank = rankItem(row.getValue(columnId), value) |
| | | |
| | | // Store the itemRank info |
| | | addMeta({ |
| | | itemRank, |
| | | }) |
| | | |
| | | // Return if the item should be filtered in/out |
| | | return itemRank.passed |
| | | } |
| | | |
| | | export default () => { |
| | | let logsSettingsModalRef: HTMLDialogElement | undefined |
| | | |
| | | const [t] = useI18n() |
| | | |
| | | let seq = 1 |
| | | const [logs, setLogs] = createSignal<LogWithSeq[]>([]) |
| | | |
| | | const logsData = useWsRequest<Log>('logs', { level: logLevel() }) |
| | | |
| | | createEffect(() => { |
| | | const data = logsData() |
| | | |
| | | if (!data) { |
| | | return |
| | | } |
| | | |
| | | setLogs((logs) => [{ ...data, seq }, ...logs].slice(0, logMaxRows())) |
| | | |
| | | seq++ |
| | | }) |
| | | |
| | | const [globalFilter, setGlobalFilter] = createSignal('') |
| | | |
| | | const [sorting, setSorting] = makePersisted(createSignal<SortingState>([]), { |
| | | name: 'logsTableSorting', |
| | | storage: localStorage, |
| | | }) |
| | | |
| | | const columns: ColumnDef<LogWithSeq>[] = [ |
| | | { |
| | | header: t('sequence'), |
| | | accessorFn: (row) => row.seq, |
| | | }, |
| | | { |
| | | header: t('type'), |
| | | accessorFn: (row) => row.type, |
| | | cell: ({ row }) => { |
| | | const type = row.original.type as LOG_LEVEL |
| | | |
| | | let className = '' |
| | | |
| | | switch (type) { |
| | | case LOG_LEVEL.Error: |
| | | className = 'text-error' |
| | | break |
| | | case LOG_LEVEL.Warning: |
| | | className = 'text-warning' |
| | | break |
| | | case LOG_LEVEL.Info: |
| | | className = 'text-info' |
| | | break |
| | | case LOG_LEVEL.Debug: |
| | | className = 'text-success' |
| | | break |
| | | } |
| | | |
| | | return <span class={className}>{`[${row.original.type}]`}</span> |
| | | }, |
| | | }, |
| | | { |
| | | header: t('payload'), |
| | | accessorFn: (row) => row.payload, |
| | | }, |
| | | ] |
| | | |
| | | const table = createSolidTable({ |
| | | filterFns: { |
| | | fuzzy: fuzzyFilter, |
| | | }, |
| | | state: { |
| | | get globalFilter() { |
| | | return globalFilter() |
| | | }, |
| | | get sorting() { |
| | | return sorting() |
| | | }, |
| | | }, |
| | | get data() { |
| | | return logs() |
| | | }, |
| | | sortDescFirst: true, |
| | | columns, |
| | | onGlobalFilterChange: setGlobalFilter, |
| | | onSortingChange: setSorting, |
| | | globalFilterFn: fuzzyFilter, |
| | | getFilteredRowModel: getFilteredRowModel(), |
| | | getSortedRowModel: getSortedRowModel(), |
| | | getCoreRowModel: getCoreRowModel(), |
| | | }) |
| | | |
| | | return ( |
| | | <div class="flex h-full flex-col gap-2"> |
| | | <div class="join w-full"> |
| | | <input |
| | | type="search" |
| | | class="input join-item input-primary input-sm flex-1 flex-shrink-0 sm:input-md" |
| | | placeholder={t('search')} |
| | | onInput={(e) => setGlobalFilter(e.target.value)} |
| | | /> |
| | | |
| | | <Button |
| | | class="join-item btn-sm sm:btn-md" |
| | | onClick={() => logsSettingsModalRef?.showModal()} |
| | | icon={<IconSettings />} |
| | | /> |
| | | </div> |
| | | |
| | | <div class="overflow-x-auto whitespace-nowrap rounded-md bg-base-300"> |
| | | <table |
| | | class={twMerge( |
| | | tableSizeClassName(logsTableSize()), |
| | | 'table relative rounded-none', |
| | | )} |
| | | > |
| | | <thead class="sticky top-0 z-10"> |
| | | <Index each={table.getHeaderGroups()}> |
| | | {(keyedHeaderGroup) => { |
| | | const headerGroup = keyedHeaderGroup() |
| | | |
| | | return ( |
| | | <tr> |
| | | <Index each={headerGroup.headers}> |
| | | {(keyedHeader) => { |
| | | const header = keyedHeader() |
| | | |
| | | return ( |
| | | <th class="bg-base-200"> |
| | | <div class="flex items-center"> |
| | | <div |
| | | class={twMerge( |
| | | header.column.getCanSort() && |
| | | 'cursor-pointer select-none', |
| | | 'flex-1', |
| | | )} |
| | | onClick={header.column.getToggleSortingHandler()} |
| | | > |
| | | {flexRender( |
| | | header.column.columnDef.header, |
| | | header.getContext(), |
| | | )} |
| | | </div> |
| | | |
| | | {{ |
| | | asc: <IconSortAscending />, |
| | | desc: <IconSortDescending />, |
| | | }[header.column.getIsSorted() as string] ?? null} |
| | | </div> |
| | | </th> |
| | | ) |
| | | }} |
| | | </Index> |
| | | </tr> |
| | | ) |
| | | }} |
| | | </Index> |
| | | </thead> |
| | | |
| | | <tbody> |
| | | <For each={table.getRowModel().rows}> |
| | | {(row) => ( |
| | | <tr class="hover:!bg-primary hover:text-primary-content"> |
| | | <For each={row.getVisibleCells()}> |
| | | {(cell) => ( |
| | | <td class="py-2"> |
| | | {flexRender( |
| | | cell.column.columnDef.cell, |
| | | cell.getContext(), |
| | | )} |
| | | </td> |
| | | )} |
| | | </For> |
| | | </tr> |
| | | )} |
| | | </For> |
| | | </tbody> |
| | | </table> |
| | | </div> |
| | | |
| | | <LogsSettingsModal ref={(el) => (logsSettingsModalRef = el)} /> |
| | | </div> |
| | | ) |
| | | } |
| New file |
| | |
| | | import { makeTimer } from '@solid-primitives/timer' |
| | | import type { ApexOptions } from 'apexcharts' |
| | | import byteSize from 'byte-size' |
| | | import { merge } from 'lodash' |
| | | import { SolidApexCharts } from 'solid-apexcharts' |
| | | import { |
| | | JSX, |
| | | ParentComponent, |
| | | batch, |
| | | children, |
| | | createEffect, |
| | | createMemo, |
| | | createSignal, |
| | | } from 'solid-js' |
| | | import { CHART_MAX_XAXIS, DEFAULT_CHART_OPTIONS } from '~/constants' |
| | | import { useI18n } from '~/i18n' |
| | | import { latestConnectionMsg, useWsRequest } from '~/signals' |
| | | |
| | | const TrafficWidget: ParentComponent<{ label: JSX.Element }> = (props) => ( |
| | | <div class="stat flex-1 place-items-center"> |
| | | <div class="stat-title text-primary-content">{props.label}</div> |
| | | <div class="stat-value text-2xl text-primary-content lg:text-3xl"> |
| | | {children(() => props.children)()} |
| | | </div> |
| | | </div> |
| | | ) |
| | | |
| | | export default () => { |
| | | const [t] = useI18n() |
| | | |
| | | const [traffics, setTraffics] = createSignal<{ down: number; up: number }[]>( |
| | | [], |
| | | ) |
| | | const [memories, setMemories] = createSignal<number[]>([]) |
| | | |
| | | // https://github.com/apexcharts/apexcharts.js/blob/main/samples/source/line/realtime.xml |
| | | // TODO: needs a better way |
| | | makeTimer( |
| | | () => { |
| | | batch(() => { |
| | | setTraffics((traffics) => traffics.slice(-CHART_MAX_XAXIS)) |
| | | setMemories((memo) => memo.slice(-CHART_MAX_XAXIS)) |
| | | }) |
| | | }, |
| | | // we shrink the chart data array size down every minute to prevent memory leaks |
| | | 60 * 1000, |
| | | setInterval, |
| | | ) |
| | | |
| | | const traffic = useWsRequest<{ down: number; up: number }>('traffic') |
| | | |
| | | createEffect(() => { |
| | | const t = traffic() |
| | | |
| | | if (t) setTraffics((traffics) => [...traffics, t]) |
| | | }) |
| | | |
| | | const trafficChartOptions = createMemo<ApexOptions>(() => |
| | | merge({ title: { text: t('traffic') } }, DEFAULT_CHART_OPTIONS), |
| | | ) |
| | | |
| | | const trafficChartSeries = createMemo(() => [ |
| | | { |
| | | name: t('down'), |
| | | data: traffics().map((t) => t.down), |
| | | }, |
| | | { |
| | | name: t('up'), |
| | | data: traffics().map((t) => t.up), |
| | | }, |
| | | ]) |
| | | |
| | | const memory = useWsRequest<{ inuse: number }>('memory') |
| | | |
| | | createEffect(() => { |
| | | const m = memory()?.inuse |
| | | |
| | | if (m) setMemories((memories) => [...memories, m]) |
| | | }) |
| | | |
| | | const memoryChartOptions = createMemo<ApexOptions>(() => |
| | | merge({ title: { text: t('memory') } }, DEFAULT_CHART_OPTIONS), |
| | | ) |
| | | |
| | | const memoryChartSeries = createMemo(() => [ |
| | | { name: t('memory'), data: memories() }, |
| | | ]) |
| | | |
| | | return ( |
| | | <div class="flex flex-col gap-2"> |
| | | <div class="stats stats-vertical w-full grid-cols-2 bg-primary shadow lg:stats-horizontal lg:flex"> |
| | | <TrafficWidget label={t('upload')}> |
| | | {byteSize(traffic()?.up || 0).toString()}/s |
| | | </TrafficWidget> |
| | | |
| | | <TrafficWidget label={t('download')}> |
| | | {byteSize(traffic()?.down || 0).toString()}/s |
| | | </TrafficWidget> |
| | | |
| | | <TrafficWidget label={t('uploadTotal')}> |
| | | {byteSize(latestConnectionMsg()?.uploadTotal || 0).toString()} |
| | | </TrafficWidget> |
| | | |
| | | <TrafficWidget label={t('downloadTotal')}> |
| | | {byteSize(latestConnectionMsg()?.downloadTotal || 0).toString()} |
| | | </TrafficWidget> |
| | | |
| | | <TrafficWidget label={t('activeConnections')}> |
| | | {latestConnectionMsg()?.connections?.length || 0} |
| | | </TrafficWidget> |
| | | |
| | | <TrafficWidget label={t('memoryUsage')}> |
| | | {byteSize(memory()?.inuse || 0).toString()} |
| | | </TrafficWidget> |
| | | </div> |
| | | |
| | | <div class="rounded-box flex flex-col gap-2 bg-base-300 py-4 lg:flex-row"> |
| | | <div class="flex-1"> |
| | | <SolidApexCharts |
| | | type="area" |
| | | options={trafficChartOptions()} |
| | | series={trafficChartSeries()} |
| | | /> |
| | | </div> |
| | | <div class="flex-1"> |
| | | <SolidApexCharts |
| | | type="line" |
| | | options={memoryChartOptions()} |
| | | series={memoryChartSeries()} |
| | | /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | ) |
| | | } |
| New file |
| | |
| | | import { |
| | | IconBrandSpeedtest, |
| | | IconReload, |
| | | IconSettings, |
| | | } from '@tabler/icons-solidjs' |
| | | import { For, Show, createMemo, createSignal, onMount } from 'solid-js' |
| | | import { twMerge } from 'tailwind-merge' |
| | | import { |
| | | Button, |
| | | Collapse, |
| | | ProxiesSettingsModal, |
| | | ProxyNodeCard, |
| | | ProxyNodePreview, |
| | | RenderInTwoColumns, |
| | | SubscriptionInfo, |
| | | } from '~/components' |
| | | import { |
| | | filterProxiesByAvailability, |
| | | sortProxiesByOrderingType, |
| | | } from '~/helpers' |
| | | import { useI18n } from '~/i18n' |
| | | import { |
| | | formatTimeFromNow, |
| | | hideUnAvailableProxies, |
| | | proxiesOrderingType, |
| | | useProxies, |
| | | } from '~/signals' |
| | | |
| | | enum ActiveTab { |
| | | proxyProviders = 'proxyProviders', |
| | | proxies = 'proxies', |
| | | } |
| | | |
| | | export default () => { |
| | | let proxiesSettingsModalRef: HTMLDialogElement | undefined |
| | | |
| | | const [t] = useI18n() |
| | | |
| | | const { |
| | | fetchProxies, |
| | | proxies, |
| | | selectProxyInGroup, |
| | | latencyMap, |
| | | proxyProviders, |
| | | updateProviderByProviderName, |
| | | updateAllProvider, |
| | | proxyGroupLatencyTest, |
| | | proxyProviderLatencyTest, |
| | | collapsedMap, |
| | | setCollapsedMap, |
| | | proxyGroupLatencyTestingMap, |
| | | proxyProviderLatencyTestingMap, |
| | | isAllProviderUpdating, |
| | | updatingMap, |
| | | } = useProxies() |
| | | |
| | | onMount(fetchProxies) |
| | | |
| | | const onProxyGroupLatencyTestClick = async ( |
| | | e: MouseEvent, |
| | | groupName: string, |
| | | ) => { |
| | | e.stopPropagation() |
| | | void proxyGroupLatencyTest(groupName) |
| | | } |
| | | |
| | | const onProxyProviderLatencyTestClick = ( |
| | | e: MouseEvent, |
| | | providerName: string, |
| | | ) => { |
| | | e.stopPropagation() |
| | | void proxyProviderLatencyTest(providerName) |
| | | } |
| | | |
| | | const onUpdateProxyProviderClick = (e: MouseEvent, providerName: string) => { |
| | | e.stopPropagation() |
| | | void updateProviderByProviderName(providerName) |
| | | } |
| | | |
| | | const onUpdateAllProviderClick = async (e: MouseEvent) => { |
| | | e.stopPropagation() |
| | | void updateAllProvider() |
| | | } |
| | | |
| | | const [activeTab, setActiveTab] = createSignal(ActiveTab.proxies) |
| | | |
| | | const tabs = () => [ |
| | | { |
| | | type: ActiveTab.proxies, |
| | | name: t('proxies'), |
| | | count: proxies().length, |
| | | }, |
| | | { |
| | | type: ActiveTab.proxyProviders, |
| | | name: t('proxyProviders'), |
| | | count: proxyProviders().length, |
| | | }, |
| | | ] |
| | | |
| | | return ( |
| | | <div class="flex h-full flex-col gap-2"> |
| | | <div class="flex items-center gap-2"> |
| | | <div class="tabs-boxed tabs gap-2"> |
| | | <For each={tabs()}> |
| | | {(tab) => ( |
| | | <button |
| | | class={twMerge( |
| | | activeTab() === tab.type && 'tab-active', |
| | | 'tab tab-sm gap-2 px-2 sm:tab-md', |
| | | )} |
| | | onClick={() => setActiveTab(tab.type)} |
| | | > |
| | | <span>{tab.name}</span> |
| | | <div class="badge badge-sm">{tab.count}</div> |
| | | </button> |
| | | )} |
| | | </For> |
| | | </div> |
| | | |
| | | <Show when={activeTab() === ActiveTab.proxyProviders}> |
| | | <Button |
| | | class="btn btn-circle btn-sm" |
| | | disabled={isAllProviderUpdating()} |
| | | onClick={(e) => onUpdateAllProviderClick(e)} |
| | | icon={ |
| | | <IconReload |
| | | class={twMerge( |
| | | isAllProviderUpdating() && 'animate-spin text-success', |
| | | )} |
| | | /> |
| | | } |
| | | /> |
| | | </Show> |
| | | |
| | | <div class="ml-auto"> |
| | | <Button |
| | | class="btn-circle btn-sm sm:btn-md" |
| | | onClick={() => proxiesSettingsModalRef?.showModal()} |
| | | icon={<IconSettings />} |
| | | /> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="flex-1 overflow-y-auto"> |
| | | <Show when={activeTab() === ActiveTab.proxies}> |
| | | <RenderInTwoColumns> |
| | | <For each={proxies()}> |
| | | {(proxyGroup) => { |
| | | const sortedProxyNames = createMemo(() => |
| | | filterProxiesByAvailability( |
| | | sortProxiesByOrderingType( |
| | | proxyGroup.all ?? [], |
| | | latencyMap(), |
| | | proxiesOrderingType(), |
| | | ), |
| | | latencyMap(), |
| | | hideUnAvailableProxies(), |
| | | ), |
| | | ) |
| | | |
| | | const title = ( |
| | | <> |
| | | <div class="flex items-center justify-between pr-8"> |
| | | <div class="flex items-center gap-2"> |
| | | <span>{proxyGroup.name}</span> |
| | | <div class="badge badge-sm"> |
| | | {proxyGroup.all?.length} |
| | | </div> |
| | | </div> |
| | | |
| | | <Button |
| | | class="btn-circle btn-sm" |
| | | disabled={ |
| | | proxyGroupLatencyTestingMap()[proxyGroup.name] |
| | | } |
| | | onClick={(e) => |
| | | onProxyGroupLatencyTestClick(e, proxyGroup.name) |
| | | } |
| | | icon={ |
| | | <IconBrandSpeedtest |
| | | class={twMerge( |
| | | proxyGroupLatencyTestingMap()[proxyGroup.name] && |
| | | 'animate-pulse text-success', |
| | | )} |
| | | /> |
| | | } |
| | | /> |
| | | </div> |
| | | |
| | | <div class="text-sm text-slate-500"> |
| | | {proxyGroup.type}{' '} |
| | | {proxyGroup.now?.length > 0 && ` :: ${proxyGroup.now}`} |
| | | </div> |
| | | |
| | | <Show when={!collapsedMap()[proxyGroup.name]}> |
| | | <ProxyNodePreview |
| | | proxyNameList={sortedProxyNames()} |
| | | now={proxyGroup.now} |
| | | /> |
| | | </Show> |
| | | </> |
| | | ) |
| | | |
| | | return ( |
| | | <Collapse |
| | | isOpen={collapsedMap()[proxyGroup.name]} |
| | | title={title} |
| | | onCollapse={(val) => setCollapsedMap(proxyGroup.name, val)} |
| | | > |
| | | <For each={sortedProxyNames()}> |
| | | {(proxyName) => ( |
| | | <ProxyNodeCard |
| | | proxyName={proxyName} |
| | | isSelected={proxyGroup.now === proxyName} |
| | | onClick={() => |
| | | void selectProxyInGroup(proxyGroup, proxyName) |
| | | } |
| | | /> |
| | | )} |
| | | </For> |
| | | </Collapse> |
| | | ) |
| | | }} |
| | | </For> |
| | | </RenderInTwoColumns> |
| | | </Show> |
| | | |
| | | <Show when={activeTab() === ActiveTab.proxyProviders}> |
| | | <RenderInTwoColumns> |
| | | <For each={proxyProviders()}> |
| | | {(proxyProvider) => { |
| | | const sortedProxyNames = createMemo(() => |
| | | sortProxiesByOrderingType( |
| | | proxyProvider.proxies.map((i) => i.name) ?? [], |
| | | latencyMap(), |
| | | proxiesOrderingType(), |
| | | ), |
| | | ) |
| | | |
| | | const title = ( |
| | | <> |
| | | <div class="flex items-center justify-between pr-8"> |
| | | <div class="flex items-center gap-2"> |
| | | <span>{proxyProvider.name}</span> |
| | | <div class="badge badge-sm"> |
| | | {proxyProvider.proxies.length} |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="flex items-center gap-2"> |
| | | <Button |
| | | class="btn btn-circle btn-sm" |
| | | disabled={updatingMap()[proxyProvider.name]} |
| | | onClick={(e) => |
| | | onUpdateProxyProviderClick(e, proxyProvider.name) |
| | | } |
| | | icon={ |
| | | <IconReload |
| | | class={twMerge( |
| | | updatingMap()[proxyProvider.name] && |
| | | 'animate-spin text-success', |
| | | )} |
| | | /> |
| | | } |
| | | /> |
| | | |
| | | <Button |
| | | class="btn btn-circle btn-sm" |
| | | disabled={ |
| | | proxyProviderLatencyTestingMap()[proxyProvider.name] |
| | | } |
| | | onClick={(e) => |
| | | onProxyProviderLatencyTestClick( |
| | | e, |
| | | proxyProvider.name, |
| | | ) |
| | | } |
| | | icon={ |
| | | <IconBrandSpeedtest |
| | | class={twMerge( |
| | | proxyProviderLatencyTestingMap()[ |
| | | proxyProvider.name |
| | | ] && 'animate-pulse text-success', |
| | | )} |
| | | /> |
| | | } |
| | | /> |
| | | </div> |
| | | </div> |
| | | |
| | | <SubscriptionInfo |
| | | subscriptionInfo={proxyProvider.subscriptionInfo} |
| | | /> |
| | | |
| | | <div class="text-sm text-slate-500"> |
| | | {proxyProvider.vehicleType} :: {t('updated')}{' '} |
| | | {formatTimeFromNow(proxyProvider.updatedAt)} |
| | | </div> |
| | | |
| | | <Show when={!collapsedMap()[proxyProvider.name]}> |
| | | <ProxyNodePreview proxyNameList={sortedProxyNames()} /> |
| | | </Show> |
| | | </> |
| | | ) |
| | | |
| | | return ( |
| | | <Collapse |
| | | isOpen={collapsedMap()[proxyProvider.name]} |
| | | title={title} |
| | | onCollapse={(val) => |
| | | setCollapsedMap(proxyProvider.name, val) |
| | | } |
| | | > |
| | | <For each={sortedProxyNames()}> |
| | | {(proxyName) => <ProxyNodeCard proxyName={proxyName} />} |
| | | </For> |
| | | </Collapse> |
| | | ) |
| | | }} |
| | | </For> |
| | | </RenderInTwoColumns> |
| | | </Show> |
| | | </div> |
| | | |
| | | <ProxiesSettingsModal ref={(el) => (proxiesSettingsModalRef = el)} /> |
| | | </div> |
| | | ) |
| | | } |
| New file |
| | |
| | | import { IconReload } from '@tabler/icons-solidjs' |
| | | import { createVirtualizer } from '@tanstack/solid-virtual' |
| | | import { matchSorter } from 'match-sorter' |
| | | import { For, Show, createMemo, createSignal, onMount } from 'solid-js' |
| | | import { twMerge } from 'tailwind-merge' |
| | | import { Button } from '~/components' |
| | | import { useStringBooleanMap } from '~/helpers' |
| | | import { useI18n } from '~/i18n' |
| | | import { formatTimeFromNow, useRules } from '~/signals' |
| | | import { Rule, RuleProvider } from '~/types' |
| | | |
| | | enum ActiveTab { |
| | | ruleProviders = 'ruleProviders', |
| | | rules = 'rules', |
| | | } |
| | | |
| | | export default () => { |
| | | const [t] = useI18n() |
| | | const { |
| | | rules, |
| | | ruleProviders, |
| | | updateRules, |
| | | updateAllRuleProvider, |
| | | updateRuleProviderByName, |
| | | } = useRules() |
| | | |
| | | onMount(updateRules) |
| | | |
| | | const { map: updatingMap, setWithCallback: setUpdatingMap } = |
| | | useStringBooleanMap() |
| | | const [allProviderIsUpdating, setAllProviderIsUpdating] = createSignal(false) |
| | | |
| | | const onUpdateProviderClick = (e: MouseEvent, name: string) => { |
| | | e.stopPropagation() |
| | | void setUpdatingMap(name, () => updateRuleProviderByName(name)) |
| | | } |
| | | |
| | | const onUpdateAllProviderClick = async (e: MouseEvent) => { |
| | | e.stopPropagation() |
| | | setAllProviderIsUpdating(true) |
| | | try { |
| | | await updateAllRuleProvider() |
| | | } catch {} |
| | | setAllProviderIsUpdating(false) |
| | | } |
| | | |
| | | const [activeTab, setActiveTab] = createSignal(ActiveTab.rules) |
| | | |
| | | const tabs = () => [ |
| | | { |
| | | type: ActiveTab.rules, |
| | | name: t('rules'), |
| | | count: rules().length, |
| | | }, |
| | | { |
| | | type: ActiveTab.ruleProviders, |
| | | name: t('ruleProviders'), |
| | | count: ruleProviders().length, |
| | | }, |
| | | ] |
| | | |
| | | const [globalFilter, setGlobalFilter] = createSignal('') |
| | | |
| | | const filteredRules = createMemo(() => |
| | | globalFilter() |
| | | ? matchSorter(rules(), globalFilter(), { |
| | | keys: ['type', 'payload', 'type'] as (keyof Rule)[], |
| | | }) |
| | | : rules(), |
| | | ) |
| | | |
| | | const filteredRuleProviders = createMemo(() => |
| | | globalFilter() |
| | | ? matchSorter(ruleProviders(), globalFilter(), { |
| | | keys: ['name', 'vehicleType', 'behavior'] as (keyof RuleProvider)[], |
| | | }) |
| | | : ruleProviders(), |
| | | ) |
| | | |
| | | let scrollElementRef: HTMLDivElement | undefined |
| | | |
| | | const getRuleItemKey = ({ type, payload, proxy }: Rule) => |
| | | `${type}-${payload}-${proxy}` |
| | | |
| | | const ruleVirtualizer = createVirtualizer({ |
| | | get count() { |
| | | return filteredRules().length |
| | | }, |
| | | getItemKey: (index) => getRuleItemKey(filteredRules()[index]), |
| | | getScrollElement: () => scrollElementRef!, |
| | | estimateSize: () => 82, |
| | | overscan: 5, |
| | | }) |
| | | |
| | | const ruleVirtualizerItems = ruleVirtualizer.getVirtualItems() |
| | | |
| | | const getRuleProviderItemKey = ({ |
| | | type, |
| | | name, |
| | | vehicleType, |
| | | behavior, |
| | | }: RuleProvider) => `${type}-${name}-${vehicleType}-${behavior}` |
| | | |
| | | const ruleProviderVirtualizer = createVirtualizer({ |
| | | get count() { |
| | | return filteredRuleProviders().length |
| | | }, |
| | | getItemKey: (index) => |
| | | getRuleProviderItemKey(filteredRuleProviders()[index]), |
| | | getScrollElement: () => scrollElementRef!, |
| | | estimateSize: () => 82, |
| | | overscan: 5, |
| | | }) |
| | | |
| | | const ruleProviderVirtualizerItems = ruleProviderVirtualizer.getVirtualItems() |
| | | |
| | | return ( |
| | | <div class="flex h-full flex-col gap-2"> |
| | | <div class="flex items-center gap-2"> |
| | | <div class="tabs-boxed tabs gap-2"> |
| | | <For each={tabs()}> |
| | | {(tab) => ( |
| | | <button |
| | | class={twMerge( |
| | | activeTab() === tab.type && 'tab-active', |
| | | 'tab tab-sm gap-2 px-2 md:tab-md', |
| | | )} |
| | | onClick={() => setActiveTab(tab.type)} |
| | | > |
| | | <span>{tab.name}</span> |
| | | <div class="badge badge-sm">{tab.count}</div> |
| | | </button> |
| | | )} |
| | | </For> |
| | | </div> |
| | | |
| | | <Show when={activeTab() === ActiveTab.ruleProviders}> |
| | | <Button |
| | | class="btn btn-circle btn-sm" |
| | | disabled={allProviderIsUpdating()} |
| | | onClick={(e) => onUpdateAllProviderClick(e)} |
| | | icon={ |
| | | <IconReload |
| | | class={twMerge( |
| | | allProviderIsUpdating() && 'animate-spin text-success', |
| | | )} |
| | | /> |
| | | } |
| | | /> |
| | | </Show> |
| | | </div> |
| | | |
| | | <input |
| | | class="input input-bordered input-primary" |
| | | placeholder={t('search')} |
| | | value={globalFilter()} |
| | | onInput={(e) => setGlobalFilter(e.currentTarget.value)} |
| | | /> |
| | | |
| | | <div |
| | | ref={(ref) => (scrollElementRef = ref)} |
| | | class="flex-1 overflow-y-auto" |
| | | > |
| | | <Show when={activeTab() === ActiveTab.rules}> |
| | | <div |
| | | class="relative" |
| | | style={{ height: `${ruleVirtualizer.getTotalSize()}px` }} |
| | | > |
| | | {ruleVirtualizerItems.map((virtualizerItem) => { |
| | | const rule = filteredRules().find( |
| | | (rule) => getRuleItemKey(rule) === virtualizerItem.key, |
| | | )! |
| | | |
| | | return ( |
| | | <div |
| | | ref={(el) => |
| | | onMount(() => ruleVirtualizer.measureElement(el)) |
| | | } |
| | | data-index={virtualizerItem.index} |
| | | class="absolute inset-x-0 top-0 pb-2 last:pb-0" |
| | | style={{ |
| | | transform: `translateY(${virtualizerItem.start}px)`, |
| | | }} |
| | | > |
| | | <div class="card card-bordered card-compact bg-base-200 p-4"> |
| | | <div class="flex items-center gap-2"> |
| | | <span class="break-all">{rule.payload}</span> |
| | | |
| | | <Show when={rule.size !== -1}> |
| | | <div class="badge badge-sm">{rule.size}</div> |
| | | </Show> |
| | | </div> |
| | | |
| | | <div class="text-xs text-slate-500"> |
| | | {rule.type} :: {rule.proxy} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | ) |
| | | })} |
| | | </div> |
| | | </Show> |
| | | |
| | | <Show when={activeTab() === ActiveTab.ruleProviders}> |
| | | <div |
| | | class="relative" |
| | | style={{ height: `${ruleProviderVirtualizer.getTotalSize()}px` }} |
| | | > |
| | | {ruleProviderVirtualizerItems.map((virtualizerItem) => { |
| | | const ruleProvider = ruleProviders().find( |
| | | (ruleProvider) => |
| | | getRuleProviderItemKey(ruleProvider) === virtualizerItem.key, |
| | | )! |
| | | |
| | | return ( |
| | | <div |
| | | ref={(el) => |
| | | onMount(() => ruleProviderVirtualizer.measureElement(el)) |
| | | } |
| | | data-index={virtualizerItem.index} |
| | | class="absolute inset-x-0 top-0 pb-2 last:pb-0" |
| | | style={{ |
| | | transform: `translateY(${virtualizerItem.start}px)`, |
| | | }} |
| | | > |
| | | <div class="card card-bordered card-compact bg-base-200 p-4"> |
| | | <div class="flex items-center gap-2 pr-8"> |
| | | <span class="break-all">{ruleProvider.name}</span> |
| | | <div class="badge badge-sm">{ruleProvider.ruleCount}</div> |
| | | </div> |
| | | |
| | | <div class="text-xs text-slate-500"> |
| | | {ruleProvider.vehicleType} / {ruleProvider.behavior} / |
| | | {t('updated')} {formatTimeFromNow(ruleProvider.updatedAt)} |
| | | </div> |
| | | |
| | | <Button |
| | | class="btn-circle btn-sm absolute right-2 top-2 mr-2 h-4" |
| | | disabled={updatingMap()[ruleProvider.name]} |
| | | onClick={(e) => |
| | | onUpdateProviderClick(e, ruleProvider.name) |
| | | } |
| | | icon={ |
| | | <IconReload |
| | | class={twMerge( |
| | | updatingMap()[ruleProvider.name] && |
| | | 'animate-spin text-success', |
| | | )} |
| | | /> |
| | | } |
| | | /> |
| | | </div> |
| | | </div> |
| | | ) |
| | | })} |
| | | </div> |
| | | </Show> |
| | | </div> |
| | | </div> |
| | | ) |
| | | } |
| New file |
| | |
| | | import { createForm } from '@felte/solid' |
| | | import { validator } from '@felte/validator-zod' |
| | | import { useLocation, useNavigate } from '@solidjs/router' |
| | | import { IconX } from '@tabler/icons-solidjs' |
| | | import ky from 'ky' |
| | | import { For, onMount } from 'solid-js' |
| | | import { toast } from 'solid-toast' |
| | | import { v4 as uuid } from 'uuid' |
| | | import { z } from 'zod' |
| | | import { Button } from '~/components' |
| | | import { transformEndpointURL } from '~/helpers' |
| | | import { useI18n } from '~/i18n' |
| | | import { |
| | | endpointList, |
| | | selectedEndpoint, |
| | | setEndpointList, |
| | | setSelectedEndpoint, |
| | | } from '~/signals' |
| | | |
| | | const schema = z.object({ |
| | | url: z.string().min(1), |
| | | secret: z.string(), |
| | | }) |
| | | |
| | | export default () => { |
| | | const [t] = useI18n() |
| | | const location = useLocation() |
| | | const navigate = useNavigate() |
| | | |
| | | const onSetupSuccess = (id: string) => { |
| | | setSelectedEndpoint(id) |
| | | navigate('/overview') |
| | | } |
| | | |
| | | const checkEndpoint = (url: string, secret: string) => |
| | | ky |
| | | .get(url, { |
| | | headers: secret |
| | | ? { |
| | | Authorization: `Bearer ${secret}`, |
| | | } |
| | | : {}, |
| | | }) |
| | | .then(({ ok }) => ok) |
| | | .catch((err) => { |
| | | const { message } = err as Error |
| | | |
| | | toast.error(message) |
| | | }) |
| | | |
| | | const onEndpointSelect = async (id: string) => { |
| | | const endpoint = endpointList().find((e) => e.id === id) |
| | | |
| | | if (!endpoint) { |
| | | return |
| | | } |
| | | |
| | | if (!(await checkEndpoint(endpoint.url, endpoint.secret))) { |
| | | return |
| | | } |
| | | |
| | | onSetupSuccess(id) |
| | | } |
| | | |
| | | const onSubmit = async ({ url, secret }: { url: string; secret: string }) => { |
| | | const transformedURL = transformEndpointURL(url) |
| | | |
| | | if (!(await checkEndpoint(transformedURL, secret))) { |
| | | return |
| | | } |
| | | |
| | | const id = uuid() |
| | | const list = endpointList().slice() |
| | | const point = list.find((history) => history.url === transformedURL) |
| | | |
| | | if (!point) { |
| | | // new host and secret |
| | | setEndpointList([{ id, url: transformedURL, secret }, ...list]) |
| | | onSetupSuccess(id) |
| | | |
| | | return |
| | | } |
| | | |
| | | // exist host we update secret and id no matter if secret is equal or not |
| | | point.secret = secret |
| | | point.id = id |
| | | |
| | | setEndpointList(list) |
| | | onSetupSuccess(id) |
| | | } |
| | | |
| | | const onError = (err: unknown) => { |
| | | const { message } = err as Error |
| | | |
| | | toast.error(message) |
| | | } |
| | | |
| | | const { form } = createForm<z.infer<typeof schema>>({ |
| | | extend: validator({ schema }), |
| | | onSubmit, |
| | | onError, |
| | | }) |
| | | |
| | | const onRemove = (id: string) => { |
| | | if (selectedEndpoint() === id) { |
| | | setSelectedEndpoint('') |
| | | } |
| | | |
| | | setEndpointList(endpointList().filter((e) => e.id !== id)) |
| | | } |
| | | |
| | | onMount(() => { |
| | | const query = new URLSearchParams(location.search) |
| | | |
| | | if (query.has('hostname')) { |
| | | void onSubmit({ |
| | | url: `${window.location.protocol}//${query.get('hostname')}${ |
| | | query.get('port') ? `:${query.get('port')}` : '' |
| | | }`, |
| | | secret: query.get('secret') ?? '', |
| | | }) |
| | | } else if (endpointList().length === 0) { |
| | | /** |
| | | we only try auto login when there is nothing in endpoint list |
| | | or user who is using default config won't be able to switch to another endpoint ever |
| | | */ |
| | | void onSubmit({ |
| | | url: 'http://127.0.0.1:9090', |
| | | secret: '', |
| | | }) |
| | | } |
| | | }) |
| | | |
| | | return ( |
| | | <div class="mx-auto flex max-w-screen-sm flex-col items-center gap-4 py-10"> |
| | | <form class="contents" use:form={form}> |
| | | <div class="flex w-full flex-col gap-4"> |
| | | <div class="flex-1"> |
| | | <label class="label"> |
| | | <span class="label-text">{t('endpointURL')}</span> |
| | | </label> |
| | | |
| | | <input |
| | | name="url" |
| | | type="url" |
| | | class="input input-bordered w-full" |
| | | placeholder="http://127.0.0.1:9090" |
| | | list="defaultEndpoints" |
| | | /> |
| | | |
| | | <datalist id="defaultEndpoints"> |
| | | <option value="http://127.0.0.1:9090" /> |
| | | </datalist> |
| | | </div> |
| | | |
| | | <div class="flex-1"> |
| | | <label class="label"> |
| | | <span class="label-text">{t('secret')}</span> |
| | | </label> |
| | | |
| | | <input |
| | | name="secret" |
| | | type="password" |
| | | class="input input-bordered w-full" |
| | | placeholder="secret" |
| | | /> |
| | | </div> |
| | | |
| | | <Button type="submit" class="btn-primary uppercase"> |
| | | {t('add')} |
| | | </Button> |
| | | </div> |
| | | </form> |
| | | |
| | | <div class="grid w-full grid-cols-2 gap-4"> |
| | | <For each={endpointList()}> |
| | | {({ id, url }) => ( |
| | | <div |
| | | class="badge badge-info flex w-full cursor-pointer items-center justify-between gap-4 py-4" |
| | | onClick={() => onEndpointSelect(id)} |
| | | > |
| | | <span class="truncate">{url}</span> |
| | | |
| | | <Button |
| | | class="btn-circle btn-ghost btn-xs text-white" |
| | | onClick={(e) => { |
| | | e.stopPropagation() |
| | | onRemove(id) |
| | | }} |
| | | > |
| | | <IconX /> |
| | | </Button> |
| | | </div> |
| | | )} |
| | | </For> |
| | | </div> |
| | | </div> |
| | | ) |
| | | } |
| New file |
| | |
| | | import { makePersisted } from '@solid-primitives/storage' |
| | | import { createSignal } from 'solid-js' |
| | | import { |
| | | CONNECTIONS_TABLE_INITIAL_COLUMN_ORDER, |
| | | CONNECTIONS_TABLE_INITIAL_COLUMN_VISIBILITY, |
| | | DEFAULT_LOGS_TABLE_MAX_ROWS, |
| | | LATENCY_QUALITY_MAP_HTTP, |
| | | LATENCY_QUALITY_MAP_HTTPS, |
| | | LOG_LEVEL, |
| | | PROXIES_ORDERING_TYPE, |
| | | PROXIES_PREVIEW_TYPE, |
| | | TAILWINDCSS_SIZE, |
| | | themes, |
| | | } from '~/constants' |
| | | import { |
| | | ConnectionsTableColumnOrder, |
| | | ConnectionsTableColumnVisibility, |
| | | } from '~/types' |
| | | |
| | | export const [proxiesPreviewType, setProxiesPreviewType] = makePersisted( |
| | | createSignal(PROXIES_PREVIEW_TYPE.Auto), |
| | | { name: 'proxiesPreviewType', storage: localStorage }, |
| | | ) |
| | | export const [proxiesOrderingType, setProxiesOrderingType] = makePersisted( |
| | | createSignal(PROXIES_ORDERING_TYPE.NATURAL), |
| | | { name: 'proxiesOrderingType', storage: localStorage }, |
| | | ) |
| | | |
| | | export const [hideUnAvailableProxies, setHideUnAvailableProxies] = |
| | | makePersisted(createSignal(false), { |
| | | name: 'hideUnAvailableProxies', |
| | | storage: localStorage, |
| | | }) |
| | | |
| | | export const [urlForLatencyTest, setUrlForLatencyTest] = makePersisted( |
| | | createSignal('https://www.gstatic.com/generate_204'), |
| | | { name: 'urlForLatencyTest', storage: localStorage }, |
| | | ) |
| | | export const [autoCloseConns, setAutoCloseConns] = makePersisted( |
| | | createSignal(false), |
| | | { name: 'autoCloseConns', storage: localStorage }, |
| | | ) |
| | | export const [useTwemoji, setUseTwemoji] = makePersisted(createSignal(true), { |
| | | name: 'useTwemoji', |
| | | storage: localStorage, |
| | | }) |
| | | export const [autoSwitchTheme, setAutoSwitchTheme] = makePersisted( |
| | | createSignal(false), |
| | | { name: 'autoSwitchTheme', storage: localStorage }, |
| | | ) |
| | | export const [favDayTheme, setFavDayTheme] = makePersisted( |
| | | createSignal<(typeof themes)[number]>('lofi'), |
| | | { name: 'favDayTheme', storage: localStorage }, |
| | | ) |
| | | export const [favNightTheme, setFavNightTheme] = makePersisted( |
| | | createSignal<(typeof themes)[number]>('business'), |
| | | { name: 'favNightTheme', storage: localStorage }, |
| | | ) |
| | | export const [connectionsTableSize, setConnectionsTableSize] = makePersisted( |
| | | createSignal<TAILWINDCSS_SIZE>(TAILWINDCSS_SIZE.XS), |
| | | { name: 'connectionsTableSize', storage: localStorage }, |
| | | ) |
| | | export const [ |
| | | connectionsTableColumnVisibility, |
| | | setConnectionsTableColumnVisibility, |
| | | ] = makePersisted( |
| | | createSignal<ConnectionsTableColumnVisibility>( |
| | | CONNECTIONS_TABLE_INITIAL_COLUMN_VISIBILITY, |
| | | ), |
| | | { |
| | | name: 'connectionsTableColumnVisibility', |
| | | storage: localStorage, |
| | | }, |
| | | ) |
| | | export const [connectionsTableColumnOrder, setConnectionsTableColumnOrder] = |
| | | makePersisted( |
| | | createSignal<ConnectionsTableColumnOrder>( |
| | | CONNECTIONS_TABLE_INITIAL_COLUMN_ORDER, |
| | | ), |
| | | { |
| | | name: 'connectionsTableColumnOrder', |
| | | storage: localStorage, |
| | | }, |
| | | ) |
| | | export const [clientSourceIPTags, setClientSourceIPTags] = makePersisted( |
| | | createSignal<{ tagName: string; sourceIP: string }[]>([]), |
| | | { |
| | | name: 'clientSourceIPTags', |
| | | storage: localStorage, |
| | | }, |
| | | ) |
| | | export const [logsTableSize, setLogsTableSize] = makePersisted( |
| | | createSignal<TAILWINDCSS_SIZE>(TAILWINDCSS_SIZE.XS), |
| | | { name: 'logsTableSize', storage: localStorage }, |
| | | ) |
| | | export const [logLevel, setLogLevel] = makePersisted( |
| | | createSignal<LOG_LEVEL>(LOG_LEVEL.Info), |
| | | { name: 'logLevel', storage: localStorage }, |
| | | ) |
| | | export const [logMaxRows, setLogMaxRows] = makePersisted( |
| | | createSignal(DEFAULT_LOGS_TABLE_MAX_ROWS), |
| | | { |
| | | name: 'logMaxRows', |
| | | storage: localStorage, |
| | | }, |
| | | ) |
| | | |
| | | export const tableSizeClassName = (size: TAILWINDCSS_SIZE) => { |
| | | let className = 'table-xs' |
| | | |
| | | switch (size) { |
| | | case TAILWINDCSS_SIZE.XS: |
| | | className = 'table-xs' |
| | | break |
| | | case TAILWINDCSS_SIZE.SM: |
| | | className = 'table-sm' |
| | | break |
| | | case TAILWINDCSS_SIZE.MD: |
| | | className = 'table-md' |
| | | break |
| | | case TAILWINDCSS_SIZE.LG: |
| | | className = 'table-lg' |
| | | break |
| | | } |
| | | |
| | | return className |
| | | } |
| | | |
| | | export const [latencyTestTimeoutDuration, setLatencyTestTimeoutDuration] = |
| | | makePersisted(createSignal(5000), { |
| | | name: 'latencyTestTimeoutDuration', |
| | | storage: localStorage, |
| | | }) |
| | | |
| | | export const isLatencyTestByHttps = () => |
| | | urlForLatencyTest().startsWith('https') |
| | | |
| | | export const latencyQualityMap = () => |
| | | isLatencyTestByHttps() ? LATENCY_QUALITY_MAP_HTTPS : LATENCY_QUALITY_MAP_HTTP |
| New file |
| | |
| | | import { differenceWith, isNumber, unionWith } from 'lodash' |
| | | import { createEffect, createSignal, untrack } from 'solid-js' |
| | | import { CONNECTIONS_TABLE_MAX_CLOSED_ROWS } from '~/constants' |
| | | import { Connection, ConnectionRawMessage } from '~/types' |
| | | |
| | | export type WsMsg = { |
| | | connections?: ConnectionRawMessage[] |
| | | uploadTotal: number |
| | | downloadTotal: number |
| | | } | null |
| | | |
| | | // we make connections global, so we can keep track of connections when user in proxy page |
| | | // when user selects proxy and close some connections they can back and check connections |
| | | // they closed |
| | | export const [allConnections, setAllConnections] = createSignal<Connection[]>( |
| | | [], |
| | | ) |
| | | export const [latestConnectionMsg, setLatestConnectionMsg] = |
| | | createSignal<WsMsg>(null) |
| | | |
| | | export const useConnections = () => { |
| | | const [closedConnections, setClosedConnections] = createSignal<Connection[]>( |
| | | [], |
| | | ) |
| | | const [activeConnections, setActiveConnections] = createSignal<Connection[]>( |
| | | [], |
| | | ) |
| | | const [paused, setPaused] = createSignal(false) |
| | | |
| | | createEffect(() => { |
| | | const rawConns = latestConnectionMsg()?.connections |
| | | |
| | | if (!rawConns) { |
| | | return |
| | | } |
| | | |
| | | untrack(() => { |
| | | const activeConns = restructRawMsgToConnection( |
| | | rawConns, |
| | | activeConnections(), |
| | | ) |
| | | |
| | | mergeAllConnections(activeConnections()) |
| | | |
| | | if (!paused()) { |
| | | const closedConns = diffClosedConnections(activeConns, allConnections()) |
| | | |
| | | setActiveConnections(activeConns) |
| | | setClosedConnections( |
| | | closedConns.slice(-CONNECTIONS_TABLE_MAX_CLOSED_ROWS), |
| | | ) |
| | | } |
| | | |
| | | setAllConnections((allConnections) => |
| | | allConnections.slice( |
| | | -(activeConns.length + CONNECTIONS_TABLE_MAX_CLOSED_ROWS), |
| | | ), |
| | | ) |
| | | }) |
| | | }) |
| | | |
| | | return { |
| | | closedConnections, |
| | | activeConnections, |
| | | paused, |
| | | setPaused, |
| | | } |
| | | } |
| | | |
| | | export const restructRawMsgToConnection = ( |
| | | connections: ConnectionRawMessage[], |
| | | prevActiveConnections: Connection[], |
| | | ): Connection[] => { |
| | | const prevMap = new Map<string, Connection>() |
| | | prevActiveConnections.forEach((prev) => prevMap.set(prev.id, prev)) |
| | | |
| | | return connections.map((connection) => { |
| | | const prevConn = prevMap.get(connection.id) |
| | | |
| | | if ( |
| | | !prevConn || |
| | | !isNumber(prevConn.download) || |
| | | !isNumber(prevConn.upload) |
| | | ) { |
| | | return { ...connection, downloadSpeed: 0, uploadSpeed: 0 } |
| | | } |
| | | |
| | | return { |
| | | ...connection, |
| | | downloadSpeed: connection.download - prevConn.download, |
| | | uploadSpeed: connection.upload - prevConn.upload, |
| | | } |
| | | }) |
| | | } |
| | | |
| | | export const mergeAllConnections = (activeConns: Connection[]) => { |
| | | setAllConnections((allConnections) => |
| | | unionWith(allConnections, activeConns, (a, b) => a.id === b.id), |
| | | ) |
| | | } |
| | | |
| | | const diffClosedConnections = ( |
| | | activeConns: Connection[], |
| | | allConns: Connection[], |
| | | ) => differenceWith(allConns, activeConns, (a, b) => a.id === b.id) |
| New file |
| | |
| | | import dayjs from 'dayjs' |
| | | import { locale } from '~/i18n' |
| | | |
| | | export const formatTimeFromNow = (time: number | string) => |
| | | dayjs(time).locale(locale()).fromNow() |
| New file |
| | |
| | | export * from './config' |
| | | export * from './connections' |
| | | export * from './global' |
| | | export * from './proxies' |
| | | export * from './request' |
| | | export * from './rules' |
| | | export * from './theme' |
| New file |
| | |
| | | import { batch, createSignal, untrack } from 'solid-js' |
| | | import { |
| | | closeSingleConnectionAPI, |
| | | fetchProxiesAPI, |
| | | fetchProxyProvidersAPI, |
| | | proxyGroupLatencyTestAPI, |
| | | proxyLatencyTestAPI, |
| | | proxyProviderHealthCheckAPI, |
| | | selectProxyInGroupAPI, |
| | | updateProxyProviderAPI, |
| | | } from '~/apis' |
| | | import { useStringBooleanMap } from '~/helpers' |
| | | import { |
| | | autoCloseConns, |
| | | latencyQualityMap, |
| | | latencyTestTimeoutDuration, |
| | | latestConnectionMsg, |
| | | restructRawMsgToConnection, |
| | | urlForLatencyTest, |
| | | } from '~/signals' |
| | | import type { Proxy, ProxyNode, ProxyProvider } from '~/types' |
| | | |
| | | type ProxyInfo = { |
| | | name: string |
| | | udp: boolean |
| | | now: string |
| | | xudp: boolean |
| | | type: string |
| | | provider: string |
| | | } |
| | | |
| | | export type ProxyWithProvider = Proxy & { provider?: string } |
| | | export type ProxyNodeWithProvider = ProxyNode & { provider?: string } |
| | | |
| | | const { map: collapsedMap, set: setCollapsedMap } = useStringBooleanMap() |
| | | const { |
| | | map: proxyLatencyTestingMap, |
| | | setWithCallback: setProxyLatencyTestingMap, |
| | | } = useStringBooleanMap() |
| | | const { |
| | | map: proxyGroupLatencyTestingMap, |
| | | setWithCallback: setProxyGroupLatencyTestingMap, |
| | | } = useStringBooleanMap() |
| | | const { |
| | | map: proxyProviderLatencyTestingMap, |
| | | setWithCallback: setProxyProviderLatencyTestingMap, |
| | | } = useStringBooleanMap() |
| | | const { map: updatingMap, setWithCallback: setUpdatingMap } = |
| | | useStringBooleanMap() |
| | | const [isAllProviderUpdating, setIsAllProviderUpdating] = createSignal(false) |
| | | |
| | | // these signals should be global state |
| | | const [proxies, setProxies] = createSignal<ProxyWithProvider[]>([]) |
| | | const [proxyProviders, setProxyProviders] = createSignal< |
| | | (ProxyProvider & { proxies: ProxyNodeWithProvider[] })[] |
| | | >([]) |
| | | |
| | | const [latencyMap, setLatencyMap] = createSignal<Record<string, number>>({}) |
| | | const [proxyNodeMap, setProxyNodeMap] = createSignal<Record<string, ProxyInfo>>( |
| | | {}, |
| | | ) |
| | | |
| | | const setProxiesInfo = ( |
| | | proxies: (ProxyWithProvider | ProxyNodeWithProvider)[], |
| | | ) => { |
| | | const newProxyNodeMap = { ...proxyNodeMap() } |
| | | const newLatencyMap = { ...latencyMap() } |
| | | |
| | | proxies.forEach((proxy) => { |
| | | const { udp, xudp, type, now, name, provider = '' } = proxy |
| | | |
| | | const latency = |
| | | proxy.history.at(-1)?.delay || latencyQualityMap().NOT_CONNECTED |
| | | |
| | | newProxyNodeMap[proxy.name] = { udp, xudp, type, now, name, provider } |
| | | newLatencyMap[proxy.name] = latency |
| | | }) |
| | | |
| | | batch(() => { |
| | | setProxyNodeMap(newProxyNodeMap) |
| | | setLatencyMap(newLatencyMap) |
| | | }) |
| | | } |
| | | |
| | | export const useProxies = () => { |
| | | const fetchProxies = async () => { |
| | | const [{ providers }, { proxies }] = await Promise.all([ |
| | | fetchProxyProvidersAPI(), |
| | | fetchProxiesAPI(), |
| | | ]) |
| | | |
| | | const sortIndex = [...(proxies['GLOBAL'].all ?? []), 'GLOBAL'] |
| | | const sortedProxies = Object.values(proxies) |
| | | .filter((proxy) => proxy.all?.length) |
| | | .sort( |
| | | (prev, next) => |
| | | sortIndex.indexOf(prev.name) - sortIndex.indexOf(next.name), |
| | | ) |
| | | const sortedProviders = Object.values(providers).filter( |
| | | (provider) => |
| | | provider.name !== 'default' && provider.vehicleType !== 'Compatible', |
| | | ) |
| | | |
| | | const allProxies = [ |
| | | ...Object.values(proxies), |
| | | ...sortedProviders.flatMap((provider) => |
| | | provider.proxies |
| | | .filter((proxy) => !(proxy.name in proxies)) |
| | | .map((proxy) => ({ |
| | | ...proxy, |
| | | provider: provider.name, |
| | | })), |
| | | ), |
| | | ] |
| | | |
| | | batch(() => { |
| | | setProxies(sortedProxies) |
| | | setProxyProviders(sortedProviders) |
| | | setProxiesInfo(allProxies) |
| | | }) |
| | | } |
| | | |
| | | const selectProxyInGroup = async (proxy: Proxy, proxyName: string) => { |
| | | await selectProxyInGroupAPI(proxy.name, proxyName) |
| | | await fetchProxies() |
| | | |
| | | if (autoCloseConns()) { |
| | | // we don't use activeConns from useConnection here for better performance, |
| | | // and we use empty array to restruct msg because they are closed, they won't have speed anyway |
| | | untrack(() => { |
| | | const activeConns = restructRawMsgToConnection( |
| | | latestConnectionMsg()?.connections ?? [], |
| | | [], |
| | | ) |
| | | |
| | | if (activeConns.length > 0) { |
| | | activeConns.forEach(({ id, chains }) => { |
| | | if (chains.includes(proxy.name)) { |
| | | closeSingleConnectionAPI(id) |
| | | } |
| | | }) |
| | | } |
| | | }) |
| | | } |
| | | } |
| | | |
| | | const proxyLatencyTest = (proxyName: string, provider: string) => |
| | | setProxyLatencyTestingMap(proxyName, async () => { |
| | | const { delay } = await proxyLatencyTestAPI( |
| | | proxyName, |
| | | provider, |
| | | urlForLatencyTest(), |
| | | latencyTestTimeoutDuration(), |
| | | ) |
| | | |
| | | setLatencyMap((latencyMap) => ({ |
| | | ...latencyMap, |
| | | [proxyName]: delay, |
| | | })) |
| | | }) |
| | | |
| | | const proxyGroupLatencyTest = (proxyGroupName: string) => |
| | | setProxyGroupLatencyTestingMap(proxyGroupName, async () => { |
| | | const newLatencyMap = await proxyGroupLatencyTestAPI( |
| | | proxyGroupName, |
| | | urlForLatencyTest(), |
| | | latencyTestTimeoutDuration(), |
| | | ) |
| | | |
| | | setLatencyMap((latencyMap) => ({ |
| | | ...latencyMap, |
| | | ...newLatencyMap, |
| | | })) |
| | | |
| | | await fetchProxies() |
| | | }) |
| | | |
| | | const updateProviderByProviderName = (providerName: string) => |
| | | setUpdatingMap(providerName, async () => { |
| | | try { |
| | | await updateProxyProviderAPI(providerName) |
| | | } catch {} |
| | | await fetchProxies() |
| | | }) |
| | | |
| | | const updateAllProvider = async () => { |
| | | setIsAllProviderUpdating(true) |
| | | try { |
| | | await Promise.allSettled( |
| | | proxyProviders().map((provider) => |
| | | updateProxyProviderAPI(provider.name), |
| | | ), |
| | | ) |
| | | await fetchProxies() |
| | | } finally { |
| | | setIsAllProviderUpdating(false) |
| | | } |
| | | } |
| | | |
| | | const proxyProviderLatencyTest = (providerName: string) => |
| | | setProxyProviderLatencyTestingMap(providerName, async () => { |
| | | await proxyProviderHealthCheckAPI(providerName) |
| | | await fetchProxies() |
| | | }) |
| | | |
| | | return { |
| | | collapsedMap, |
| | | setCollapsedMap, |
| | | proxyLatencyTestingMap, |
| | | proxyGroupLatencyTestingMap, |
| | | proxyProviderLatencyTestingMap, |
| | | updatingMap, |
| | | isAllProviderUpdating, |
| | | proxies, |
| | | proxyProviders, |
| | | proxyLatencyTest, |
| | | proxyGroupLatencyTest, |
| | | latencyMap, |
| | | proxyNodeMap, |
| | | fetchProxies, |
| | | selectProxyInGroup, |
| | | updateProviderByProviderName, |
| | | updateAllProvider, |
| | | proxyProviderLatencyTest, |
| | | } |
| | | } |
| New file |
| | |
| | | import { createEventSignal } from '@solid-primitives/event-listener' |
| | | import { makePersisted } from '@solid-primitives/storage' |
| | | import { createReconnectingWS } from '@solid-primitives/websocket' |
| | | import ky from 'ky' |
| | | import { createMemo, createSignal } from 'solid-js' |
| | | |
| | | export const [selectedEndpoint, setSelectedEndpoint] = makePersisted( |
| | | createSignal(''), |
| | | { |
| | | name: 'selectedEndpoint', |
| | | storage: localStorage, |
| | | }, |
| | | ) |
| | | |
| | | export const [endpointList, setEndpointList] = makePersisted( |
| | | createSignal< |
| | | { |
| | | id: string |
| | | url: string |
| | | secret: string |
| | | }[] |
| | | >([]), |
| | | { name: 'endpointList', storage: localStorage }, |
| | | ) |
| | | |
| | | export const useRequest = () => { |
| | | const e = endpoint() |
| | | |
| | | if (!e) { |
| | | return ky.create({}) |
| | | } |
| | | |
| | | const headers = new Headers() |
| | | |
| | | if (e.secret) { |
| | | headers.set('Authorization', `Bearer ${e.secret}`) |
| | | } |
| | | |
| | | return ky.create({ |
| | | prefixUrl: e.url, |
| | | headers, |
| | | }) |
| | | } |
| | | |
| | | export const endpoint = () => |
| | | endpointList().find(({ id }) => id === selectedEndpoint()) |
| | | |
| | | export const secret = () => endpoint()?.secret |
| | | |
| | | export const wsEndpointURL = () => |
| | | new URL(endpoint()?.url ?? '').origin.replace('http', 'ws') |
| | | |
| | | export const useWsRequest = <T>( |
| | | path: string, |
| | | queries: Record<string, string> = {}, |
| | | ) => { |
| | | const queryParams = new URLSearchParams(queries) |
| | | queryParams.set('token', secret() ?? '') |
| | | |
| | | const ws = createReconnectingWS( |
| | | `${wsEndpointURL()}/${path}?${queryParams.toString()}`, |
| | | ) |
| | | |
| | | const event = createEventSignal<{ |
| | | message: MessageEvent |
| | | }>(ws, 'message') |
| | | |
| | | return createMemo<T | null>(() => { |
| | | const e = event() |
| | | |
| | | if (!e) { |
| | | return null |
| | | } |
| | | |
| | | return JSON.parse(event()?.data) |
| | | }) |
| | | } |
| New file |
| | |
| | | import { createSignal } from 'solid-js' |
| | | import { |
| | | fetchRuleProvidersAPI, |
| | | fetchRulesAPI, |
| | | updateRuleProviderAPI, |
| | | } from '~/apis' |
| | | import type { Rule, RuleProvider } from '~/types' |
| | | |
| | | export const useRules = () => { |
| | | const [rules, setRules] = createSignal<Rule[]>([]) |
| | | const [ruleProviders, setRuleProviders] = createSignal<RuleProvider[]>([]) |
| | | |
| | | const updateRules = async () => { |
| | | const [{ rules }, { providers }] = await Promise.all([ |
| | | fetchRulesAPI(), |
| | | fetchRuleProvidersAPI(), |
| | | ]) |
| | | |
| | | setRules(Object.values(rules)) |
| | | setRuleProviders(Object.values(providers)) |
| | | } |
| | | |
| | | const updateAllRuleProvider = async () => { |
| | | await Promise.all( |
| | | ruleProviders().map((provider) => updateRuleProviderAPI(provider.name)), |
| | | ) |
| | | await updateRules() |
| | | } |
| | | |
| | | const updateRuleProviderByName = async (providerName: string) => { |
| | | await updateRuleProviderAPI(providerName) |
| | | await updateRules() |
| | | } |
| | | |
| | | return { |
| | | rules, |
| | | ruleProviders, |
| | | updateRules, |
| | | updateAllRuleProvider, |
| | | updateRuleProviderByName, |
| | | } |
| | | } |
| New file |
| | |
| | | import { makePersisted } from '@solid-primitives/storage' |
| | | import { createSignal } from 'solid-js' |
| | | import { themes } from '~/constants' |
| | | |
| | | export const [curTheme, setCurTheme] = makePersisted( |
| | | createSignal<(typeof themes)[number]>('halloween'), |
| | | { name: 'theme', storage: localStorage }, |
| | | ) |
| New file |
| | |
| | | import { CONNECTIONS_TABLE_ACCESSOR_KEY, LOG_LEVEL } from '~/constants' |
| | | |
| | | declare module 'solid-js' { |
| | | namespace JSX { |
| | | interface Directives { |
| | | [name: string]: unknown |
| | | } |
| | | } |
| | | } |
| | | |
| | | export type Proxy = { |
| | | name: string |
| | | type: string |
| | | all?: string[] |
| | | extra: Record<string, unknown> |
| | | history: { |
| | | time: string |
| | | delay: number |
| | | }[] |
| | | udp: boolean |
| | | xudp: boolean |
| | | tfo: boolean |
| | | now: string |
| | | } |
| | | |
| | | export type ProxyNode = { |
| | | alive: boolean |
| | | type: string |
| | | name: string |
| | | tfo: boolean |
| | | udp: boolean |
| | | xudp: boolean |
| | | now: string |
| | | id: string |
| | | extra: Record<string, unknown> |
| | | history: { |
| | | time: string |
| | | delay: number |
| | | }[] |
| | | } |
| | | |
| | | export type SubscriptionInfo = { |
| | | Download?: number |
| | | Upload?: number |
| | | Total?: number |
| | | Expire?: number |
| | | } |
| | | |
| | | export type ProxyProvider = { |
| | | subscriptionInfo?: SubscriptionInfo |
| | | name: string |
| | | proxies: ProxyNode[] |
| | | testUrl: string |
| | | updatedAt: string |
| | | vehicleType: string |
| | | } |
| | | |
| | | export type Rule = { |
| | | type: string |
| | | payload: string |
| | | proxy: string |
| | | size: number |
| | | } |
| | | |
| | | export type RuleProvider = { |
| | | behavior: string |
| | | format: string |
| | | name: string |
| | | ruleCount: number |
| | | type: string |
| | | updatedAt: string |
| | | vehicleType: string |
| | | } |
| | | |
| | | export type ConnectionRawMessage = { |
| | | id: string |
| | | download: number |
| | | upload: number |
| | | chains: string[] |
| | | rule: string |
| | | rulePayload: string |
| | | start: string |
| | | metadata: { |
| | | network: string |
| | | type: string |
| | | destinationIP: string |
| | | destinationPort: string |
| | | dnsMode: string |
| | | host: string |
| | | inboundIP: string |
| | | inboundName: string |
| | | inboundPort: string |
| | | inboundUser: string |
| | | process: string |
| | | processPath: string |
| | | remoteDestination: string |
| | | sniffHost: string |
| | | sourceIP: string |
| | | sourcePort: string |
| | | specialProxy: string |
| | | specialRules: string |
| | | uid: number |
| | | } |
| | | } |
| | | |
| | | export type Connection = ConnectionRawMessage & { |
| | | downloadSpeed: number |
| | | uploadSpeed: number |
| | | } |
| | | |
| | | export type Log = { |
| | | type: LOG_LEVEL |
| | | payload: string |
| | | } |
| | | |
| | | export type Config = { |
| | | mode: 'global' | 'rule' | 'direct' |
| | | port: number |
| | | 'socks-port': number |
| | | 'redir-port': number |
| | | 'tproxy-port': number |
| | | 'mixed-port': number |
| | | tun: { |
| | | enable: boolean |
| | | device: string |
| | | stack: string |
| | | 'dns-hijack': null |
| | | 'auto-route': boolean |
| | | 'auto-detect-interface': boolean |
| | | 'file-descriptor': number |
| | | } |
| | | 'tuic-server': { |
| | | enable: boolean |
| | | listen: string |
| | | certificate: string |
| | | 'private-key': string |
| | | } |
| | | 'ss-config': string |
| | | 'vmess-config': string |
| | | authentication: null |
| | | 'allow-lan': boolean |
| | | 'bind-address': string |
| | | 'inbound-tfo': boolean |
| | | UnifiedDelay: boolean |
| | | 'log-level': string |
| | | ipv6: boolean |
| | | 'interface-name': string |
| | | 'geodata-mode': boolean |
| | | 'geodata-loader': string |
| | | 'tcp-concurrent': boolean |
| | | 'find-process-mode': string |
| | | sniffing: boolean |
| | | 'global-client-fingerprint': boolean |
| | | } |
| | | |
| | | export type DNSQuery = { |
| | | AD: boolean |
| | | CD: boolean |
| | | RA: boolean |
| | | RD: boolean |
| | | TC: boolean |
| | | status: number |
| | | Question: { |
| | | Name: string |
| | | Qtype: number |
| | | Qclass: number |
| | | }[] |
| | | Answer?: { |
| | | TTL: number |
| | | data: string |
| | | name: string |
| | | type: number |
| | | }[] |
| | | } |
| | | |
| | | export type BackendVersion = { |
| | | meta: boolean |
| | | version: string |
| | | } |
| | | |
| | | export type ConnectionsTableColumnVisibility = Partial< |
| | | Record<CONNECTIONS_TABLE_ACCESSOR_KEY, boolean> |
| | | > |
| | | export type ConnectionsTableColumnOrder = CONNECTIONS_TABLE_ACCESSOR_KEY[] |
| New file |
| | |
| | | interface ImportMetaEnv { |
| | | readonly version: string |
| | | } |
| | | |
| | | interface ImportMeta { |
| | | readonly env: ImportMetaEnv |
| | | } |
| New file |
| | |
| | | import daisyui from 'daisyui' |
| | | import { Config } from 'tailwindcss' |
| | | import safeArea from 'tailwindcss-safe-area' |
| | | |
| | | export default { |
| | | content: ['src/**/*.{css,ts,tsx}'], |
| | | plugins: [daisyui, safeArea], |
| | | daisyui: { themes: true }, |
| | | theme: { |
| | | fontFamily: { |
| | | twemoji: ['Fira Sans', 'Twemoji Mozilla', 'system-ui', 'monospace'], |
| | | 'no-twemoji': ['Fira Sans', 'system-ui', 'monospace'], |
| | | }, |
| | | }, |
| | | } as Config |
| New file |
| | |
| | | { |
| | | "compilerOptions": { |
| | | "strict": true, |
| | | "skipLibCheck": true, |
| | | "target": "ESNext", |
| | | "module": "ESNext", |
| | | "moduleResolution": "node", |
| | | "resolveJsonModule": true, |
| | | "allowSyntheticDefaultImports": true, |
| | | "esModuleInterop": true, |
| | | "jsx": "preserve", |
| | | "jsxImportSource": "solid-js", |
| | | "types": ["vite/client"], |
| | | "noEmit": true, |
| | | "isolatedModules": true, |
| | | "paths": { |
| | | "~/*": ["./src/*"] |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | declare module 'tailwindcss-safe-area' |
| New file |
| | |
| | | import { defineConfig, splitVendorChunkPlugin } from 'vite' |
| | | import { VitePWA } from 'vite-plugin-pwa' |
| | | import solidPlugin from 'vite-plugin-solid' |
| | | import pkgJSON from './package.json' |
| | | |
| | | export default defineConfig({ |
| | | base: './', |
| | | build: { chunkSizeWarningLimit: 1000 }, |
| | | resolve: { alias: { '~': '/src' } }, |
| | | define: { |
| | | 'import.meta.env.version': JSON.stringify(pkgJSON.version), |
| | | }, |
| | | plugins: [ |
| | | solidPlugin(), |
| | | |
| | | splitVendorChunkPlugin(), |
| | | |
| | | VitePWA({ |
| | | registerType: 'autoUpdate', |
| | | includeAssets: ['favicon.svg'], |
| | | manifest: { |
| | | icons: [ |
| | | { |
| | | src: 'pwa-192x192.png', |
| | | sizes: '192x192', |
| | | type: 'image/png', |
| | | }, |
| | | { |
| | | src: '/pwa-512x512.png', |
| | | sizes: '512x512', |
| | | type: 'image/png', |
| | | }, |
| | | { |
| | | src: 'pwa-512x512.png', |
| | | sizes: '512x512', |
| | | type: 'image/png', |
| | | purpose: 'any maskable', |
| | | }, |
| | | ], |
| | | }, |
| | | }), |
| | | ], |
| | | }) |