mirror of
https://github.com/bolucat/Archive.git
synced 2025-09-26 20:21:35 +08:00
Update On Fri Jul 26 20:36:04 CEST 2024
This commit is contained in:
1
.github/update.log
vendored
1
.github/update.log
vendored
@@ -714,3 +714,4 @@ Update On Mon Jul 22 20:31:05 CEST 2024
|
||||
Update On Tue Jul 23 20:31:31 CEST 2024
|
||||
Update On Wed Jul 24 20:33:56 CEST 2024
|
||||
Update On Thu Jul 25 20:31:26 CEST 2024
|
||||
Update On Fri Jul 26 20:35:53 CEST 2024
|
||||
|
127
clash-meta/component/trie/domain_set_bin.go
Normal file
127
clash-meta/component/trie/domain_set_bin.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package trie
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
func (ss *DomainSet) WriteBin(w io.Writer, count int64) (err error) {
|
||||
// version
|
||||
_, err = w.Write([]byte{1})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// count
|
||||
err = binary.Write(w, binary.BigEndian, count)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// leaves
|
||||
err = binary.Write(w, binary.BigEndian, int64(len(ss.leaves)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, d := range ss.leaves {
|
||||
err = binary.Write(w, binary.BigEndian, d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// labelBitmap
|
||||
err = binary.Write(w, binary.BigEndian, int64(len(ss.labelBitmap)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, d := range ss.labelBitmap {
|
||||
err = binary.Write(w, binary.BigEndian, d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// labels
|
||||
err = binary.Write(w, binary.BigEndian, int64(len(ss.labels)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(ss.labels)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReadDomainSetBin(r io.Reader) (ds *DomainSet, count int64, err error) {
|
||||
// version
|
||||
version := make([]byte, 1)
|
||||
_, err = io.ReadFull(r, version)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if version[0] != 1 {
|
||||
return nil, 0, errors.New("version is invalid")
|
||||
}
|
||||
|
||||
// count
|
||||
err = binary.Read(r, binary.BigEndian, &count)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
ds = &DomainSet{}
|
||||
var length int64
|
||||
|
||||
// leaves
|
||||
err = binary.Read(r, binary.BigEndian, &length)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if length < 1 {
|
||||
return nil, 0, errors.New("length is invalid")
|
||||
}
|
||||
ds.leaves = make([]uint64, length)
|
||||
for i := int64(0); i < length; i++ {
|
||||
err = binary.Read(r, binary.BigEndian, &ds.leaves[i])
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
}
|
||||
|
||||
// labelBitmap
|
||||
err = binary.Read(r, binary.BigEndian, &length)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if length < 1 {
|
||||
return nil, 0, errors.New("length is invalid")
|
||||
}
|
||||
ds.labelBitmap = make([]uint64, length)
|
||||
for i := int64(0); i < length; i++ {
|
||||
err = binary.Read(r, binary.BigEndian, &ds.labelBitmap[i])
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
}
|
||||
|
||||
// labels
|
||||
err = binary.Read(r, binary.BigEndian, &length)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if length < 1 {
|
||||
return nil, 0, errors.New("length is invalid")
|
||||
}
|
||||
ds.labels = make([]byte, length)
|
||||
_, err = io.ReadFull(r, ds.labels)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
ds.init()
|
||||
return ds, count, nil
|
||||
}
|
@@ -42,6 +42,7 @@ import (
|
||||
T "github.com/metacubex/mihomo/tunnel"
|
||||
|
||||
orderedmap "github.com/wk8/go-ordered-map/v2"
|
||||
"golang.org/x/exp/slices"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@@ -792,6 +793,9 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
|
||||
AllProviders = append(AllProviders, name)
|
||||
}
|
||||
|
||||
slices.Sort(AllProxies)
|
||||
slices.Sort(AllProviders)
|
||||
|
||||
// parse proxy group
|
||||
for idx, mapping := range groupsConfig {
|
||||
group, err := outboundgroup.ParseProxyGroup(mapping, proxies, providersMap, AllProxies, AllProviders)
|
||||
|
@@ -1,6 +1,8 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/metacubex/mihomo/common/utils"
|
||||
"github.com/metacubex/mihomo/constant"
|
||||
)
|
||||
@@ -110,9 +112,24 @@ func (rt RuleBehavior) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
func ParseBehavior(s string) (behavior RuleBehavior, err error) {
|
||||
switch s {
|
||||
case "domain":
|
||||
behavior = Domain
|
||||
case "ipcidr":
|
||||
behavior = IPCIDR
|
||||
case "classical":
|
||||
behavior = Classical
|
||||
default:
|
||||
err = fmt.Errorf("unsupported behavior type: %s", s)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const (
|
||||
YamlRule RuleFormat = iota
|
||||
TextRule
|
||||
MrsRule
|
||||
)
|
||||
|
||||
type RuleFormat int
|
||||
@@ -123,11 +140,27 @@ func (rf RuleFormat) String() string {
|
||||
return "YamlRule"
|
||||
case TextRule:
|
||||
return "TextRule"
|
||||
case MrsRule:
|
||||
return "MrsRule"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func ParseRuleFormat(s string) (format RuleFormat, err error) {
|
||||
switch s {
|
||||
case "", "yaml":
|
||||
format = YamlRule
|
||||
case "text":
|
||||
format = TextRule
|
||||
case "mrs":
|
||||
format = MrsRule
|
||||
default:
|
||||
err = fmt.Errorf("unsupported format type: %s", s)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type Tunnel interface {
|
||||
Providers() map[string]ProxyProvider
|
||||
RuleProviders() map[string]RuleProvider
|
||||
|
@@ -942,6 +942,12 @@ rule-providers:
|
||||
interval: 259200
|
||||
path: /path/to/save/file.yaml
|
||||
type: file
|
||||
rule3: # mrs类型ruleset,目前仅支持domain,可以通过“mihomo convert-ruleset domain yaml XXX.yaml XXX.mrs”转换得到
|
||||
type: http
|
||||
url: "url"
|
||||
format: mrs
|
||||
behavior: domain
|
||||
path: /path/to/save/file.mrs
|
||||
rules:
|
||||
- RULE-SET,rule1,REJECT
|
||||
- IP-ASN,1,PROXY
|
||||
|
@@ -14,6 +14,7 @@ require (
|
||||
github.com/gobwas/ws v1.4.0
|
||||
github.com/gofrs/uuid/v5 v5.2.0
|
||||
github.com/insomniacslk/dhcp v0.0.0-20240529192340-51bc6136a0a6
|
||||
github.com/klauspost/compress v1.17.9
|
||||
github.com/klauspost/cpuid/v2 v2.2.8
|
||||
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40
|
||||
github.com/mdlayher/netlink v1.7.2
|
||||
@@ -82,7 +83,6 @@ require (
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
|
||||
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||
github.com/josharian/native v1.1.0 // indirect
|
||||
github.com/klauspost/compress v1.17.4 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mdlayher/socket v0.4.1 // indirect
|
||||
|
@@ -81,8 +81,8 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF
|
||||
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
||||
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
|
@@ -33,10 +33,20 @@ func (l *Listener) Close() error {
|
||||
}
|
||||
|
||||
func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) {
|
||||
return NewWithAuthenticate(addr, tunnel, authStore.Authenticator(), additions...)
|
||||
return NewWithAuthenticator(addr, tunnel, authStore.Authenticator(), additions...)
|
||||
}
|
||||
|
||||
func NewWithAuthenticate(addr string, tunnel C.Tunnel, authenticator auth.Authenticator, additions ...inbound.Addition) (*Listener, error) {
|
||||
// NewWithAuthenticate
|
||||
// never change type traits because it's used in CFMA
|
||||
func NewWithAuthenticate(addr string, tunnel C.Tunnel, authenticate bool, additions ...inbound.Addition) (*Listener, error) {
|
||||
authenticator := authStore.Authenticator()
|
||||
if !authenticate {
|
||||
authenticator = nil
|
||||
}
|
||||
return NewWithAuthenticator(addr, tunnel, authenticator, additions...)
|
||||
}
|
||||
|
||||
func NewWithAuthenticator(addr string, tunnel C.Tunnel, authenticator auth.Authenticator, additions ...inbound.Addition) (*Listener, error) {
|
||||
isDefault := false
|
||||
if len(additions) == 0 {
|
||||
isDefault = true
|
||||
|
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/metacubex/mihomo/hub"
|
||||
"github.com/metacubex/mihomo/hub/executor"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
"github.com/metacubex/mihomo/rules/provider"
|
||||
|
||||
"go.uber.org/automaxprocs/maxprocs"
|
||||
)
|
||||
@@ -48,6 +49,12 @@ func init() {
|
||||
|
||||
func main() {
|
||||
_, _ = maxprocs.Set(maxprocs.Logger(func(string, ...any) {}))
|
||||
|
||||
if len(os.Args) > 1 && os.Args[1] == "convert-ruleset" {
|
||||
provider.ConvertMain(os.Args[2:])
|
||||
return
|
||||
}
|
||||
|
||||
if version {
|
||||
fmt.Printf("Mihomo Meta %s %s %s with %s %s\n",
|
||||
C.Version, runtime.GOOS, runtime.GOARCH, runtime.Version(), C.BuildTime)
|
||||
|
@@ -1,6 +1,9 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/metacubex/mihomo/component/trie"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
@@ -48,6 +51,25 @@ func (d *domainStrategy) FinishInsert() {
|
||||
d.domainTrie = nil
|
||||
}
|
||||
|
||||
func (d *domainStrategy) FromMrs(r io.Reader) error {
|
||||
domainSet, count, err := trie.ReadDomainSetBin(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.count = int(count)
|
||||
d.domainSet = domainSet
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *domainStrategy) WriteMrs(w io.Writer) error {
|
||||
if d.domainSet == nil {
|
||||
return errors.New("nil domainSet")
|
||||
}
|
||||
return d.domainSet.WriteBin(w, int64(d.count))
|
||||
}
|
||||
|
||||
var _ mrsRuleStrategy = (*domainStrategy)(nil)
|
||||
|
||||
func NewDomainStrategy() *domainStrategy {
|
||||
return &domainStrategy{}
|
||||
}
|
||||
|
71
clash-meta/rules/provider/mrs_converter.go
Normal file
71
clash-meta/rules/provider/mrs_converter.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
P "github.com/metacubex/mihomo/constant/provider"
|
||||
|
||||
"github.com/klauspost/compress/zstd"
|
||||
)
|
||||
|
||||
func ConvertToMrs(buf []byte, behavior P.RuleBehavior, format P.RuleFormat, w io.Writer) (err error) {
|
||||
strategy := newStrategy(behavior, nil)
|
||||
strategy, err = rulesParse(buf, strategy, format)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _strategy, ok := strategy.(mrsRuleStrategy); ok {
|
||||
var encoder *zstd.Encoder
|
||||
encoder, err = zstd.NewWriter(w)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
zstdErr := encoder.Close()
|
||||
if err == nil {
|
||||
err = zstdErr
|
||||
}
|
||||
}()
|
||||
return _strategy.WriteMrs(encoder)
|
||||
} else {
|
||||
return ErrInvalidFormat
|
||||
}
|
||||
}
|
||||
|
||||
func ConvertMain(args []string) {
|
||||
if len(args) > 3 {
|
||||
behavior, err := P.ParseBehavior(args[0])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
format, err := P.ParseRuleFormat(args[1])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
source := args[2]
|
||||
target := args[3]
|
||||
|
||||
sourceFile, err := os.ReadFile(source)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
targetFile, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = ConvertToMrs(sourceFile, behavior, format, targetFile)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = targetFile.Close()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
panic("Usage: convert-ruleset <behavior> <format> <source file> <target file>")
|
||||
}
|
||||
}
|
@@ -32,28 +32,13 @@ func ParseRuleProvider(name string, mapping map[string]interface{}, parse func(t
|
||||
if err := decoder.Decode(mapping, schema); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var behavior P.RuleBehavior
|
||||
|
||||
switch schema.Behavior {
|
||||
case "domain":
|
||||
behavior = P.Domain
|
||||
case "ipcidr":
|
||||
behavior = P.IPCIDR
|
||||
case "classical":
|
||||
behavior = P.Classical
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported behavior type: %s", schema.Behavior)
|
||||
behavior, err := P.ParseBehavior(schema.Behavior)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var format P.RuleFormat
|
||||
|
||||
switch schema.Format {
|
||||
case "", "yaml":
|
||||
format = P.YamlRule
|
||||
case "text":
|
||||
format = P.TextRule
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported format type: %s", schema.Format)
|
||||
format, err := P.ParseRuleFormat(schema.Format)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var vehicle P.Vehicle
|
||||
|
@@ -4,16 +4,18 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/metacubex/mihomo/common/pool"
|
||||
"github.com/metacubex/mihomo/component/resource"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
P "github.com/metacubex/mihomo/constant/provider"
|
||||
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var tunnel P.Tunnel
|
||||
@@ -52,6 +54,12 @@ type ruleStrategy interface {
|
||||
FinishInsert()
|
||||
}
|
||||
|
||||
type mrsRuleStrategy interface {
|
||||
ruleStrategy
|
||||
FromMrs(r io.Reader) error
|
||||
WriteMrs(w io.Writer) error
|
||||
}
|
||||
|
||||
func (rp *ruleSetProvider) Type() P.ProviderType {
|
||||
return P.Rule
|
||||
}
|
||||
@@ -152,9 +160,23 @@ func newStrategy(behavior P.RuleBehavior, parse func(tp, payload, target string,
|
||||
}
|
||||
|
||||
var ErrNoPayload = errors.New("file must have a `payload` field")
|
||||
var ErrInvalidFormat = errors.New("invalid format")
|
||||
|
||||
func rulesParse(buf []byte, strategy ruleStrategy, format P.RuleFormat) (ruleStrategy, error) {
|
||||
strategy.Reset()
|
||||
if format == P.MrsRule {
|
||||
if _strategy, ok := strategy.(mrsRuleStrategy); ok {
|
||||
reader, err := zstd.NewReader(bytes.NewReader(buf))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer reader.Close()
|
||||
err = _strategy.FromMrs(reader)
|
||||
return strategy, err
|
||||
} else {
|
||||
return nil, ErrInvalidFormat
|
||||
}
|
||||
}
|
||||
|
||||
schema := &RulePayload{}
|
||||
|
||||
@@ -228,6 +250,8 @@ func rulesParse(buf []byte, strategy ruleStrategy, format P.RuleFormat) (ruleStr
|
||||
if len(schema.Payload) > 0 {
|
||||
str = schema.Payload[0]
|
||||
}
|
||||
default:
|
||||
return nil, ErrInvalidFormat
|
||||
}
|
||||
|
||||
if str == "" {
|
||||
|
@@ -1,425 +0,0 @@
|
||||
// modified from https://github.com/lerna-lite/lerna-lite/blob//v1.9.0/packages/core/src/conventional-commits/get-github-commits.ts
|
||||
// ref: https://github.com/conventional-changelog/conventional-changelog/issues/349#issuecomment-1200070203
|
||||
"use strict";
|
||||
const conventionalChangelogConfig = require("conventional-changelog-conventionalcommits");
|
||||
const github = require("@actions/github");
|
||||
const { execSync, spawnSync } = require("node:child_process");
|
||||
const dedent = require("dedent");
|
||||
|
||||
const GIT_COMMIT_WITH_AUTHOR_FORMAT =
|
||||
"%B%n-hash-%n%H%n-gitTags-%n%d%n-committerDate-%n%ci%n-authorName-%n%an%n-authorEmail-%n%ae%n-gpgStatus-%n%G?%n-gpgSigner-%n%GS";
|
||||
|
||||
const extraCommitMsg = `by @{{userLogin}}`;
|
||||
|
||||
const QUERY_PAGE_SIZE = 100;
|
||||
|
||||
function writerOptsTransform(
|
||||
originalTransform,
|
||||
commitsSinceLastRelease,
|
||||
commit,
|
||||
context,
|
||||
) {
|
||||
// execute original writerOpts transform
|
||||
const extendedCommit = originalTransform(commit, context);
|
||||
|
||||
// then add client remote detail (login) to the commit object and return it
|
||||
if (extendedCommit) {
|
||||
// search current commit with the commits since last release array returned from fetching GitHub API
|
||||
const remoteCommit = commitsSinceLastRelease.find(
|
||||
(c) => c.shortHash === commit.shortHash,
|
||||
);
|
||||
if (remoteCommit?.login) {
|
||||
commit.userLogin = remoteCommit.login;
|
||||
}
|
||||
}
|
||||
|
||||
return extendedCommit;
|
||||
}
|
||||
|
||||
function trimChars(str, cutset) {
|
||||
let start = 0,
|
||||
end = str.length;
|
||||
|
||||
while (start < end && cutset.indexOf(str[start]) >= 0) ++start;
|
||||
|
||||
while (end > start && cutset.indexOf(str[end - 1]) >= 0) --end;
|
||||
|
||||
return start > 0 || end < str.length ? str.substring(start, end) : str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse git output and return relevant metadata.
|
||||
* @param {string} stdout Result of `git describe`
|
||||
* @param {string} [cwd] Defaults to `process.cwd()`
|
||||
* @param [separator] Separator used within independent version tags, defaults to @
|
||||
* @returns {DescribeRefFallbackResult|DescribeRefDetailedResult}
|
||||
*/
|
||||
function parse(stdout, cwd, separator) {
|
||||
separator = separator || "@";
|
||||
const minimalShaRegex = /^([0-9a-f]{7,40})(-dirty)?$/;
|
||||
// when git describe fails to locate tags, it returns only the minimal sha
|
||||
if (minimalShaRegex.test(stdout)) {
|
||||
// repo might still be dirty
|
||||
const [, sha, isDirty] = minimalShaRegex.exec(stdout);
|
||||
|
||||
// count number of commits since beginning of time
|
||||
const refCount = trimChars(
|
||||
spawnSync("git", ["rev-list", "--count", sha], { cwd }).stdout.toString(),
|
||||
"\n \r",
|
||||
);
|
||||
|
||||
return { refCount, sha, isDirty: Boolean(isDirty) };
|
||||
}
|
||||
|
||||
// If the user has specified a custom separator, it may not be regex-safe, so escape it
|
||||
const escapedSeparator = separator.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const regexPattern = new RegExp(
|
||||
`^((?:.*${escapedSeparator})?(.*))-(\\d+)-g([0-9a-f]+)(-dirty)?$`,
|
||||
);
|
||||
|
||||
const [, lastTagName, lastVersion, refCount, sha, isDirty] =
|
||||
regexPattern.exec(stdout) || [];
|
||||
|
||||
return { lastTagName, lastVersion, refCount, sha, isDirty: Boolean(isDirty) };
|
||||
}
|
||||
|
||||
function getArgs(options, includeMergedTags) {
|
||||
let args = [
|
||||
"describe",
|
||||
// fallback to short sha if no tags located
|
||||
"--always",
|
||||
// always return full result, helps identify existing release
|
||||
"--long",
|
||||
// annotate if uncommitted changes present
|
||||
"--dirty",
|
||||
// prefer tags originating on upstream branch
|
||||
"--first-parent",
|
||||
];
|
||||
|
||||
if (options.match) {
|
||||
args.push("--match", options.match);
|
||||
}
|
||||
|
||||
if (includeMergedTags) {
|
||||
// we want to consider all tags, also from merged branches
|
||||
args = args.filter((arg) => arg !== "--first-parent");
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
function describeRefSync(options = {}, includeMergedTags, dryRun = false) {
|
||||
console.error(
|
||||
"git",
|
||||
"describeRefSync",
|
||||
getArgs(options, includeMergedTags),
|
||||
// options,
|
||||
// dryRun,
|
||||
);
|
||||
const stdout = trimChars(
|
||||
spawnSync(
|
||||
"git",
|
||||
getArgs(options, includeMergedTags),
|
||||
// options,
|
||||
// dryRun,
|
||||
).stdout.toString("utf8"),
|
||||
"\n \r",
|
||||
);
|
||||
const result = parse(stdout, options.cwd, options.separator);
|
||||
|
||||
if (options?.match) {
|
||||
console.error("git-describe.sync", "%j => %j", options?.match, stdout);
|
||||
}
|
||||
if (stdout) {
|
||||
console.log(stdout);
|
||||
console.error("git-describe", "parsed => %j", result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getOldestCommitSinceLastTag(
|
||||
execOpts,
|
||||
isIndependent,
|
||||
includeMergedTags,
|
||||
) {
|
||||
let commitResult = "";
|
||||
const describeOptions = { ...execOpts };
|
||||
if (isIndependent) {
|
||||
describeOptions.match = "*@*"; // independent tag pattern
|
||||
}
|
||||
const { lastTagName } = describeRefSync(describeOptions, includeMergedTags);
|
||||
|
||||
if (lastTagName) {
|
||||
const gitCommandArgs = [
|
||||
"log",
|
||||
`${lastTagName}..HEAD`,
|
||||
'--format="%h %aI"',
|
||||
"--reverse",
|
||||
];
|
||||
console.error("git", "getCurrentBranchOldestCommitSinceLastTag");
|
||||
console.error("exec", `git ${gitCommandArgs.join(" ")}`);
|
||||
let stdout = trimChars(
|
||||
spawnSync(
|
||||
"git",
|
||||
gitCommandArgs,
|
||||
// execOpts
|
||||
).stdout.toString("utf8"),
|
||||
"\n \r",
|
||||
);
|
||||
if (!stdout) {
|
||||
// in some occasion the previous git command might return nothing, in that case we'll return the tag detail instead
|
||||
stdout = trimChars(
|
||||
spawnSync(
|
||||
"git",
|
||||
["log", "-1", '--format="%h %aI"', lastTagName],
|
||||
// execOpts,
|
||||
).stdout.toString() || "",
|
||||
"\n \r",
|
||||
);
|
||||
}
|
||||
[commitResult] = stdout.split("\n");
|
||||
} else {
|
||||
const gitCommandArgs = [
|
||||
"log",
|
||||
"--oneline",
|
||||
'--format="%h %aI"',
|
||||
"--reverse",
|
||||
"--max-parents=0",
|
||||
"HEAD",
|
||||
];
|
||||
console.error("git", "getCurrentBranchFirstCommit");
|
||||
console.error("exec", `git ${gitCommandArgs.join(" ")}`);
|
||||
commitResult = trimChars(
|
||||
spawnSync(
|
||||
"git",
|
||||
gitCommandArgs,
|
||||
// execOpts
|
||||
).stdout.toString("utf8"),
|
||||
"\n \r",
|
||||
);
|
||||
}
|
||||
|
||||
const [, commitHash, commitDate] =
|
||||
/^"?([0-9a-f]+)\s([0-9\-|\+T\:]*)"?$/.exec(commitResult) || [];
|
||||
// prettier-ignore
|
||||
console.error('oldestCommitSinceLastTag', `commit found since last tag: ${lastTagName} - (SHA) ${commitHash} - ${commitDate}`);
|
||||
|
||||
return { commitHash, commitDate };
|
||||
}
|
||||
|
||||
async function getCommitsSinceLastRelease(branchName, isIndependent, execOpts) {
|
||||
// get the last release tag date or the first commit date if no release tag found
|
||||
const { commitDate } = getOldestCommitSinceLastTag(
|
||||
execOpts,
|
||||
isIndependent,
|
||||
false,
|
||||
);
|
||||
|
||||
return getGithubCommits(branchName, commitDate, execOpts);
|
||||
}
|
||||
|
||||
/**
|
||||
* From a dot (.) notation path, find and return a property within an object given a complex object path
|
||||
* Note that the object path does should not include the parent itself
|
||||
* for example if we want to get `address.zip` from `user` object, we would call `getComplexObjectValue(user, 'address.zip')`
|
||||
* @param object - object to search from
|
||||
* @param path - complex object path to find descendant property from, must be a string with dot (.) notation
|
||||
* @returns outputValue - the object property value found if any
|
||||
*/
|
||||
function getComplexObjectValue(object, path) {
|
||||
if (!object || !path) {
|
||||
return object;
|
||||
}
|
||||
return path.split(".").reduce((obj, prop) => obj?.[prop], object);
|
||||
}
|
||||
|
||||
function getOldestCommitSinceLastTag(
|
||||
execOpts,
|
||||
isIndependent,
|
||||
includeMergedTags,
|
||||
) {
|
||||
let commitResult = "";
|
||||
const describeOptions = { ...execOpts };
|
||||
if (isIndependent) {
|
||||
describeOptions.match = "*@*"; // independent tag pattern
|
||||
}
|
||||
const { lastTagName } = describeRefSync(describeOptions, includeMergedTags);
|
||||
|
||||
if (lastTagName) {
|
||||
const gitCommandArgs = [
|
||||
"log",
|
||||
`${lastTagName}..HEAD`,
|
||||
'--format="%h %aI"',
|
||||
"--reverse",
|
||||
];
|
||||
console.error("git", "getCurrentBranchOldestCommitSinceLastTag");
|
||||
console.error("exec", `git ${gitCommandArgs.join(" ")}`);
|
||||
let stdout = trimChars(
|
||||
spawnSync(
|
||||
"git",
|
||||
gitCommandArgs,
|
||||
// execOpts
|
||||
).stdout.toString(),
|
||||
"\n \r",
|
||||
);
|
||||
if (!stdout) {
|
||||
// in some occasion the previous git command might return nothing, in that case we'll return the tag detail instead
|
||||
stdout = trimChars(
|
||||
spawnSync(
|
||||
"git",
|
||||
["log", "-1", '--format="%h %aI"', lastTagName],
|
||||
// execOpts,
|
||||
).stdout.toString() || "",
|
||||
"\n \r",
|
||||
);
|
||||
}
|
||||
[commitResult] = stdout.split("\n");
|
||||
} else {
|
||||
const gitCommandArgs = [
|
||||
"log",
|
||||
"--oneline",
|
||||
'--format="%h %aI"',
|
||||
"--reverse",
|
||||
"--max-parents=0",
|
||||
"HEAD",
|
||||
];
|
||||
console.error("git", "getCurrentBranchFirstCommit");
|
||||
console.error("exec", `git ${gitCommandArgs.join(" ")}`);
|
||||
commitResult = trimChars(
|
||||
spawnSync(
|
||||
"git",
|
||||
gitCommandArgs,
|
||||
// execOpts
|
||||
).stdout.toString(),
|
||||
"\n \r",
|
||||
);
|
||||
}
|
||||
|
||||
const [, commitHash, commitDate] =
|
||||
/^"?([0-9a-f]+)\s([0-9\-|\+T\:]*)"?$/.exec(commitResult) || [];
|
||||
// prettier-ignore
|
||||
console.error('oldestCommitSinceLastTag', `commit found since last tag: ${lastTagName} - (SHA) ${commitHash} - ${commitDate}`);
|
||||
|
||||
return { commitHash, commitDate };
|
||||
}
|
||||
|
||||
// Retrieve previous commits since last release from GitHub API
|
||||
async function getGithubCommits(branchName, sinceDate, execOpts) {
|
||||
const octokit = github.getOctokit(process.env.GITHUB_TOKEN);
|
||||
const remoteCommits = [];
|
||||
|
||||
let afterCursor = "";
|
||||
let hasNextPage = false;
|
||||
|
||||
do {
|
||||
const afterCursorStr = afterCursor ? `, after: "${afterCursor}"` : "";
|
||||
const queryStr = dedent`
|
||||
query getCommits($repo: String!, $owner: String!, $branchName: String!, $pageSize: Int!, $since: GitTimestamp!) {
|
||||
repository(name: $repo, owner: $owner) {
|
||||
ref(qualifiedName: $branchName) {
|
||||
target { ... on Commit {
|
||||
history(first: $pageSize, since: $since ${afterCursorStr}) {
|
||||
nodes { oid, message, author { name, user { login }}}
|
||||
pageInfo { hasNextPage, endCursor }
|
||||
}}}}}}
|
||||
`.trim();
|
||||
|
||||
const response = await octokit.graphql(queryStr, {
|
||||
owner: "keiko233",
|
||||
repo: "clash-nyanpasu",
|
||||
afterCursor,
|
||||
branchName,
|
||||
pageSize: QUERY_PAGE_SIZE,
|
||||
since: sinceDate,
|
||||
});
|
||||
|
||||
const historyData = getComplexObjectValue(
|
||||
response,
|
||||
"repository.ref.target.history",
|
||||
);
|
||||
const pageInfo = historyData?.pageInfo;
|
||||
hasNextPage = pageInfo?.hasNextPage ?? false;
|
||||
afterCursor = pageInfo?.endCursor ?? "";
|
||||
|
||||
if (historyData?.nodes) {
|
||||
for (const commit of historyData.nodes) {
|
||||
if (commit?.oid && commit?.author) {
|
||||
remoteCommits.push({
|
||||
shortHash: commit.oid.substring(0, 7),
|
||||
authorName: commit?.author.name,
|
||||
login: commit?.author?.user?.login ?? "",
|
||||
message: commit?.message ?? "",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (hasNextPage);
|
||||
|
||||
console.error(
|
||||
"github",
|
||||
"found %s commits since last release timestamp %s",
|
||||
remoteCommits.length,
|
||||
sinceDate,
|
||||
);
|
||||
|
||||
return remoteCommits;
|
||||
}
|
||||
|
||||
module.exports = (async () => {
|
||||
try {
|
||||
const config = await conventionalChangelogConfig({
|
||||
types: [
|
||||
{
|
||||
type: "feat",
|
||||
section: "✨ Features",
|
||||
},
|
||||
{
|
||||
type: "fix",
|
||||
section: "🐛 Bug Fixes",
|
||||
},
|
||||
{
|
||||
type: "chore",
|
||||
section: "🧹 Maintenance",
|
||||
},
|
||||
{
|
||||
type: "docs",
|
||||
section: "📚 Documentation",
|
||||
},
|
||||
{
|
||||
type: "style",
|
||||
section: "💅 Styles",
|
||||
},
|
||||
{
|
||||
type: "refactor",
|
||||
section: "🔨 Refactoring",
|
||||
},
|
||||
{
|
||||
type: "perf",
|
||||
section: "⚡ Performance Improvements",
|
||||
},
|
||||
{
|
||||
type: "test",
|
||||
section: "✅ Tests",
|
||||
},
|
||||
],
|
||||
});
|
||||
const commitsSinceLastRelease = await getCommitsSinceLastRelease(
|
||||
"main",
|
||||
false,
|
||||
);
|
||||
config.gitRawCommitsOpts.format = GIT_COMMIT_WITH_AUTHOR_FORMAT;
|
||||
config.writerOpts.commitPartial =
|
||||
config.writerOpts.commitPartial.replace(/\n*$/, "") +
|
||||
` {{#if @root.linkReferences~}}${extraCommitMsg}{{~/if}}\n`;
|
||||
config.writerOpts.transform = writerOptsTransform.bind(
|
||||
null,
|
||||
config.writerOpts.transform,
|
||||
commitsSinceLastRelease,
|
||||
);
|
||||
return config;
|
||||
} catch (e) {
|
||||
console.error("pre-changelog-generation", e);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
@@ -1,10 +0,0 @@
|
||||
// Next version e.g. 1.12.3
|
||||
module.exports = exports = {};
|
||||
module.exports.preVersionGeneration = function preVersionGeneration(version) {
|
||||
return process.env.NYANPASU_VERSION;
|
||||
};
|
||||
|
||||
// Next tag e.g. v1.12.3
|
||||
module.exports.preTagGeneration = function preTagGeneration(tag) {
|
||||
return `v${process.env.NYANPASU_VERSION}`;
|
||||
};
|
34
clash-nyanpasu/.prettierrc.js
Normal file
34
clash-nyanpasu/.prettierrc.js
Normal file
@@ -0,0 +1,34 @@
|
||||
// @ts-check
|
||||
import IanvsSorImportsPlugin from "@ianvs/prettier-plugin-sort-imports";
|
||||
|
||||
/** @type {import("prettier").Config} */
|
||||
export default {
|
||||
endOfLine: "lf",
|
||||
semi: true,
|
||||
singleQuote: false,
|
||||
bracketSpacing: true,
|
||||
tabWidth: 2,
|
||||
trailingComma: "all",
|
||||
overrides: [
|
||||
{
|
||||
files: ["tsconfig.json", "jsconfig.json"],
|
||||
options: {
|
||||
parser: "jsonc",
|
||||
},
|
||||
},
|
||||
],
|
||||
importOrder: [
|
||||
"^@ui/(.*)$",
|
||||
"^@interface/(.*)$",
|
||||
"^@/(.*)$",
|
||||
"^@(.*)$",
|
||||
"^[./]",
|
||||
],
|
||||
importOrderParserPlugins: ["typescript", "jsx", "decorators-legacy"],
|
||||
importOrderTypeScriptVersion: "5.0.0",
|
||||
plugins: [
|
||||
IanvsSorImportsPlugin,
|
||||
"prettier-plugin-tailwindcss",
|
||||
"prettier-plugin-toml",
|
||||
],
|
||||
};
|
315
clash-nyanpasu/backend/Cargo.lock
generated
315
clash-nyanpasu/backend/Cargo.lock
generated
@@ -113,9 +113,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.14"
|
||||
version = "0.6.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
|
||||
checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
@@ -128,33 +128,33 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.7"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
|
||||
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.4"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
|
||||
checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391"
|
||||
checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.3"
|
||||
version = "3.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
|
||||
checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys 0.52.0",
|
||||
@@ -678,9 +678,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.9.1"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706"
|
||||
checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
@@ -891,9 +891,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.10"
|
||||
version = "4.5.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f6b81fb3c84f5563d509c59b5a48d935f689e993afa90fe39047f05adef9142"
|
||||
checksum = "35723e6a11662c2afb578bcf0b88bf6ea8e21282a953428f240574fcc3a2b5b3"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -901,9 +901,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.10"
|
||||
version = "4.5.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca6706fd5224857d9ac5eb9355f6683563cc0541c7cd9d014043b57cbec78ac"
|
||||
checksum = "49eb96cbfa7cfa35017b7cd548c75b14c3118c98b423041d70562665e07fb0fa"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -913,9 +913,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.8"
|
||||
version = "4.5.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085"
|
||||
checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
@@ -925,9 +925,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.1"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70"
|
||||
checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
|
||||
|
||||
[[package]]
|
||||
name = "clash-nyanpasu"
|
||||
@@ -967,6 +967,7 @@ dependencies = [
|
||||
"md-5",
|
||||
"nanoid",
|
||||
"num_cpus",
|
||||
"nyanpasu-ipc",
|
||||
"nyanpasu-utils",
|
||||
"objc",
|
||||
"once_cell",
|
||||
@@ -1011,6 +1012,7 @@ dependencies = [
|
||||
"uuid",
|
||||
"webview2-com-bridge",
|
||||
"which 6.0.1",
|
||||
"whoami",
|
||||
"window-shadows",
|
||||
"window-vibrancy",
|
||||
"windows-sys 0.52.0",
|
||||
@@ -1083,9 +1085,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.1"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
|
||||
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
|
||||
|
||||
[[package]]
|
||||
name = "colored"
|
||||
@@ -1621,7 +1623,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "dirs-utils"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/LibNyanpasu/nyanpasu-utils#31f59134a7e94922f13b07b21047390b962ce3c4"
|
||||
source = "git+https://github.com/LibNyanpasu/nyanpasu-utils.git#a4d554586049a548528ed88c3f33a44450610f51"
|
||||
dependencies = [
|
||||
"dirs-next",
|
||||
"thiserror",
|
||||
@@ -1671,6 +1673,12 @@ dependencies = [
|
||||
"libloading 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "doctest-file"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562"
|
||||
|
||||
[[package]]
|
||||
name = "document-features"
|
||||
version = "0.2.10"
|
||||
@@ -1728,7 +1736,7 @@ dependencies = [
|
||||
"cc",
|
||||
"memchr",
|
||||
"rustc_version 0.4.0",
|
||||
"toml 0.8.15",
|
||||
"toml 0.8.16",
|
||||
"vswhom",
|
||||
"winreg 0.52.0",
|
||||
]
|
||||
@@ -1777,18 +1785,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6dc8c8ff84895b051f07a0e65f975cf225131742531338752abfb324e4449ff"
|
||||
checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab"
|
||||
dependencies = [
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.11.4"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06676b12debf7bba6903559720abca942d3a66b8acb88815fd2c7c6537e9ade1"
|
||||
checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -2523,7 +2531,26 @@ dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http 0.2.12",
|
||||
"indexmap 2.2.6",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"http 1.1.0",
|
||||
"indexmap 2.2.6",
|
||||
"slab",
|
||||
"tokio",
|
||||
@@ -2663,6 +2690,17 @@ dependencies = [
|
||||
"itoa 1.0.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"itoa 1.0.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "0.4.6"
|
||||
@@ -2670,7 +2708,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
"http 0.2.12",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http 1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body-util"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http 1.1.0",
|
||||
"http-body 1.0.1",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
@@ -2711,9 +2772,9 @@ dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"h2 0.3.26",
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa 1.0.11",
|
||||
@@ -2725,6 +2786,27 @@ dependencies = [
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"h2 0.4.5",
|
||||
"http 1.1.0",
|
||||
"http-body 1.0.1",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa 1.0.11",
|
||||
"pin-project-lite",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.24.2"
|
||||
@@ -2732,8 +2814,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"http",
|
||||
"hyper",
|
||||
"http 0.2.12",
|
||||
"hyper 0.14.30",
|
||||
"rustls",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
@@ -2746,12 +2828,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"hyper",
|
||||
"hyper 0.14.30",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"http 1.1.0",
|
||||
"http-body 1.0.1",
|
||||
"hyper 1.4.1",
|
||||
"pin-project-lite",
|
||||
"socket2 0.5.7",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.60"
|
||||
@@ -2966,6 +3068,21 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "interprocess"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2f4e4a06d42fab3e85ab1b419ad32b09eab58b901d40c57935ff92db3287a13"
|
||||
dependencies = [
|
||||
"doctest-file",
|
||||
"futures-core",
|
||||
"libc",
|
||||
"recvmsg",
|
||||
"tokio",
|
||||
"widestring 1.1.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-lifetimes"
|
||||
version = "1.0.11"
|
||||
@@ -3015,9 +3132,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.0"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "isolang"
|
||||
@@ -3858,10 +3975,34 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nyanpasu-ipc"
|
||||
version = "1.0.3"
|
||||
source = "git+https://github.com/LibNyanpasu/nyanpasu-service.git#10b455d0ee1c366f1ed3fcaa26b37764ab00c69b"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"derive_builder",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"http-body-util",
|
||||
"hyper 1.4.1",
|
||||
"hyper-util",
|
||||
"interprocess 2.2.1",
|
||||
"nyanpasu-utils",
|
||||
"pin-project-lite",
|
||||
"serde",
|
||||
"simd-json",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"tracing-attributes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nyanpasu-utils"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/LibNyanpasu/nyanpasu-utils#31f59134a7e94922f13b07b21047390b962ce3c4"
|
||||
source = "git+https://github.com/LibNyanpasu/nyanpasu-utils.git#a4d554586049a548528ed88c3f33a44450610f51"
|
||||
dependencies = [
|
||||
"constcat",
|
||||
"derive_builder",
|
||||
@@ -4143,7 +4284,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "os-utils"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/LibNyanpasu/nyanpasu-utils#31f59134a7e94922f13b07b21047390b962ce3c4"
|
||||
source = "git+https://github.com/LibNyanpasu/nyanpasu-utils.git#a4d554586049a548528ed88c3f33a44450610f51"
|
||||
dependencies = [
|
||||
"nix 0.29.0",
|
||||
"shared_child",
|
||||
@@ -4910,6 +5051,12 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "recvmsg"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175"
|
||||
|
||||
[[package]]
|
||||
name = "redb"
|
||||
version = "2.1.1"
|
||||
@@ -5023,10 +5170,10 @@ dependencies = [
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"h2 0.3.26",
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"hyper 0.14.30",
|
||||
"hyper-rustls",
|
||||
"hyper-tls",
|
||||
"ipnet",
|
||||
@@ -5472,9 +5619,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.6"
|
||||
version = "0.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0"
|
||||
checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -6004,7 +6151,7 @@ dependencies = [
|
||||
"cfg-expr 0.15.8",
|
||||
"heck 0.5.0",
|
||||
"pkg-config",
|
||||
"toml 0.8.15",
|
||||
"toml 0.8.16",
|
||||
"version-compare 0.2.0",
|
||||
]
|
||||
|
||||
@@ -6106,7 +6253,7 @@ dependencies = [
|
||||
"glob",
|
||||
"gtk",
|
||||
"heck 0.5.0",
|
||||
"http",
|
||||
"http 0.2.12",
|
||||
"ignore",
|
||||
"indexmap 1.9.3",
|
||||
"infer 0.9.0",
|
||||
@@ -6214,7 +6361,7 @@ name = "tauri-plugin-deep-link"
|
||||
version = "0.1.2"
|
||||
dependencies = [
|
||||
"dirs 5.0.1",
|
||||
"interprocess",
|
||||
"interprocess 1.2.1",
|
||||
"log",
|
||||
"objc2",
|
||||
"once_cell",
|
||||
@@ -6230,7 +6377,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3068ed62b63dedc705558f4248c7ecbd5561f0f8050949859ea0db2326f26012"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
"http 0.2.12",
|
||||
"http-range",
|
||||
"rand 0.8.5",
|
||||
"raw-window-handle 0.5.2",
|
||||
@@ -6619,21 +6766,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.15"
|
||||
version = "0.8.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac2caab0bf757388c6c0ae23b3293fdb463fee59434529014f85e3263b995c28"
|
||||
checksum = "81967dd0dd2c1ab0bc3468bd7caecc32b8a4aa47d0c8c695d8c2b2108168d62c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit 0.22.16",
|
||||
"toml_edit 0.22.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.6"
|
||||
version = "0.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf"
|
||||
checksum = "f8fb9f64314842840f1d940ac544da178732128f1c78c21772e876579e0da1db"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -6664,17 +6811,38 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.16"
|
||||
version = "0.22.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "278f3d518e152219c994ce877758516bca5e118eaed6996192a774fb9fbf0788"
|
||||
checksum = "8d9f8729f5aea9562aac1cc0441f5d6de3cff1ee0c5d67293eeca5eb36ee7c16"
|
||||
dependencies = [
|
||||
"indexmap 2.2.6",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"winnow 0.6.15",
|
||||
"winnow 0.6.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"pin-project",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
|
||||
|
||||
[[package]]
|
||||
name = "tower-service"
|
||||
version = "0.3.2"
|
||||
@@ -6964,9 +7132,9 @@ checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "vswhom"
|
||||
@@ -7055,6 +7223,12 @@ version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasite"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.92"
|
||||
@@ -7368,6 +7542,17 @@ dependencies = [
|
||||
"winsafe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "whoami"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9"
|
||||
dependencies = [
|
||||
"redox_syscall 0.4.1",
|
||||
"wasite",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "widestring"
|
||||
version = "0.4.3"
|
||||
@@ -7940,9 +8125,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.6.15"
|
||||
version = "0.6.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "557404e450152cd6795bb558bca69e43c585055f4606e3bcae5894fc6dac9ba0"
|
||||
checksum = "b480ae9340fc261e6be3e95a1ba86d54ae3f9171132a73ce8d4bbaf68339507c"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -8019,7 +8204,7 @@ dependencies = [
|
||||
"glib",
|
||||
"gtk",
|
||||
"html5ever",
|
||||
"http",
|
||||
"http 0.2.12",
|
||||
"kuchikiki",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -8275,9 +8460,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zip-extensions"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb0a99499b3497d765525c5d05e3ade9ca4a731c184365c19472c3fd6ba86341"
|
||||
checksum = "386508a00aae1d8218b9252a41f59bba739ccee3f8e420bb90bcb1c30d960d4a"
|
||||
dependencies = [
|
||||
"zip 2.1.5",
|
||||
]
|
||||
|
@@ -18,7 +18,8 @@ rustc_version = "0.4"
|
||||
semver = "1.0"
|
||||
|
||||
[dependencies]
|
||||
nyanpasu-utils = { git = "https://github.com/LibNyanpasu/nyanpasu-utils" }
|
||||
nyanpasu-ipc = { git = "https://github.com/LibNyanpasu/nyanpasu-service.git" }
|
||||
nyanpasu-utils = { git = "https://github.com/LibNyanpasu/nyanpasu-utils.git" }
|
||||
which = "6"
|
||||
anyhow = "1.0"
|
||||
dirs = "5.0.1"
|
||||
@@ -110,6 +111,7 @@ ansi-str = "0.8"
|
||||
humansize = "2.1.3"
|
||||
convert_case = "0.6.0"
|
||||
os_pipe = "1.2.0"
|
||||
whoami = "1.5.1"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
cocoa = "0.25.0"
|
||||
|
@@ -9,6 +9,7 @@ pub mod logging;
|
||||
pub use self::clash_strategy::{ClashStrategy, ExternalControllerPortStrategy};
|
||||
pub use logging::LoggingLevel;
|
||||
|
||||
// TODO: when support sing-box, remove this struct
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub enum ClashCore {
|
||||
#[serde(rename = "clash", alias = "clash-premium")]
|
||||
@@ -52,6 +53,41 @@ impl std::fmt::Display for ClashCore {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ClashCore> for nyanpasu_utils::core::CoreType {
|
||||
fn from(core: &ClashCore) -> Self {
|
||||
match core {
|
||||
ClashCore::ClashPremium => nyanpasu_utils::core::CoreType::Clash(
|
||||
nyanpasu_utils::core::ClashCoreType::ClashPremium,
|
||||
),
|
||||
ClashCore::ClashRs => nyanpasu_utils::core::CoreType::Clash(
|
||||
nyanpasu_utils::core::ClashCoreType::ClashRust,
|
||||
),
|
||||
ClashCore::Mihomo => {
|
||||
nyanpasu_utils::core::CoreType::Clash(nyanpasu_utils::core::ClashCoreType::Mihomo)
|
||||
}
|
||||
ClashCore::MihomoAlpha => nyanpasu_utils::core::CoreType::Clash(
|
||||
nyanpasu_utils::core::ClashCoreType::MihomoAlpha,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&nyanpasu_utils::core::CoreType> for ClashCore {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(core: &nyanpasu_utils::core::CoreType) -> Result<Self> {
|
||||
match core {
|
||||
nyanpasu_utils::core::CoreType::Clash(clash) => match clash {
|
||||
nyanpasu_utils::core::ClashCoreType::ClashPremium => Ok(ClashCore::ClashPremium),
|
||||
nyanpasu_utils::core::ClashCoreType::ClashRust => Ok(ClashCore::ClashRs),
|
||||
nyanpasu_utils::core::ClashCoreType::Mihomo => Ok(ClashCore::Mihomo),
|
||||
nyanpasu_utils::core::ClashCoreType::MihomoAlpha => Ok(ClashCore::MihomoAlpha),
|
||||
},
|
||||
_ => Err(anyhow::anyhow!("unsupported core type")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### `verge.yaml` schema
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct IVerge {
|
||||
|
@@ -5,51 +5,288 @@ use crate::{
|
||||
log_err,
|
||||
utils::dirs,
|
||||
};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use anyhow::{bail, Result};
|
||||
use nyanpasu_ipc::{api::status::CoreState, utils::get_current_ts};
|
||||
use nyanpasu_utils::{
|
||||
core::{
|
||||
instance::{CoreInstance, CoreInstanceBuilder},
|
||||
CommandEvent,
|
||||
},
|
||||
runtime::{block_on, spawn},
|
||||
};
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use std::{fs, io::Write, sync::Arc, time::Duration};
|
||||
use sysinfo::{Pid, System};
|
||||
use tauri::api::process::{Command, CommandChild, CommandEvent};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicI64, Ordering},
|
||||
Arc,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
use tauri::api::process::Command;
|
||||
use tokio::time::sleep;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use crate::core::win_service;
|
||||
pub enum RunType {
|
||||
/// Run as child process directly
|
||||
Normal,
|
||||
/// Run by Nyanpasu Service via a ipc call
|
||||
Service,
|
||||
// TODO: Not implemented yet
|
||||
/// Run as elevated process, if profile advice to run as elevated
|
||||
Elevated,
|
||||
}
|
||||
|
||||
impl Default for RunType {
|
||||
fn default() -> Self {
|
||||
let enable_service = Config::verge()
|
||||
.latest()
|
||||
.enable_service_mode
|
||||
.unwrap_or(false);
|
||||
if enable_service {
|
||||
RunType::Service
|
||||
} else {
|
||||
RunType::Normal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Instance {
|
||||
Child {
|
||||
child: Mutex<Arc<CoreInstance>>,
|
||||
stated_changed_at: Arc<AtomicI64>,
|
||||
kill_flag: Arc<AtomicBool>,
|
||||
},
|
||||
Service,
|
||||
}
|
||||
|
||||
impl Instance {
|
||||
pub fn try_new(run_type: RunType) -> Result<Self> {
|
||||
let core_type: nyanpasu_utils::core::CoreType = {
|
||||
(Config::verge()
|
||||
.latest()
|
||||
.clash_core
|
||||
.as_ref()
|
||||
.unwrap_or(&ClashCore::ClashPremium))
|
||||
.into()
|
||||
};
|
||||
let data_dir = dirs::app_data_dir()?;
|
||||
let binary = find_binary_path(&core_type)?;
|
||||
let config_path = Config::generate_file(ConfigType::Run)?;
|
||||
let pid_path = dirs::clash_pid_path()?;
|
||||
match run_type {
|
||||
RunType::Normal => {
|
||||
let instance = Arc::new(
|
||||
CoreInstanceBuilder::default()
|
||||
.core_type(core_type)
|
||||
.app_dir(data_dir)
|
||||
.binary_path(binary)
|
||||
.config_path(config_path.clone())
|
||||
.pid_path(pid_path)
|
||||
.build()?,
|
||||
);
|
||||
Ok(Instance::Child {
|
||||
child: Mutex::new(instance),
|
||||
kill_flag: Arc::new(AtomicBool::new(false)),
|
||||
stated_changed_at: Arc::new(AtomicI64::new(get_current_ts())),
|
||||
})
|
||||
}
|
||||
RunType::Service => Ok(Instance::Service),
|
||||
RunType::Elevated => {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start(&self) -> Result<()> {
|
||||
match self {
|
||||
Instance::Child {
|
||||
child,
|
||||
kill_flag,
|
||||
stated_changed_at,
|
||||
} => {
|
||||
let instance = {
|
||||
let child = child.lock();
|
||||
child.clone()
|
||||
};
|
||||
let is_premium = {
|
||||
let child = child.lock();
|
||||
matches!(
|
||||
child.core_type,
|
||||
nyanpasu_utils::core::CoreType::Clash(
|
||||
nyanpasu_utils::core::ClashCoreType::ClashPremium
|
||||
)
|
||||
)
|
||||
};
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel::<anyhow::Result<()>>(1); // use mpsc channel just to avoid type moved error, though it never fails
|
||||
let stated_changed_at = stated_changed_at.clone();
|
||||
let kill_flag = kill_flag.clone();
|
||||
// This block below is to handle the stdio from the core process
|
||||
tokio::spawn(async move {
|
||||
match instance.run().await {
|
||||
Ok((_, mut rx)) => {
|
||||
kill_flag.store(false, Ordering::Relaxed); // reset kill flag
|
||||
let mut err_buf: Vec<String> = Vec::with_capacity(6);
|
||||
loop {
|
||||
if let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
CommandEvent::Stdout(line) => {
|
||||
if is_premium {
|
||||
let log = api::parse_log(line.clone());
|
||||
log::info!(target: "app", "[clash]: {}", log);
|
||||
} else {
|
||||
log::info!(target: "app", "[clash]: {}", line);
|
||||
}
|
||||
Logger::global().set_log(line);
|
||||
}
|
||||
CommandEvent::Stderr(line) => {
|
||||
log::error!(target: "app", "[clash]: {}", line);
|
||||
err_buf.push(line.clone());
|
||||
Logger::global().set_log(line);
|
||||
}
|
||||
CommandEvent::Error(e) => {
|
||||
log::error!(target: "app", "[clash]: {}", e);
|
||||
let err = anyhow::anyhow!(format!(
|
||||
"{}\n{}",
|
||||
e,
|
||||
err_buf.join("\n")
|
||||
));
|
||||
Logger::global().set_log(e);
|
||||
let _ = tx.send(Err(err)).await;
|
||||
stated_changed_at
|
||||
.store(get_current_ts(), Ordering::Relaxed);
|
||||
break;
|
||||
}
|
||||
CommandEvent::Terminated(status) => {
|
||||
log::error!(
|
||||
target: "app",
|
||||
"core terminated with status: {:?}",
|
||||
status
|
||||
);
|
||||
stated_changed_at
|
||||
.store(get_current_ts(), Ordering::Relaxed);
|
||||
if status.code != Some(0)
|
||||
|| !matches!(status.signal, Some(9) | Some(15))
|
||||
{
|
||||
let err = anyhow::anyhow!(format!(
|
||||
"core terminated with status: {:?}\n{}",
|
||||
status,
|
||||
err_buf.join("\n")
|
||||
));
|
||||
tracing::error!("{}\n{}", err, err_buf.join("\n"));
|
||||
if tx.send(Err(err)).await.is_err()
|
||||
&& !kill_flag.load(Ordering::Relaxed)
|
||||
{
|
||||
std::thread::spawn(move || {
|
||||
block_on(async {
|
||||
tracing::info!(
|
||||
"Trying to recover core."
|
||||
);
|
||||
let _ = CoreManager::global()
|
||||
.recover_core()
|
||||
.await;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
CommandEvent::DelayCheckpointPass => {
|
||||
tracing::debug!("delay checkpoint pass");
|
||||
stated_changed_at
|
||||
.store(get_current_ts(), Ordering::Relaxed);
|
||||
tx.send(Ok(())).await.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
spawn(async move {
|
||||
tx.send(Err(err.into())).await.unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
rx.recv().await.unwrap()?;
|
||||
Ok(())
|
||||
}
|
||||
Instance::Service => {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn stop(&self) -> Result<()> {
|
||||
let state = self.state();
|
||||
match self {
|
||||
Instance::Child {
|
||||
child,
|
||||
stated_changed_at,
|
||||
kill_flag,
|
||||
} => {
|
||||
if matches!(state.as_ref(), CoreState::Stopped(_)) {
|
||||
anyhow::bail!("core is already stopped");
|
||||
}
|
||||
kill_flag.store(true, Ordering::Relaxed);
|
||||
let child = {
|
||||
let child = child.lock();
|
||||
child.clone()
|
||||
};
|
||||
child.kill().await?;
|
||||
stated_changed_at.store(get_current_ts(), Ordering::Relaxed);
|
||||
Ok(())
|
||||
}
|
||||
Instance::Service => {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn restart(&self) -> Result<()> {
|
||||
let state = self.state();
|
||||
if matches!(state.as_ref(), CoreState::Running) {
|
||||
self.stop().await?;
|
||||
}
|
||||
self.start().await
|
||||
}
|
||||
|
||||
pub fn state<'a>(&self) -> Cow<'a, CoreState> {
|
||||
match self {
|
||||
Instance::Child { child, .. } => {
|
||||
let this = child.lock();
|
||||
Cow::Owned(match this.state() {
|
||||
nyanpasu_utils::core::instance::CoreInstanceState::Running => {
|
||||
CoreState::Running
|
||||
}
|
||||
nyanpasu_utils::core::instance::CoreInstanceState::Stopped => {
|
||||
CoreState::Stopped(None)
|
||||
}
|
||||
})
|
||||
}
|
||||
Instance::Service => {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CoreManager {
|
||||
sidecar: Arc<Mutex<Option<CommandChild>>>,
|
||||
|
||||
#[allow(unused)]
|
||||
use_service_mode: Arc<Mutex<bool>>,
|
||||
instance: Mutex<Option<Arc<Instance>>>,
|
||||
}
|
||||
|
||||
impl CoreManager {
|
||||
pub fn global() -> &'static CoreManager {
|
||||
static CORE_MANAGER: OnceCell<CoreManager> = OnceCell::new();
|
||||
|
||||
CORE_MANAGER.get_or_init(|| CoreManager {
|
||||
sidecar: Arc::new(Mutex::new(None)),
|
||||
use_service_mode: Arc::new(Mutex::new(false)),
|
||||
instance: Mutex::new(None),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn init(&self) -> Result<()> {
|
||||
// kill old clash process
|
||||
let _ = dirs::clash_pid_path()
|
||||
.and_then(|path| fs::read(path).map(|p| p.to_vec()).context(""))
|
||||
.and_then(|pid| String::from_utf8_lossy(&pid).parse().context(""))
|
||||
.map(|pid| {
|
||||
let mut system = System::new();
|
||||
system.refresh_all();
|
||||
if let Some(proc) = system.process(Pid::from_u32(pid)) {
|
||||
if proc.name().contains("clash") {
|
||||
log::debug!(target: "app", "kill old clash process");
|
||||
proc.kill();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tauri::async_runtime::spawn(async {
|
||||
// 启动clash
|
||||
log_err!(Self::global().run_core().await);
|
||||
@@ -88,34 +325,25 @@ impl CoreManager {
|
||||
|
||||
/// 启动核心
|
||||
pub async fn run_core(&self) -> Result<()> {
|
||||
#[allow(unused_mut)]
|
||||
let mut should_kill = match self.sidecar.lock().take() {
|
||||
Some(child) => {
|
||||
log::debug!(target: "app", "stop the core by sidecar");
|
||||
let _ = child.kill();
|
||||
true
|
||||
{
|
||||
let instance = {
|
||||
let instance = self.instance.lock();
|
||||
instance.as_ref().cloned()
|
||||
};
|
||||
if let Some(instance) = instance.as_ref() {
|
||||
if matches!(instance.state().as_ref(), CoreState::Running) {
|
||||
log::debug!(target: "app", "core is already running, stop it first...");
|
||||
instance.stop().await?;
|
||||
}
|
||||
}
|
||||
None => false,
|
||||
};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
if *self.use_service_mode.lock() {
|
||||
log::debug!(target: "app", "stop the core by service");
|
||||
log_err!(win_service::stop_core_by_service().await);
|
||||
should_kill = true;
|
||||
}
|
||||
|
||||
// 这里得等一会儿
|
||||
if should_kill {
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
|
||||
// 检查端口是否可用
|
||||
// TODO: 修复下面这个方法,从而允许 Fallback 到其他端口
|
||||
Config::clash()
|
||||
.latest()
|
||||
.prepare_external_controller_port()?;
|
||||
|
||||
let config_path = Config::generate_file(ConfigType::Run)?;
|
||||
let instance = Arc::new(Instance::try_new(RunType::Normal)?);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
@@ -134,147 +362,81 @@ impl CoreManager {
|
||||
log::debug!(target: "app", "{event:?}");
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
// FIXME: 重构服务模式
|
||||
// #[cfg(target_os = "windows")]
|
||||
// {
|
||||
// // 服务模式
|
||||
// let enable = { Config::verge().latest().enable_service_mode };
|
||||
// let enable = enable.unwrap_or(false);
|
||||
|
||||
// *self.use_service_mode.lock() = enable;
|
||||
|
||||
// if enable {
|
||||
// // 服务模式启动失败就直接运行 sidecar
|
||||
// log::debug!(target: "app", "try to run core in service mode");
|
||||
// let res = async {
|
||||
// win_service::check_service().await?;
|
||||
// win_service::run_core_by_service(&config_path).await
|
||||
// }
|
||||
// .await;
|
||||
// match res {
|
||||
// Ok(_) => return Ok(()),
|
||||
// Err(err) => {
|
||||
// // 修改这个值,免得stop出错
|
||||
// *self.use_service_mode.lock() = false;
|
||||
// log::error!(target: "app", "{err}");
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
{
|
||||
// 服务模式
|
||||
let enable = { Config::verge().latest().enable_service_mode };
|
||||
let enable = enable.unwrap_or(false);
|
||||
|
||||
*self.use_service_mode.lock() = enable;
|
||||
|
||||
if enable {
|
||||
// 服务模式启动失败就直接运行 sidecar
|
||||
log::debug!(target: "app", "try to run core in service mode");
|
||||
let res = async {
|
||||
win_service::check_service().await?;
|
||||
win_service::run_core_by_service(&config_path).await
|
||||
}
|
||||
.await;
|
||||
match res {
|
||||
Ok(_) => return Ok(()),
|
||||
Err(err) => {
|
||||
// 修改这个值,免得stop出错
|
||||
*self.use_service_mode.lock() = false;
|
||||
log::error!(target: "app", "{err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut this = self.instance.lock();
|
||||
*this = Some(instance.clone());
|
||||
}
|
||||
|
||||
let app_dir = dirs::app_data_dir()?;
|
||||
let app_dir = dirs::path_to_str(&app_dir)?;
|
||||
|
||||
let clash_core = { Config::verge().latest().clash_core.clone() };
|
||||
let clash_core = clash_core.unwrap_or(ClashCore::ClashPremium);
|
||||
let is_clash = matches!(&clash_core, ClashCore::ClashPremium);
|
||||
|
||||
let config_path = dirs::path_to_str(&config_path)?;
|
||||
|
||||
// fix #212
|
||||
let args = match &clash_core {
|
||||
ClashCore::Mihomo | ClashCore::MihomoAlpha => {
|
||||
vec!["-m", "-d", app_dir, "-f", config_path]
|
||||
}
|
||||
ClashCore::ClashRs => vec!["-d", app_dir, "-c", config_path],
|
||||
ClashCore::ClashPremium => vec!["-d", app_dir, "-f", config_path],
|
||||
};
|
||||
|
||||
let cmd = Command::new_sidecar(clash_core)?;
|
||||
let (mut rx, cmd_child) = cmd.args(args).spawn()?;
|
||||
|
||||
// 将pid写入文件中
|
||||
crate::log_err!((|| {
|
||||
let pid = cmd_child.pid();
|
||||
let path = dirs::clash_pid_path()?;
|
||||
fs::File::create(path)
|
||||
.context("failed to create the pid file")?
|
||||
.write(format!("{pid}").as_bytes())
|
||||
.context("failed to write pid to the file")?;
|
||||
<Result<()>>::Ok(())
|
||||
})());
|
||||
|
||||
let mut sidecar = self.sidecar.lock();
|
||||
*sidecar = Some(cmd_child);
|
||||
drop(sidecar);
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
CommandEvent::Stdout(line) => {
|
||||
if is_clash {
|
||||
let stdout = api::parse_log(line.clone());
|
||||
log::info!(target: "app", "[clash]: {stdout}");
|
||||
} else {
|
||||
log::info!(target: "app", "[clash]: {line}");
|
||||
};
|
||||
Logger::global().set_log(line);
|
||||
}
|
||||
CommandEvent::Stderr(err) => {
|
||||
// let stdout = api::parse_log(err.clone());
|
||||
log::error!(target: "app", "[clash]: {err}");
|
||||
Logger::global().set_log(err);
|
||||
}
|
||||
CommandEvent::Error(err) => {
|
||||
log::error!(target: "app", "[clash]: {err}");
|
||||
Logger::global().set_log(err);
|
||||
}
|
||||
CommandEvent::Terminated(_) => {
|
||||
log::info!(target: "app", "clash core terminated");
|
||||
let _ = CoreManager::global().recover_core();
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
instance.start().await
|
||||
}
|
||||
|
||||
/// 重启内核
|
||||
pub fn recover_core(&'static self) -> Result<()> {
|
||||
// 服务模式不管
|
||||
#[cfg(target_os = "windows")]
|
||||
if *self.use_service_mode.lock() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 清空原来的sidecar值
|
||||
if let Some(sidecar) = self.sidecar.lock().take() {
|
||||
let _ = sidecar.kill();
|
||||
}
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
// 6秒之后再查看服务是否正常 (时间随便搞的)
|
||||
// terminated 可能是切换内核 (切换内核已经有500ms的延迟)
|
||||
sleep(Duration::from_millis(6666)).await;
|
||||
|
||||
if self.sidecar.lock().is_none() {
|
||||
log::info!(target: "app", "recover clash core");
|
||||
|
||||
// 重新启动app
|
||||
if let Err(err) = self.run_core().await {
|
||||
log::error!(target: "app", "failed to recover clash core");
|
||||
log::error!(target: "app", "{err}");
|
||||
|
||||
let _ = self.recover_core();
|
||||
pub async fn recover_core(&'static self) -> Result<()> {
|
||||
// 清除原来的实例
|
||||
{
|
||||
let instance = {
|
||||
let mut this = self.instance.lock();
|
||||
this.take()
|
||||
};
|
||||
if let Some(instance) = instance {
|
||||
if matches!(instance.state().as_ref(), CoreState::Running) {
|
||||
log::debug!(target: "app", "core is running, stop it first...");
|
||||
instance.stop().await?;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if let Err(err) = self.run_core().await {
|
||||
log::error!(target: "app", "failed to recover clash core");
|
||||
log::error!(target: "app", "{err}");
|
||||
tokio::time::sleep(Duration::from_secs(5)).await; // sleep 5s
|
||||
std::thread::spawn(move || {
|
||||
block_on(async {
|
||||
let _ = CoreManager::global().recover_core().await;
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 停止核心运行
|
||||
pub fn stop_core(&self) -> Result<()> {
|
||||
#[cfg(target_os = "windows")]
|
||||
if *self.use_service_mode.lock() {
|
||||
log::debug!(target: "app", "stop the core by service");
|
||||
tauri::async_runtime::block_on(async move {
|
||||
log_err!(win_service::stop_core_by_service().await);
|
||||
});
|
||||
return Ok(());
|
||||
}
|
||||
pub async fn stop_core(&self) -> Result<()> {
|
||||
// #[cfg(target_os = "windows")]
|
||||
// if *self.use_service_mode.lock() {
|
||||
// log::debug!(target: "app", "stop the core by service");
|
||||
// tauri::async_runtime::block_on(async move {
|
||||
// log_err!(win_service::stop_core_by_service().await);
|
||||
// });
|
||||
// return Ok(());
|
||||
// }
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
@@ -291,16 +453,18 @@ impl CoreManager {
|
||||
Ok(_) => return Ok(()),
|
||||
Err(err) => {
|
||||
// 修改这个值,免得stop出错
|
||||
*self.use_service_mode.lock() = false;
|
||||
// *self.use_service_mode.lock() = false;
|
||||
log::error!(target: "app", "{err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut sidecar = self.sidecar.lock();
|
||||
if let Some(child) = sidecar.take() {
|
||||
log::debug!(target: "app", "stop the core by sidecar");
|
||||
let _ = child.kill();
|
||||
let instance = {
|
||||
let instance = self.instance.lock();
|
||||
instance.as_ref().cloned()
|
||||
};
|
||||
if let Some(instance) = instance.as_ref() {
|
||||
instance.stop().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -309,10 +473,6 @@ impl CoreManager {
|
||||
pub async fn change_core(&self, clash_core: Option<ClashCore>) -> Result<()> {
|
||||
let clash_core = clash_core.ok_or(anyhow::anyhow!("clash core is null"))?;
|
||||
|
||||
// if &clash_core != "clash" && &clash_core != "clash-meta" && &clash_core != "clash-rs" {
|
||||
// bail!("invalid clash core name \"{clash_core}\"");
|
||||
// }
|
||||
|
||||
log::debug!(target: "app", "change core to `{clash_core}`");
|
||||
|
||||
Config::verge().draft().clash_core = Some(clash_core);
|
||||
@@ -335,6 +495,7 @@ impl CoreManager {
|
||||
Err(err) => {
|
||||
Config::verge().discard();
|
||||
Config::runtime().discard();
|
||||
self.run_core().await?;
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
@@ -373,3 +534,25 @@ impl CoreManager {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: support system path search via a config or flag
|
||||
// FIXME: move this fn to nyanpasu-utils
|
||||
/// Search the binary path of the core: Data Dir -> Sidecar Dir
|
||||
pub fn find_binary_path(core_type: &nyanpasu_utils::core::CoreType) -> std::io::Result<PathBuf> {
|
||||
let data_dir = dirs::app_data_dir()
|
||||
.map_err(|err| std::io::Error::new(std::io::ErrorKind::NotFound, err.to_string()))?;
|
||||
let binary_path = data_dir.join(core_type.get_executable_name());
|
||||
if binary_path.exists() {
|
||||
return Ok(binary_path);
|
||||
}
|
||||
let app_dir = dirs::app_install_dir()
|
||||
.map_err(|err| std::io::Error::new(std::io::ErrorKind::NotFound, err.to_string()))?;
|
||||
let binary_path = app_dir.join(core_type.get_executable_name());
|
||||
if binary_path.exists() {
|
||||
return Ok(binary_path);
|
||||
}
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
format!("{} not found", core_type.get_executable_name()),
|
||||
))
|
||||
}
|
||||
|
@@ -24,20 +24,31 @@ impl<'a> Migration<'a> for MigrateAppHomeDir {
|
||||
// Allow deprecated because we are moving deprecated files to new locations
|
||||
#[allow(deprecated)]
|
||||
fn migrate(&self) -> std::io::Result<()> {
|
||||
let home_dir = crate::utils::dirs::app_home_dir().unwrap();
|
||||
if !home_dir.exists() {
|
||||
println!("Home dir not found, skipping migration");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// create the app config and data dir
|
||||
println!("Creating app config and data dir");
|
||||
let app_config_dir = crate::utils::dirs::app_config_dir().unwrap();
|
||||
fs_extra::dir::create_all(crate::utils::dirs::app_config_dir().unwrap(), false)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
|
||||
if !app_config_dir.exists() {
|
||||
std::fs::create_dir_all(&app_config_dir)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
|
||||
}
|
||||
let app_data_dir = crate::utils::dirs::app_data_dir().unwrap();
|
||||
fs_extra::dir::create_all(crate::utils::dirs::app_data_dir().unwrap(), false)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
|
||||
if !app_data_dir.exists() {
|
||||
std::fs::create_dir_all(&app_data_dir)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
|
||||
}
|
||||
|
||||
// move the config files to the new config dir
|
||||
let file_opts = fs_extra::file::CopyOptions::default().skip_exist(true);
|
||||
let dir_opts = fs_extra::dir::CopyOptions::default()
|
||||
.skip_exist(true)
|
||||
.content_only(true);
|
||||
let home_dir = crate::utils::dirs::app_home_dir().unwrap();
|
||||
|
||||
// move clash runtime config
|
||||
let path = home_dir.join("clash-verge.yaml");
|
||||
if path.exists() {
|
||||
|
@@ -12,3 +12,4 @@ pub mod win_service;
|
||||
pub mod win_uwp;
|
||||
pub use self::clash::core::*;
|
||||
pub mod migration;
|
||||
pub mod service;
|
||||
|
133
clash-nyanpasu/backend/tauri/src/core/service/control.rs
Normal file
133
clash-nyanpasu/backend/tauri/src/core/service/control.rs
Normal file
@@ -0,0 +1,133 @@
|
||||
use crate::utils::dirs::{app_config_dir, app_data_dir, app_install_dir};
|
||||
use runas::Command as RunasCommand;
|
||||
|
||||
use super::SERVICE_PATH;
|
||||
|
||||
pub async fn install_service() -> anyhow::Result<()> {
|
||||
let user = {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
nyanpasu_utils::os::get_current_user_sid().await?
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
whoami::username()
|
||||
}
|
||||
};
|
||||
let data_dir = app_data_dir()?;
|
||||
let config_dir = app_config_dir()?;
|
||||
let app_dir = app_install_dir()?;
|
||||
let child = tokio::task::spawn_blocking(move || {
|
||||
RunasCommand::new(SERVICE_PATH.as_path())
|
||||
.args(&[
|
||||
"install",
|
||||
"--user",
|
||||
&user,
|
||||
"--nyanpasu-data-dir",
|
||||
data_dir.to_str().unwrap(),
|
||||
"--nyanpasu-config-dir",
|
||||
config_dir.to_str().unwrap(),
|
||||
"--nyanpasu-app-dir",
|
||||
app_dir.to_str().unwrap(),
|
||||
])
|
||||
.gui(true)
|
||||
.show(true)
|
||||
.status()
|
||||
})
|
||||
.await??;
|
||||
if !child.success() {
|
||||
anyhow::bail!(
|
||||
"failed to install service, exit code: {}",
|
||||
child.code().unwrap()
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn uninstall_service() -> anyhow::Result<()> {
|
||||
let child = tokio::task::spawn_blocking(move || {
|
||||
RunasCommand::new(SERVICE_PATH.as_path())
|
||||
.args(&["uninstall"])
|
||||
.gui(true)
|
||||
.show(true)
|
||||
.status()
|
||||
})
|
||||
.await??;
|
||||
if !child.success() {
|
||||
anyhow::bail!(
|
||||
"failed to uninstall service, exit code: {}",
|
||||
child.code().unwrap()
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn start_service() -> anyhow::Result<()> {
|
||||
let child = tokio::task::spawn_blocking(move || {
|
||||
RunasCommand::new(SERVICE_PATH.as_path())
|
||||
.args(&["start"])
|
||||
.gui(true)
|
||||
.show(true)
|
||||
.status()
|
||||
})
|
||||
.await??;
|
||||
if !child.success() {
|
||||
anyhow::bail!(
|
||||
"failed to start service, exit code: {}",
|
||||
child.code().unwrap()
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn stop_service() -> anyhow::Result<()> {
|
||||
let child = tokio::task::spawn_blocking(move || {
|
||||
RunasCommand::new(SERVICE_PATH.as_path())
|
||||
.args(&["stop"])
|
||||
.gui(true)
|
||||
.show(true)
|
||||
.status()
|
||||
})
|
||||
.await??;
|
||||
if !child.success() {
|
||||
anyhow::bail!(
|
||||
"failed to stop service, exit code: {}",
|
||||
child.code().unwrap()
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn restart_service() -> anyhow::Result<()> {
|
||||
let child = tokio::task::spawn_blocking(move || {
|
||||
RunasCommand::new(SERVICE_PATH.as_path())
|
||||
.args(&["restart"])
|
||||
.gui(true)
|
||||
.show(true)
|
||||
.status()
|
||||
})
|
||||
.await??;
|
||||
if !child.success() {
|
||||
anyhow::bail!(
|
||||
"failed to restart service, exit code: {}",
|
||||
child.code().unwrap()
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn status() -> anyhow::Result<nyanpasu_ipc::types::ServiceStatus> {
|
||||
let child = tokio::process::Command::new(SERVICE_PATH.as_path())
|
||||
.args(["status", "--json"])
|
||||
.output()
|
||||
.await?;
|
||||
if !child.status.success() {
|
||||
anyhow::bail!(
|
||||
"failed to get service status, exit code: {}",
|
||||
child.status.code().unwrap()
|
||||
);
|
||||
}
|
||||
let mut status = String::from_utf8(child.stdout)?;
|
||||
let status: nyanpasu_ipc::types::ServiceStatus = unsafe { simd_json::from_str(&mut status)? };
|
||||
Ok(status)
|
||||
}
|
4
clash-nyanpasu/backend/tauri/src/core/service/ipc.rs
Normal file
4
clash-nyanpasu/backend/tauri/src/core/service/ipc.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub enum IpcState {
|
||||
Connected,
|
||||
Disconnected,
|
||||
}
|
14
clash-nyanpasu/backend/tauri/src/core/service/mod.rs
Normal file
14
clash-nyanpasu/backend/tauri/src/core/service/mod.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::utils::dirs::app_install_dir;
|
||||
|
||||
pub mod control;
|
||||
pub mod ipc;
|
||||
|
||||
const SERVICE_NAME: &str = "nyanpasu-service";
|
||||
static SERVICE_PATH: Lazy<PathBuf> = Lazy::new(|| {
|
||||
let app_path = app_install_dir().unwrap();
|
||||
app_path.join(format!("{}{}", SERVICE_NAME, std::env::consts::EXE_SUFFIX))
|
||||
});
|
@@ -211,7 +211,7 @@ impl Updater {
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
if current_core == self.core_type {
|
||||
tokio::task::spawn_blocking(move || CoreManager::global().stop_core()).await??;
|
||||
CoreManager::global().stop_core().await?;
|
||||
return Ok(());
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
|
@@ -53,6 +53,7 @@ fn deadlock_detection() {
|
||||
}
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
// share the tauri async runtime to nyanpasu-utils
|
||||
#[cfg(feature = "deadlock-detection")]
|
||||
deadlock_detection();
|
||||
|
||||
|
@@ -54,101 +54,89 @@ pub fn get_portable_flag() -> bool {
|
||||
/// initialize portable flag
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn init_portable_flag() -> Result<()> {
|
||||
use tauri::utils::platform::current_exe;
|
||||
|
||||
let exe = current_exe()?;
|
||||
|
||||
if let Some(dir) = exe.parent() {
|
||||
let dir = PathBuf::from(dir).join(".config/PORTABLE");
|
||||
|
||||
if dir.exists() {
|
||||
PORTABLE_FLAG.get_or_init(|| true);
|
||||
return Ok(());
|
||||
}
|
||||
let dir = app_install_dir()?;
|
||||
let portable_file = dir.join(".config/PORTABLE");
|
||||
if portable_file.exists() {
|
||||
PORTABLE_FLAG.get_or_init(|| true);
|
||||
return Ok(());
|
||||
}
|
||||
PORTABLE_FLAG.get_or_init(|| false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn app_config_dir() -> Result<PathBuf> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use tauri::utils::platform::current_exe;
|
||||
|
||||
if *PORTABLE_FLAG.get().unwrap_or(&false) {
|
||||
let app_exe = current_exe()?;
|
||||
let app_exe = dunce::canonicalize(app_exe)?;
|
||||
let app_dir = app_exe
|
||||
.parent()
|
||||
.ok_or(anyhow::anyhow!("failed to check the old portable app dir"))?;
|
||||
return Ok(PathBuf::from(app_dir)
|
||||
.join(".config")
|
||||
.join(PREVIOUS_APP_NAME));
|
||||
} else if let Ok(Some(path)) = super::winreg::get_app_dir() {
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
||||
suggest_config_dir(&APP_DIR_PLACEHOLDER)
|
||||
.ok_or(anyhow::anyhow!("failed to get the app config dir"))
|
||||
.and_then(|dir| {
|
||||
if !dir.exists() {
|
||||
fs::create_dir_all(&dir)?;
|
||||
let path: Option<PathBuf> = {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if *PORTABLE_FLAG.get().unwrap_or(&false) {
|
||||
let app_dir = app_install_dir()?;
|
||||
Some(app_dir.join(".config").join(PREVIOUS_APP_NAME))
|
||||
} else if let Ok(Some(path)) = super::winreg::get_app_dir() {
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
Ok(dir)
|
||||
})
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
match path {
|
||||
Some(path) => Ok(path),
|
||||
None => suggest_config_dir(&APP_DIR_PLACEHOLDER)
|
||||
.ok_or(anyhow::anyhow!("failed to get the app config dir")),
|
||||
}
|
||||
.and_then(|dir| {
|
||||
if !dir.exists() {
|
||||
fs::create_dir_all(&dir)?;
|
||||
}
|
||||
Ok(dir)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn app_data_dir() -> Result<PathBuf> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use tauri::utils::platform::current_exe;
|
||||
|
||||
if *PORTABLE_FLAG.get().unwrap_or(&false) {
|
||||
let app_exe = current_exe()?;
|
||||
let app_exe = dunce::canonicalize(app_exe)?;
|
||||
let app_dir = app_exe
|
||||
.parent()
|
||||
.ok_or(anyhow::anyhow!("failed to check the old portable app dir"))?;
|
||||
|
||||
let data_dir = PathBuf::from(app_dir).join(".data").join(PREVIOUS_APP_NAME);
|
||||
|
||||
if !data_dir.exists() {
|
||||
fs::create_dir_all(&data_dir)?;
|
||||
let path: Option<PathBuf> = {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if *PORTABLE_FLAG.get().unwrap_or(&false) {
|
||||
let app_dir = app_install_dir()?;
|
||||
Some(app_dir.join(".data").join(PREVIOUS_APP_NAME))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
return Ok(data_dir);
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
suggest_data_dir(&APP_DIR_PLACEHOLDER)
|
||||
.ok_or(anyhow::anyhow!("failed to get the app data dir"))
|
||||
.and_then(|dir| {
|
||||
if !dir.exists() {
|
||||
fs::create_dir_all(&dir)?;
|
||||
}
|
||||
Ok(dir)
|
||||
})
|
||||
match path {
|
||||
Some(path) => Ok(path),
|
||||
None => suggest_data_dir(&APP_DIR_PLACEHOLDER)
|
||||
.ok_or(anyhow::anyhow!("failed to get the app data dir")),
|
||||
}
|
||||
.and_then(|dir| {
|
||||
if !dir.exists() {
|
||||
fs::create_dir_all(&dir)?;
|
||||
}
|
||||
Ok(dir)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn old_app_home_dir() -> Result<PathBuf> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use tauri::utils::platform::current_exe;
|
||||
|
||||
if !PORTABLE_FLAG.get().unwrap_or(&false) {
|
||||
Ok(home_dir()
|
||||
.ok_or(anyhow::anyhow!("failed to check old app home dir"))?
|
||||
.join(".config")
|
||||
.join(PREVIOUS_APP_NAME))
|
||||
} else {
|
||||
let app_exe = current_exe()?;
|
||||
let app_exe = dunce::canonicalize(app_exe)?;
|
||||
let app_dir = app_exe
|
||||
.parent()
|
||||
.ok_or(anyhow::anyhow!("failed to check the old portable app dir"))?;
|
||||
Ok(PathBuf::from(app_dir)
|
||||
.join(".config")
|
||||
.join(PREVIOUS_APP_NAME))
|
||||
let app_dir = app_install_dir()?;
|
||||
Ok(app_dir.join(".config").join(PREVIOUS_APP_NAME))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +163,6 @@ pub fn app_home_dir() -> Result<PathBuf> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use crate::utils::winreg::get_app_dir;
|
||||
use tauri::utils::platform::current_exe;
|
||||
if !PORTABLE_FLAG.get().unwrap_or(&false) {
|
||||
let reg_app_dir = get_app_dir()?;
|
||||
if let Some(reg_app_dir) = reg_app_dir {
|
||||
@@ -186,13 +173,7 @@ pub fn app_home_dir() -> Result<PathBuf> {
|
||||
.join(".config")
|
||||
.join(APP_NAME));
|
||||
}
|
||||
|
||||
let app_exe = current_exe()?;
|
||||
let app_exe = dunce::canonicalize(app_exe)?;
|
||||
let app_dir = app_exe
|
||||
.parent()
|
||||
.ok_or(anyhow::anyhow!("failed to get the portable app dir"))?;
|
||||
Ok(PathBuf::from(app_dir).join(".config").join(APP_NAME))
|
||||
Ok((app_install_dir()?).join(".config").join(APP_NAME))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@@ -215,6 +196,30 @@ pub fn app_resources_dir() -> Result<PathBuf> {
|
||||
Err(anyhow::anyhow!("failed to get the resource dir"))
|
||||
}
|
||||
|
||||
/// Cache dir, it safe to clean up
|
||||
pub fn cache_dir() -> Result<PathBuf> {
|
||||
let mut dir = dirs::cache_dir()
|
||||
.ok_or(anyhow::anyhow!("failed to get the cache dir"))?
|
||||
.join(APP_DIR_PLACEHOLDER.as_ref());
|
||||
if cfg!(windows) {
|
||||
dir.push("cache");
|
||||
}
|
||||
if !dir.exists() {
|
||||
fs::create_dir_all(&dir)?;
|
||||
}
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
/// App install dir, sidecars should placed here
|
||||
pub fn app_install_dir() -> Result<PathBuf> {
|
||||
let exe = tauri::utils::platform::current_exe()?;
|
||||
let exe = dunce::canonicalize(exe)?;
|
||||
let dir = exe
|
||||
.parent()
|
||||
.ok_or(anyhow::anyhow!("failed to get the app install dir"))?;
|
||||
Ok(PathBuf::from(dir))
|
||||
}
|
||||
|
||||
/// profiles dir
|
||||
pub fn app_profiles_dir() -> Result<PathBuf> {
|
||||
Ok(app_config_dir()?.join("profiles"))
|
||||
|
@@ -15,7 +15,7 @@ use anyhow::Result;
|
||||
use semver::Version;
|
||||
use serde_yaml::Mapping;
|
||||
use std::net::TcpListener;
|
||||
use tauri::{api::process::Command, App, AppHandle, Manager};
|
||||
use tauri::{api::process::Command, async_runtime::block_on, App, AppHandle, Manager};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn set_window_controls_pos(window: cocoa::base::id, x: f64, y: f64) {
|
||||
@@ -139,7 +139,7 @@ pub fn resolve_setup(app: &mut App) {
|
||||
/// reset system proxy
|
||||
pub fn resolve_reset() {
|
||||
log_err!(sysopt::Sysopt::global().reset_sysproxy());
|
||||
log_err!(CoreManager::global().stop_core());
|
||||
log_err!(block_on(CoreManager::global().stop_core()));
|
||||
}
|
||||
|
||||
/// create main window
|
||||
|
@@ -30,7 +30,8 @@
|
||||
"sidecar/clash",
|
||||
"sidecar/mihomo",
|
||||
"sidecar/mihomo-alpha",
|
||||
"sidecar/clash-rs"
|
||||
"sidecar/clash-rs",
|
||||
"sidecar/nyanpasu-service"
|
||||
],
|
||||
"copyright": "© 2024 Clash Nyanpasu All Rights Reserved",
|
||||
"category": "DeveloperTool",
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import useSWR from "swr";
|
||||
import { Clash, clash } from "../service/clash";
|
||||
import * as tauri from "@/service/tauri";
|
||||
import { ClashConfig, Profile } from "..";
|
||||
import { Clash, clash } from "../service/clash";
|
||||
|
||||
/**
|
||||
* useClash with swr.
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import useSWR from "swr";
|
||||
import {
|
||||
Clash,
|
||||
clash as clashApi,
|
||||
ProviderItem,
|
||||
ProviderRules,
|
||||
clash as clashApi,
|
||||
} from "@/service";
|
||||
import * as tauri from "@/service/tauri";
|
||||
import useSWR from "swr";
|
||||
|
||||
export const useClashCore = () => {
|
||||
const { getGroupDelay, getProxiesDelay, ...clash } = clashApi();
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { useWebSocket } from "ahooks";
|
||||
import { useClash } from "./useClash";
|
||||
import { useMemo } from "react";
|
||||
import { useClash } from "./useClash";
|
||||
|
||||
export const useClashWS = () => {
|
||||
const { getClashInfo } = useClash();
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { useMemo } from "react";
|
||||
import useSWR from "swr";
|
||||
import * as service from "@/service";
|
||||
import { VergeConfig } from "@/service";
|
||||
import { fetchCoreVersion, fetchLatestCore } from "@/service/core";
|
||||
import { useClash } from "./useClash";
|
||||
import { useMemo } from "react";
|
||||
import * as tauri from "@/service/tauri";
|
||||
import { useClash } from "./useClash";
|
||||
|
||||
/**
|
||||
* useNyanpasu with swr.
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import useSWR from "swr";
|
||||
import { ofetch } from "ofetch";
|
||||
import useSWR from "swr";
|
||||
|
||||
interface IPSBResponse {
|
||||
organization: string;
|
||||
|
@@ -56,7 +56,7 @@
|
||||
"@vitejs/plugin-react-swc": "3.7.0",
|
||||
"clsx": "2.1.1",
|
||||
"sass": "1.77.8",
|
||||
"shiki": "1.11.1",
|
||||
"shiki": "1.11.2",
|
||||
"tailwindcss-textshadow": "2.1.3",
|
||||
"vite": "5.3.5",
|
||||
"vite-plugin-monaco-editor": "1.1.3",
|
||||
|
@@ -1,13 +1,13 @@
|
||||
import { Allotment } from "allotment";
|
||||
import "allotment/dist/style.css";
|
||||
import { ReactNode } from "react";
|
||||
import getSystem from "@/utils/get-system";
|
||||
import { alpha, useTheme } from "@mui/material";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import { appWindow } from "@tauri-apps/api/window";
|
||||
import { ReactNode } from "react";
|
||||
import { LayoutControl } from "../layout/layout-control";
|
||||
import styles from "./app-container.module.scss";
|
||||
import AppDrawer from "./app-drawer";
|
||||
import { alpha, useTheme } from "@mui/material";
|
||||
import { Allotment } from "allotment";
|
||||
import "allotment/dist/style.css";
|
||||
import DrawerContent from "./drawer-content";
|
||||
|
||||
const OS = getSystem();
|
||||
@@ -51,12 +51,12 @@ export const AppContainer = ({
|
||||
|
||||
<Allotment.Pane visible={true} className={styles.container}>
|
||||
{OS === "windows" && (
|
||||
<LayoutControl className="fixed right-6 top-1.5 !z-top" />
|
||||
<LayoutControl className="!z-top fixed right-6 top-1.5" />
|
||||
)}
|
||||
|
||||
{OS === "macos" && (
|
||||
<div
|
||||
className="fixed z-top left-6 top-3 h-8 w-[4.5rem] rounded-full"
|
||||
className="z-top fixed left-6 top-3 h-8 w-[4.5rem] rounded-full"
|
||||
style={{ backgroundColor: alpha(palette.primary.main, 0.1) }}
|
||||
/>
|
||||
)}
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { MenuOpen } from "@mui/icons-material";
|
||||
import { Backdrop, IconButton, alpha, useTheme } from "@mui/material";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useState } from "react";
|
||||
import AnimatedLogo from "../layout/animated-logo";
|
||||
import { classNames } from "@/utils";
|
||||
import getSystem from "@/utils/get-system";
|
||||
import { MenuOpen } from "@mui/icons-material";
|
||||
import { alpha, Backdrop, IconButton, useTheme } from "@mui/material";
|
||||
import AnimatedLogo from "../layout/animated-logo";
|
||||
import DrawerContent from "./drawer-content";
|
||||
|
||||
export const AppDrawer = () => {
|
||||
@@ -16,7 +16,7 @@ export const AppDrawer = () => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"flex items-center gap-2 fixed z-10",
|
||||
"fixed z-10 flex items-center gap-2",
|
||||
getSystem() === "macos" ? "left-[6.5rem] top-3" : "left-6 top-1.5",
|
||||
)}
|
||||
data-windrag
|
||||
@@ -33,7 +33,7 @@ export const AppDrawer = () => {
|
||||
</IconButton>
|
||||
|
||||
<div className="size-5" data-windrag>
|
||||
<AnimatedLogo className="w-full h-full" data-windrag />
|
||||
<AnimatedLogo className="h-full w-full" data-windrag />
|
||||
</div>
|
||||
|
||||
<div className="text-lg" data-windrag>
|
||||
@@ -56,7 +56,7 @@ export const AppDrawer = () => {
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<AnimatePresence initial={false}>
|
||||
<div className="w-full h-full">
|
||||
<div className="h-full w-full">
|
||||
<motion.div
|
||||
className="h-full"
|
||||
animate={open ? "open" : "closed"}
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import getSystem from "@/utils/get-system";
|
||||
import clsx from "clsx";
|
||||
import AnimatedLogo from "../layout/animated-logo";
|
||||
import { getRoutesWithIcon } from "@/utils/routes-utils";
|
||||
import RouteListItem from "./modules/route-list-item";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useSize } from "ahooks";
|
||||
import clsx from "clsx";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import getSystem from "@/utils/get-system";
|
||||
import { languageQuirks } from "@/utils/language";
|
||||
import { getRoutesWithIcon } from "@/utils/routes-utils";
|
||||
import { useNyanpasu } from "@nyanpasu/interface";
|
||||
import AnimatedLogo from "../layout/animated-logo";
|
||||
import RouteListItem from "./modules/route-list-item";
|
||||
|
||||
export const DrawerContent = ({ className }: { className?: string }) => {
|
||||
const [onlyIcon, setOnlyIcon] = useState(false);
|
||||
@@ -59,14 +59,14 @@ export const DrawerContent = ({ className }: { className?: string }) => {
|
||||
}}
|
||||
data-windrag
|
||||
>
|
||||
<div className="flex items-center justify-center gap-4 mx-2">
|
||||
<div className=" h-full max-w-28 max-h-28" data-windrag>
|
||||
<AnimatedLogo className="w-full h-full" data-windrag />
|
||||
<div className="mx-2 flex items-center justify-center gap-4">
|
||||
<div className="h-full max-h-28 max-w-28" data-windrag>
|
||||
<AnimatedLogo className="h-full w-full" data-windrag />
|
||||
</div>
|
||||
|
||||
{!onlyIcon && (
|
||||
<div
|
||||
className="text-lg font-bold mt-1 mr-1 whitespace-pre-wrap"
|
||||
className="mr-1 mt-1 whitespace-pre-wrap text-lg font-bold"
|
||||
data-windrag
|
||||
>
|
||||
{"Clash\nNyanpasu"}
|
||||
@@ -74,7 +74,7 @@ export const DrawerContent = ({ className }: { className?: string }) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 overflow-y-auto scrollbar-hidden !overflow-x-hidden">
|
||||
<div className="scrollbar-hidden flex flex-col gap-2 overflow-y-auto !overflow-x-hidden">
|
||||
{Object.entries(routes).map(([name, { path, icon }]) => {
|
||||
return (
|
||||
<RouteListItem
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { useNyanpasu } from "@nyanpasu/interface";
|
||||
import { locale } from "dayjs";
|
||||
import { changeLanguage } from "i18next";
|
||||
import { useEffect } from "react";
|
||||
import { useNyanpasu } from "@nyanpasu/interface";
|
||||
|
||||
export const LocalesProvider = () => {
|
||||
const { nyanpasuConfig } = useNyanpasu();
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { classNames } from "@/utils";
|
||||
import { languageQuirks } from "@/utils/language";
|
||||
import { SvgIconComponent } from "@mui/icons-material";
|
||||
import { ListItemButton, ListItemIcon, alpha, useTheme } from "@mui/material";
|
||||
import { useNyanpasu } from "@nyanpasu/interface";
|
||||
import { createElement } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMatch, useNavigate } from "react-router-dom";
|
||||
import { classNames } from "@/utils";
|
||||
import { languageQuirks } from "@/utils/language";
|
||||
import { SvgIconComponent } from "@mui/icons-material";
|
||||
import { alpha, ListItemButton, ListItemIcon, useTheme } from "@mui/material";
|
||||
import { useNyanpasu } from "@nyanpasu/interface";
|
||||
|
||||
export const RouteListItem = ({
|
||||
name,
|
||||
@@ -31,7 +31,7 @@ export const RouteListItem = ({
|
||||
return (
|
||||
<ListItemButton
|
||||
className={classNames(
|
||||
onlyIcon ? "!rounded-3xl !size-16 !mx-auto" : "!pr-14 !rounded-full",
|
||||
onlyIcon ? "!mx-auto !size-16 !rounded-3xl" : "!rounded-full !pr-14",
|
||||
)}
|
||||
sx={{
|
||||
backgroundColor: match
|
||||
@@ -56,7 +56,7 @@ export const RouteListItem = ({
|
||||
{!onlyIcon && (
|
||||
<div
|
||||
className={classNames(
|
||||
"pt-1 pb-1 w-full text-nowrap",
|
||||
"w-full text-nowrap pb-1 pt-1",
|
||||
nyanpasuConfig?.language &&
|
||||
languageQuirks[nyanpasuConfig?.language].drawer.itemClassNames,
|
||||
)}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { alpha, Box, Typography } from "@mui/material";
|
||||
import { InboxRounded } from "@mui/icons-material";
|
||||
import { alpha, Box, Typography } from "@mui/material";
|
||||
|
||||
interface Props {
|
||||
text?: React.ReactNode;
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { CheckCircleRounded, Close, ErrorRounded } from "@mui/icons-material";
|
||||
import { Box, IconButton, Slide, Snackbar, Typography } from "@mui/material";
|
||||
import { ReactNode, useState } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { CheckCircleRounded, Close, ErrorRounded } from "@mui/icons-material";
|
||||
import { Box, IconButton, Slide, Snackbar, Typography } from "@mui/material";
|
||||
|
||||
interface InnerProps {
|
||||
type: string;
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { ReactNode } from "react";
|
||||
import { classNames } from "@/utils";
|
||||
import { Public } from "@mui/icons-material";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export interface ContentDisplayProps {
|
||||
className?: string;
|
||||
@@ -15,7 +15,7 @@ export const ContentDisplay = ({
|
||||
}: ContentDisplayProps) => (
|
||||
<div
|
||||
className={classNames(
|
||||
"h-full w-full flex items-center justify-center",
|
||||
"flex h-full w-full items-center justify-center",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Close } from "@mui/icons-material";
|
||||
import { Tooltip } from "@mui/material";
|
||||
import { useClash } from "@nyanpasu/interface";
|
||||
import { FloatingButton } from "@nyanpasu/ui";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const CloseConnectionsButton = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -17,7 +17,7 @@ export const CloseConnectionsButton = () => {
|
||||
return (
|
||||
<Tooltip title={t("Close All")}>
|
||||
<FloatingButton onClick={onCloseAll}>
|
||||
<Close className="!size-8 absolute" />
|
||||
<Close className="absolute !size-8" />
|
||||
</FloatingButton>
|
||||
</Tooltip>
|
||||
);
|
||||
|
@@ -1,17 +1,17 @@
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
import { useLockFn } from "ahooks";
|
||||
import dayjs from "dayjs";
|
||||
import {
|
||||
MaterialReactTable,
|
||||
useMaterialReactTable,
|
||||
type MRT_ColumnDef,
|
||||
} from "material-react-table";
|
||||
import { useClashWS, Connection, useClash } from "@nyanpasu/interface";
|
||||
import dayjs from "dayjs";
|
||||
import { useRef, useMemo } from "react";
|
||||
import { useMemo, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { containsSearchTerm } from "@/utils";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
import Cancel from "@mui/icons-material/Cancel";
|
||||
import { IconButton } from "@mui/material";
|
||||
import { containsSearchTerm } from "@/utils";
|
||||
import { Connection, useClash, useClashWS } from "@nyanpasu/interface";
|
||||
import ContentDisplay from "../base/content-display";
|
||||
|
||||
export type TableConnection = Connection.Item & {
|
||||
@@ -85,7 +85,7 @@ export const ConnectionsTable = ({ searchTerm }: { searchTerm?: string }) => {
|
||||
enableSorting: false,
|
||||
enableGlobalFilter: false,
|
||||
accessorFn: ({ id }) => (
|
||||
<div className="w-full flex justify-center">
|
||||
<div className="flex w-full justify-center">
|
||||
<IconButton
|
||||
color="primary"
|
||||
className="size-5"
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
alpha,
|
||||
FilledInputProps,
|
||||
TextField,
|
||||
TextFieldProps,
|
||||
alpha,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const HeaderSearch = (props: TextFieldProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
@@ -1,3 +1,6 @@
|
||||
import { useInterval } from "ahooks";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Dataline, { DatalineProps } from "@/components/dashboard/dataline";
|
||||
import {
|
||||
ArrowDownward,
|
||||
@@ -13,9 +16,6 @@ import {
|
||||
useClashWS,
|
||||
useNyanpasu,
|
||||
} from "@nyanpasu/interface";
|
||||
import { useInterval } from "ahooks";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const DataPanel = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -125,7 +125,7 @@ export const DataPanel = () => {
|
||||
return Datalines.map((props, index) => {
|
||||
return (
|
||||
<Grid key={`data-${index}`} {...gridLayout} className="w-full">
|
||||
<Dataline {...props} className="min-h-48 max-h-1/8" />
|
||||
<Dataline {...props} className="max-h-1/8 min-h-48" />
|
||||
</Grid>
|
||||
);
|
||||
});
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { cloneElement, FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
import { SvgIconComponent } from "@mui/icons-material";
|
||||
import { Paper } from "@mui/material";
|
||||
import { Sparkline } from "@nyanpasu/ui";
|
||||
import { FC, cloneElement } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface DatalineProps {
|
||||
data: number[];
|
||||
@@ -23,22 +23,22 @@ export const Dataline: FC<DatalineProps> = ({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Paper className="!rounded-3xl relative">
|
||||
<Paper className="relative !rounded-3xl">
|
||||
<Sparkline data={data} className="rounded-3xl" />
|
||||
|
||||
<div className="absolute top-0 p-4 h-full flex flex-col gap-4 justify-between">
|
||||
<div className="absolute top-0 flex h-full flex-col justify-between gap-4 p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
{cloneElement(icon)}
|
||||
|
||||
<div className="font-bold">{title}</div>
|
||||
</div>
|
||||
|
||||
<div className="font-bold text-2xl text-shadow-md">
|
||||
<div className="text-shadow-md text-2xl font-bold">
|
||||
{type === "raw" ? data.at(-1) : parseTraffic(data.at(-1)).join(" ")}
|
||||
{type === "speed" && "/s"}
|
||||
</div>
|
||||
|
||||
<div className=" h-5">
|
||||
<div className="h-5">
|
||||
{total !== undefined && (
|
||||
<span className="text-shadow-sm">
|
||||
{t("Total")}: {parseTraffic(total).join(" ")}
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { useInterval } from "ahooks";
|
||||
import { countryCodeEmoji } from "country-code-emoji";
|
||||
import { useRef, useState } from "react";
|
||||
import { Visibility, VisibilityOff } from "@mui/icons-material";
|
||||
import { CircularProgress, IconButton, Paper, Tooltip } from "@mui/material";
|
||||
import Grid from "@mui/material/Unstable_Grid2";
|
||||
import { timing, useIPSB } from "@nyanpasu/interface";
|
||||
import { useInterval } from "ahooks";
|
||||
import { useRef, useState } from "react";
|
||||
import { countryCodeEmoji } from "country-code-emoji";
|
||||
import { Visibility, VisibilityOff } from "@mui/icons-material";
|
||||
import { cn } from "@nyanpasu/ui";
|
||||
import { getColorForDelay } from "../proxies/utils";
|
||||
|
||||
@@ -57,12 +57,12 @@ export const HealthPanel = () => {
|
||||
|
||||
return (
|
||||
<Grid sm={12} md={8} lg={6} xl={4} className="w-full">
|
||||
<Paper className="!rounded-3xl relative">
|
||||
<div className="p-4 flex justify-between gap-8">
|
||||
<Paper className="relative !rounded-3xl">
|
||||
<div className="flex justify-between gap-8 p-4">
|
||||
<div className="flex flex-col justify-between">
|
||||
{Object.entries(health).map(([name, value]) => {
|
||||
return (
|
||||
<div key={name} className="flex gap-1 justify-between">
|
||||
<div key={name} className="flex justify-between gap-1">
|
||||
<div className="min-w-20 font-bold">{name}:</div>
|
||||
|
||||
<div
|
||||
@@ -76,11 +76,11 @@ export const HealthPanel = () => {
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center gap-4 flex-1 relative select-text">
|
||||
<div className="relative flex flex-1 select-text justify-center gap-4">
|
||||
{data && (
|
||||
<>
|
||||
<div className="text-5xl relative">
|
||||
<span className="blur opacity-50">
|
||||
<div className="relative text-5xl">
|
||||
<span className="opacity-50 blur">
|
||||
{countryCodeEmoji(data.country_code)}
|
||||
</span>
|
||||
|
||||
@@ -90,7 +90,7 @@ export const HealthPanel = () => {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="text-xl font-bold text-shadow-md flex justify-between items-end">
|
||||
<div className="text-shadow-md flex items-end justify-between text-xl font-bold">
|
||||
<div>{data.country}</div>
|
||||
|
||||
<Tooltip title="Click to Refresh Now">
|
||||
@@ -111,8 +111,8 @@ export const HealthPanel = () => {
|
||||
|
||||
<div className="text-sm">AS{data.asn}</div>
|
||||
|
||||
<div className="w-full flex gap-4 items-center">
|
||||
<div className="font-mono relative">
|
||||
<div className="flex w-full items-center gap-4">
|
||||
<div className="relative font-mono">
|
||||
<span
|
||||
className={cn(
|
||||
"transition-opacity",
|
||||
@@ -124,10 +124,10 @@ export const HealthPanel = () => {
|
||||
|
||||
<span
|
||||
className={cn(
|
||||
"bg-slate-300 absolute w-full h-full left-0 transition-opacity rounded-lg",
|
||||
"absolute left-0 h-full w-full rounded-lg bg-slate-300 transition-opacity",
|
||||
showIPAddress
|
||||
? "opacity-0"
|
||||
: "opacity-100 animate-pulse",
|
||||
: "animate-pulse opacity-100",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { AnimatePresence, motion, Variants } from "framer-motion";
|
||||
import { CSSProperties } from "react";
|
||||
import LogoSvg from "@/assets/image/logo.svg?react";
|
||||
import { classNames } from "@/utils";
|
||||
import { useNyanpasu } from "@nyanpasu/interface";
|
||||
import { AnimatePresence, Variants, motion } from "framer-motion";
|
||||
import { CSSProperties } from "react";
|
||||
import styles from "./animated-logo.module.scss";
|
||||
|
||||
const Logo = motion(LogoSvg);
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import { debounce } from "lodash-es";
|
||||
import { useEffect, useState } from "react";
|
||||
import { NotificationType, useNotification } from "@/hooks/use-notification";
|
||||
import { classNames } from "@/utils";
|
||||
import {
|
||||
@@ -10,8 +12,6 @@ import { alpha, Button, ButtonProps, useTheme } from "@mui/material";
|
||||
import { save_window_size_state } from "@nyanpasu/interface";
|
||||
import { platform, type Platform } from "@tauri-apps/api/os";
|
||||
import { appWindow } from "@tauri-apps/api/window";
|
||||
import { debounce } from "lodash-es";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const CtrlButton = (props: ButtonProps) => {
|
||||
const { palette } = useTheme();
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { NotificationType, useNotification } from "@/hooks/use-notification";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { NotificationType, useNotification } from "@/hooks/use-notification";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
|
||||
export const NoticeProvider = () => {
|
||||
const { t } = useTranslation();
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { AnimatePresence, motion, Variant } from "framer-motion";
|
||||
import { useLocation, useOutlet } from "react-router-dom";
|
||||
import { classNames } from "@/utils";
|
||||
import { useNyanpasu } from "@nyanpasu/interface";
|
||||
import { AnimatePresence, Variant, motion } from "framer-motion";
|
||||
import { useLocation, useOutlet } from "react-router-dom";
|
||||
|
||||
type PageVariantKey = "initial" | "visible" | "hidden";
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
|
||||
export const SchemeProvider = () => {
|
||||
const navigate = useNavigate();
|
||||
|
@@ -1,13 +1,13 @@
|
||||
import { useWhyDidYouUpdate } from "ahooks";
|
||||
import { useAtomValue, useSetAtom } from "jotai";
|
||||
import { mergeWith } from "lodash-es";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { alpha, darken, lighten, Theme, useColorScheme } from "@mui/material";
|
||||
import { appWindow } from "@tauri-apps/api/window";
|
||||
import { defaultTheme } from "@/pages/_theme";
|
||||
import { themeMode as themeModeAtom } from "@/store";
|
||||
import { alpha, darken, lighten, Theme, useColorScheme } from "@mui/material";
|
||||
import { useNyanpasu } from "@nyanpasu/interface";
|
||||
import { createMDYTheme } from "@nyanpasu/ui";
|
||||
import { useAtomValue, useSetAtom } from "jotai";
|
||||
import { themeMode as themeModeAtom } from "@/store";
|
||||
import { useWhyDidYouUpdate } from "ahooks";
|
||||
import { mergeWith } from "lodash-es";
|
||||
import { appWindow } from "@tauri-apps/api/window";
|
||||
|
||||
const applyRootStyleVar = (mode: "light" | "dark", theme: Theme) => {
|
||||
const root = document.documentElement;
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { useSetAtom } from "jotai";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { atomLogData } from "@/store";
|
||||
import { Close } from "@mui/icons-material";
|
||||
import { Tooltip } from "@mui/material";
|
||||
import { FloatingButton } from "@nyanpasu/ui";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const ClearLogButton = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -17,7 +17,7 @@ export const ClearLogButton = () => {
|
||||
return (
|
||||
<Tooltip title={t("Clear")}>
|
||||
<FloatingButton onClick={onClear}>
|
||||
<Close className="!size-8 absolute" />
|
||||
<Close className="absolute !size-8" />
|
||||
</FloatingButton>
|
||||
</Tooltip>
|
||||
);
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { FilledInputProps, TextField, alpha, useTheme } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { alpha, FilledInputProps, TextField, useTheme } from "@mui/material";
|
||||
|
||||
export interface LogFilterProps {
|
||||
value: string;
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { useAsyncEffect } from "ahooks";
|
||||
import { useState } from "react";
|
||||
import { classNames } from "@/utils";
|
||||
import { formatAnsi } from "@/utils/shiki";
|
||||
import { useTheme } from "@mui/material";
|
||||
import { LogMessage } from "@nyanpasu/interface";
|
||||
import { useAsyncEffect } from "ahooks";
|
||||
import { useState } from "react";
|
||||
import styles from "./log-item.module.scss";
|
||||
import { classNames } from "@/utils";
|
||||
|
||||
export const LogItem = ({ value }: { value: LogMessage }) => {
|
||||
const { palette } = useTheme();
|
||||
@@ -22,7 +22,7 @@ export const LogItem = ({ value }: { value: LogMessage }) => {
|
||||
}, [value.payload]);
|
||||
|
||||
return (
|
||||
<div className="w-full font-mono p-4 pt-2 pb-0">
|
||||
<div className="w-full p-4 pb-0 pt-2 font-mono">
|
||||
<div className="flex gap-2">
|
||||
<span className="font-thin">{value.time}</span>
|
||||
|
||||
@@ -36,7 +36,7 @@ export const LogItem = ({ value }: { value: LogMessage }) => {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="text-wrap border-slate-200 border-b pb-2">
|
||||
<div className="text-wrap border-b border-slate-200 pb-2">
|
||||
<p
|
||||
className={classNames(
|
||||
styles.item,
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Button, Menu, MenuItem, alpha, useTheme } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import { alpha, Button, Menu, MenuItem, useTheme } from "@mui/material";
|
||||
|
||||
export interface LogLevelProps {
|
||||
value: string;
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { LogMessage } from "@nyanpasu/interface";
|
||||
import { useDebounceEffect } from "ahooks";
|
||||
import { useRef } from "react";
|
||||
import { VList, VListHandle } from "virtua";
|
||||
import { LogMessage } from "@nyanpasu/interface";
|
||||
import LogItem from "./log-item";
|
||||
|
||||
export const LogList = ({ data }: { data: LogMessage[] }) => {
|
||||
@@ -25,7 +25,7 @@ export const LogList = ({ data }: { data: LogMessage[] }) => {
|
||||
return (
|
||||
<VList
|
||||
ref={vListRef}
|
||||
className="flex flex-col gap-2 p-2 overflow-auto select-text min-h-full"
|
||||
className="flex min-h-full select-text flex-col gap-2 overflow-auto p-2"
|
||||
reverse
|
||||
>
|
||||
{data.map((item, index) => {
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { atomLogData } from "@/store";
|
||||
import { LogMessage, useClashWS } from "@nyanpasu/interface";
|
||||
import dayjs from "dayjs";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { useEffect } from "react";
|
||||
import { atomLogData } from "@/store";
|
||||
import { LogMessage, useClashWS } from "@nyanpasu/interface";
|
||||
|
||||
const MAX_LOG_NUM = 1000;
|
||||
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { atomEnableLog } from "@/store";
|
||||
import { IconButton } from "@mui/material";
|
||||
import { useAtom } from "jotai";
|
||||
import { atomEnableLog } from "@/store";
|
||||
import {
|
||||
PauseCircleOutlineRounded,
|
||||
PlayCircleOutlineRounded,
|
||||
} from "@mui/icons-material";
|
||||
import { IconButton } from "@mui/material";
|
||||
|
||||
export const LogToggle = () => {
|
||||
const [enableLog, setEnableLog] = useAtom(atomEnableLog);
|
||||
|
@@ -1,16 +1,16 @@
|
||||
import { Edit, Add } from "@mui/icons-material";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { memo } from "react";
|
||||
import { Add, Edit } from "@mui/icons-material";
|
||||
import {
|
||||
ListItemButton,
|
||||
alpha,
|
||||
ListItemText,
|
||||
IconButton,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { Profile, useClash } from "@nyanpasu/interface";
|
||||
import { filterProfiles } from "../utils";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { memo } from "react";
|
||||
|
||||
const ChainItem = memo(function ChainItem({
|
||||
name,
|
||||
@@ -29,7 +29,7 @@ const ChainItem = memo(function ChainItem({
|
||||
|
||||
return (
|
||||
<ListItemButton
|
||||
className="!mt-2 !mb-2"
|
||||
className="!mb-2 !mt-2"
|
||||
sx={{
|
||||
backgroundColor: selected
|
||||
? alpha(palette.primary.main, 0.3)
|
||||
@@ -91,7 +91,7 @@ export const SideChain = ({ global, profile, onChainEdit }: SideChainProps) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="!pl-2 !pr-2 overflow-auto h-full">
|
||||
<div className="h-full overflow-auto !pl-2 !pr-2">
|
||||
{scripts?.map((item, index) => {
|
||||
const selected = global
|
||||
? getProfiles.data?.chain?.includes(item.uid)
|
||||
@@ -110,7 +110,7 @@ export const SideChain = ({ global, profile, onChainEdit }: SideChainProps) => {
|
||||
})}
|
||||
|
||||
<ListItemButton
|
||||
className="!mt-2 !mb-2"
|
||||
className="!mb-2 !mt-2"
|
||||
sx={{
|
||||
backgroundColor: alpha(palette.secondary.main, 0.1),
|
||||
borderRadius: 4,
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { isEmpty } from "lodash-es";
|
||||
import { memo } from "react";
|
||||
import { VList } from "virtua";
|
||||
import { classNames } from "@/utils";
|
||||
import { RamenDining, Terminal } from "@mui/icons-material";
|
||||
import { Divider } from "@mui/material";
|
||||
import { useClash } from "@nyanpasu/interface";
|
||||
import { memo } from "react";
|
||||
import { isEmpty } from "lodash-es";
|
||||
import { VList } from "virtua";
|
||||
import { filterProfiles } from "../utils";
|
||||
import { classNames } from "@/utils";
|
||||
|
||||
const LogListItem = memo(function LogListItem({
|
||||
name,
|
||||
@@ -20,7 +20,7 @@ const LogListItem = memo(function LogListItem({
|
||||
<>
|
||||
{showDivider && <Divider />}
|
||||
|
||||
<div className="w-full font-mono break-all">
|
||||
<div className="w-full break-all font-mono">
|
||||
<span className="text-red-500">[{name}]: </span>
|
||||
<span>{item}</span>
|
||||
</div>
|
||||
@@ -39,7 +39,7 @@ export const SideLog = ({ className }: SideLogProps) => {
|
||||
|
||||
return (
|
||||
<div className={classNames("w-full", className)}>
|
||||
<div className="p-2 pl-4 flex justify-between items-center">
|
||||
<div className="flex items-center justify-between p-2 pl-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal />
|
||||
|
||||
@@ -49,7 +49,7 @@ export const SideLog = ({ className }: SideLogProps) => {
|
||||
|
||||
<Divider />
|
||||
|
||||
<VList className="flex flex-col gap-2 p-2 overflow-auto select-text">
|
||||
<VList className="flex select-text flex-col gap-2 overflow-auto p-2">
|
||||
{!isEmpty(getRuntimeLogs.data) ? (
|
||||
Object.entries(getRuntimeLogs.data).map(([uid, content]) => {
|
||||
return content.map((item, index) => {
|
||||
@@ -66,7 +66,7 @@ export const SideLog = ({ className }: SideLogProps) => {
|
||||
});
|
||||
})
|
||||
) : (
|
||||
<div className="w-full h-full min-h-48 flex flex-col justify-center items-center">
|
||||
<div className="flex h-full min-h-48 w-full flex-col items-center justify-center">
|
||||
<RamenDining className="!size-10" />
|
||||
<p>No Log</p>
|
||||
</div>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { Add } from "@mui/icons-material";
|
||||
import { FloatingButton } from "@nyanpasu/ui";
|
||||
import { useState } from "react";
|
||||
import { ProfileDialog } from "./profile-dialog";
|
||||
|
||||
export const NewProfileButton = () => {
|
||||
@@ -9,7 +9,7 @@ export const NewProfileButton = () => {
|
||||
return (
|
||||
<>
|
||||
<FloatingButton onClick={() => setOpen(true)}>
|
||||
<Add className="!size-8 absolute" />
|
||||
<Add className="absolute !size-8" />
|
||||
</FloatingButton>
|
||||
|
||||
<ProfileDialog open={open} onClose={() => setOpen(false)} />
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Profile, useClash } from "@nyanpasu/interface";
|
||||
import { BaseDialog } from "@nyanpasu/ui";
|
||||
import { version } from "~/package.json";
|
||||
import { useAsyncEffect, useReactive } from "ahooks";
|
||||
import { useRef, useState } from "react";
|
||||
import {
|
||||
Controller,
|
||||
@@ -8,13 +8,13 @@ import {
|
||||
useForm,
|
||||
} from "react-hook-form-mui";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { version } from "~/package.json";
|
||||
import { LabelSwitch } from "../setting/modules/clash-field";
|
||||
import { ReadProfile } from "./read-profile";
|
||||
import { Divider, InputAdornment } from "@mui/material";
|
||||
import { ProfileMonacoView, ProfileMonacoViewRef } from "./profile-monaco-view";
|
||||
import { useAsyncEffect, useReactive } from "ahooks";
|
||||
import { classNames } from "@/utils";
|
||||
import { Divider, InputAdornment } from "@mui/material";
|
||||
import { Profile, useClash } from "@nyanpasu/interface";
|
||||
import { BaseDialog } from "@nyanpasu/ui";
|
||||
import { LabelSwitch } from "../setting/modules/clash-field";
|
||||
import { ProfileMonacoView, ProfileMonacoViewRef } from "./profile-monaco-view";
|
||||
import { ReadProfile } from "./read-profile";
|
||||
|
||||
export interface ProfileDialogProps {
|
||||
profile?: Profile.Item;
|
||||
@@ -126,7 +126,7 @@ export const ProfileDialog = ({
|
||||
};
|
||||
|
||||
const MetaInfo = ({ className }: { className?: string }) => (
|
||||
<div className={classNames("flex flex-col gap-4 pt-2 pb-2", className)}>
|
||||
<div className={classNames("flex flex-col gap-4 pb-2 pt-2", className)}>
|
||||
{!isEdit && (
|
||||
<SelectElement
|
||||
label={t("Type")}
|
||||
@@ -231,7 +231,7 @@ export const ProfileDialog = ({
|
||||
<ReadProfile onSelected={handleProfileSelected} />
|
||||
|
||||
{localProfileMessage && (
|
||||
<div className="text-red-500 ml-2">{localProfileMessage}</div>
|
||||
<div className="ml-2 text-red-500">{localProfileMessage}</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
@@ -260,7 +260,7 @@ export const ProfileDialog = ({
|
||||
>
|
||||
{isEdit ? (
|
||||
<div className="flex h-full">
|
||||
<div className="pt-4 pb-4 overflow-auto w-96">
|
||||
<div className="w-96 overflow-auto pb-4 pt-4">
|
||||
<MetaInfo className="pl-4 pr-4" />
|
||||
</div>
|
||||
|
||||
|
@@ -1,30 +1,30 @@
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
import {
|
||||
Update,
|
||||
FilterDrama,
|
||||
InsertDriveFile,
|
||||
FiberManualRecord,
|
||||
Terminal,
|
||||
} from "@mui/icons-material";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import {
|
||||
Paper,
|
||||
LinearProgress,
|
||||
Chip,
|
||||
Tooltip,
|
||||
Menu,
|
||||
MenuItem,
|
||||
useTheme,
|
||||
Button,
|
||||
alpha,
|
||||
} from "@mui/material";
|
||||
import { Profile, useClash } from "@nyanpasu/interface";
|
||||
import { useLockFn, useSetState } from "ahooks";
|
||||
import dayjs from "dayjs";
|
||||
import { memo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ProfileDialog } from "./profile-dialog";
|
||||
import { useMessage } from "@/hooks/use-notification";
|
||||
import { useLockFn, useSetState } from "ahooks";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
import {
|
||||
FiberManualRecord,
|
||||
FilterDrama,
|
||||
InsertDriveFile,
|
||||
Terminal,
|
||||
Update,
|
||||
} from "@mui/icons-material";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import {
|
||||
alpha,
|
||||
Button,
|
||||
Chip,
|
||||
LinearProgress,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Tooltip,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { Profile, useClash } from "@nyanpasu/interface";
|
||||
import { ProfileDialog } from "./profile-dialog";
|
||||
|
||||
export interface ProfileItemProps {
|
||||
item: Profile.Item;
|
||||
@@ -159,7 +159,7 @@ export const ProfileItem = memo(function ProfileItem({
|
||||
return (
|
||||
<>
|
||||
<Paper
|
||||
className="p-5 flex flex-col gap-4"
|
||||
className="flex flex-col gap-4 p-5"
|
||||
sx={{
|
||||
borderRadius: 6,
|
||||
backgroundColor: selected
|
||||
@@ -178,7 +178,7 @@ export const ProfileItem = memo(function ProfileItem({
|
||||
|
||||
{selected && (
|
||||
<FiberManualRecord
|
||||
className="!size-3 mr-auto animate-bounce top-0"
|
||||
className="top-0 mr-auto !size-3 animate-bounce"
|
||||
sx={{ fill: palette.success.main }}
|
||||
/>
|
||||
)}
|
||||
@@ -189,7 +189,7 @@ export const ProfileItem = memo(function ProfileItem({
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-lg font-bold truncate">{item.name}</p>
|
||||
<p className="truncate text-lg font-bold">{item.name}</p>
|
||||
<p className="truncate">{item.desc}</p>
|
||||
</div>
|
||||
|
||||
@@ -207,7 +207,7 @@ export const ProfileItem = memo(function ProfileItem({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 justify-end">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
className="!mr-auto"
|
||||
size="small"
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { useDebounceEffect, useUpdateEffect } from "ahooks";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
|
||||
import { monaco } from "@/services/monaco";
|
||||
import { useDebounceEffect, useUpdateEffect } from "ahooks";
|
||||
import { themeMode } from "@/store";
|
||||
import { useAtomValue } from "jotai";
|
||||
|
||||
export interface ProfileMonacoViewProps {
|
||||
open: boolean;
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import { Allotment } from "allotment";
|
||||
import "allotment/dist/style.css";
|
||||
import { useState } from "react";
|
||||
import { Close } from "@mui/icons-material";
|
||||
import { IconButton } from "@mui/material";
|
||||
import { Profile } from "@nyanpasu/interface";
|
||||
import { useState } from "react";
|
||||
import { ScriptDialog } from "./script-dialog";
|
||||
import { SideLog } from "./modules/side-log";
|
||||
import { Allotment } from "allotment";
|
||||
import "allotment/dist/style.css";
|
||||
import { SideChain } from "./modules/side-chain";
|
||||
import { SideLog } from "./modules/side-log";
|
||||
import { ScriptDialog } from "./script-dialog";
|
||||
|
||||
export interface ProfileSideProps {
|
||||
profile?: Profile.Item;
|
||||
@@ -26,7 +26,7 @@ export const ProfileSide = ({ profile, global, onClose }: ProfileSideProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="p-4 pr-2 flex justify-between items-start">
|
||||
<div className="flex items-start justify-between p-4 pr-2">
|
||||
<div>
|
||||
<div className="text-xl font-bold">Proxy Chains</div>
|
||||
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
ClearRounded,
|
||||
ContentCopyRounded,
|
||||
@@ -12,10 +14,8 @@ import {
|
||||
Tooltip,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { readText } from "@tauri-apps/api/clipboard";
|
||||
import { useClash } from "@nyanpasu/interface";
|
||||
import { readText } from "@tauri-apps/api/clipboard";
|
||||
|
||||
export const QuickImport = () => {
|
||||
const { t } = useTranslation();
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { useState } from "react";
|
||||
import getSystem from "@/utils/get-system";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import { open } from "@tauri-apps/api/dialog";
|
||||
import { readTextFile } from "@tauri-apps/api/fs";
|
||||
import { useState } from "react";
|
||||
|
||||
const isWin = getSystem() === "windows";
|
||||
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import { Divider } from "@mui/material";
|
||||
import { BaseDialog, BaseDialogProps } from "@nyanpasu/ui";
|
||||
import { useRef } from "react";
|
||||
import { useAsyncEffect, useReactive } from "ahooks";
|
||||
import { Profile, useClash } from "@nyanpasu/interface";
|
||||
import { ProfileMonacoView, ProfileMonacoViewRef } from "./profile-monaco-view";
|
||||
import { isEqual } from "lodash-es";
|
||||
import { useRef } from "react";
|
||||
import { SelectElement, TextFieldElement, useForm } from "react-hook-form-mui";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { isEqual } from "lodash-es";
|
||||
import { Divider } from "@mui/material";
|
||||
import { Profile, useClash } from "@nyanpasu/interface";
|
||||
import { BaseDialog, BaseDialogProps } from "@nyanpasu/ui";
|
||||
import { ProfileMonacoView, ProfileMonacoViewRef } from "./profile-monaco-view";
|
||||
|
||||
export interface ScriptDialogProps extends Omit<BaseDialogProps, "title"> {
|
||||
open: boolean;
|
||||
@@ -143,8 +143,8 @@ export const ScriptDialog = ({
|
||||
{...props}
|
||||
>
|
||||
<div className="flex h-full">
|
||||
<div className="pt-4 pb-4 overflow-auto">
|
||||
<div className="flex flex-col gap-4 pl-4 pr-4 pb-4">
|
||||
<div className="overflow-auto pb-4 pt-4">
|
||||
<div className="flex flex-col gap-4 pb-4 pl-4 pr-4">
|
||||
{!isEdit && (
|
||||
<SelectElement
|
||||
label={t("Type")}
|
||||
|
@@ -1,13 +1,13 @@
|
||||
import { useLockFn } from "ahooks";
|
||||
import dayjs from "dayjs";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMessage } from "@/hooks/use-notification";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
import { Refresh } from "@mui/icons-material";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import { Chip, LinearProgress, Paper, Tooltip } from "@mui/material";
|
||||
import { ProviderItem, useClashCore } from "@nyanpasu/interface";
|
||||
import { useLockFn } from "ahooks";
|
||||
import dayjs from "dayjs";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ProxiesProviderTraffic from "./proxies-provider-traffic";
|
||||
|
||||
export interface ProxiesProviderProps {
|
||||
@@ -38,21 +38,21 @@ export const ProxiesProvider = ({ provider }: ProxiesProviderProps) => {
|
||||
|
||||
return (
|
||||
<Paper
|
||||
className="p-5 flex flex-col gap-2 justify-between h-full"
|
||||
className="flex h-full flex-col justify-between gap-2 p-5"
|
||||
sx={{
|
||||
borderRadius: 6,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="ml-1">
|
||||
<p className="text-lg font-bold truncate">{provider.name}</p>
|
||||
<p className="truncate text-lg font-bold">{provider.name}</p>
|
||||
|
||||
<p className="truncate text-sm">
|
||||
{provider.vehicleType}/{provider.type}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-right">
|
||||
<div className="text-right text-sm">
|
||||
{t("Last Update", {
|
||||
fromNow: dayjs(provider.updatedAt).fromNow(),
|
||||
})}
|
||||
@@ -65,7 +65,7 @@ export const ProxiesProvider = ({ provider }: ProxiesProviderProps) => {
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Chip
|
||||
className="font-bold truncate"
|
||||
className="truncate font-bold"
|
||||
label={t("Proxy Set proxies", {
|
||||
rule: provider.proxies.length,
|
||||
})}
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import { useLockFn } from "ahooks";
|
||||
import dayjs from "dayjs";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMessage } from "@/hooks/use-notification";
|
||||
import { Refresh } from "@mui/icons-material";
|
||||
import LoadingButton from "@mui/lab/LoadingButton/LoadingButton";
|
||||
import { Chip, Paper } from "@mui/material";
|
||||
import { ProviderRules, useClashCore } from "@nyanpasu/interface";
|
||||
import { useLockFn } from "ahooks";
|
||||
import dayjs from "dayjs";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface RulesProviderProps {
|
||||
provider: ProviderRules;
|
||||
@@ -36,21 +36,21 @@ export default function RulesProvider({ provider }: RulesProviderProps) {
|
||||
|
||||
return (
|
||||
<Paper
|
||||
className="p-5 flex flex-col gap-2"
|
||||
className="flex flex-col gap-2 p-5"
|
||||
sx={{
|
||||
borderRadius: 6,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="ml-1">
|
||||
<p className="text-lg font-bold truncate">{provider.name}</p>
|
||||
<p className="truncate text-lg font-bold">{provider.name}</p>
|
||||
|
||||
<p className="truncate text-sm">
|
||||
{provider.vehicleType}/{provider.behavior}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-right">
|
||||
<div className="text-right text-sm">
|
||||
{t("Last Update", {
|
||||
fromNow: dayjs(provider.updatedAt).fromNow(),
|
||||
})}
|
||||
@@ -59,7 +59,7 @@ export default function RulesProvider({ provider }: RulesProviderProps) {
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Chip
|
||||
className="font-bold truncate"
|
||||
className="truncate font-bold"
|
||||
label={t("Rule Set rules", {
|
||||
rule: provider.ruleCount,
|
||||
})}
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { useMessage } from "@/hooks/use-notification";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import { useClashCore } from "@nyanpasu/interface";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useState } from "react";
|
||||
import { Refresh } from "@mui/icons-material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMessage } from "@/hooks/use-notification";
|
||||
import { Refresh } from "@mui/icons-material";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import { useClashCore } from "@nyanpasu/interface";
|
||||
|
||||
export const UpdateProviders = () => {
|
||||
const { t } = useTranslation();
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { useMessage } from "@/hooks/use-notification";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import { useClashCore } from "@nyanpasu/interface";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useState } from "react";
|
||||
import { Refresh } from "@mui/icons-material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMessage } from "@/hooks/use-notification";
|
||||
import { Refresh } from "@mui/icons-material";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import { useClashCore } from "@nyanpasu/interface";
|
||||
|
||||
export const UpdateProxiesProviders = () => {
|
||||
const { t } = useTranslation();
|
||||
|
@@ -1,3 +1,6 @@
|
||||
import { useDebounceFn, useLockFn } from "ahooks";
|
||||
import { memo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { classNames } from "@/utils";
|
||||
import { Bolt, Done } from "@mui/icons-material";
|
||||
import {
|
||||
@@ -7,9 +10,6 @@ import {
|
||||
Tooltip,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { useDebounceFn, useLockFn } from "ahooks";
|
||||
import { memo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const DelayButton = memo(function DelayButton({
|
||||
onClick,
|
||||
@@ -47,7 +47,7 @@ export const DelayButton = memo(function DelayButton({
|
||||
return (
|
||||
<Tooltip title={t("Delay check")}>
|
||||
<Button
|
||||
className="size-16 backdrop-blur !rounded-2xl !fixed z-10 bottom-8 right-8"
|
||||
className="!fixed bottom-8 right-8 z-10 size-16 !rounded-2xl backdrop-blur"
|
||||
sx={{
|
||||
boxShadow: 8,
|
||||
backgroundColor: alpha(
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import clsx from "clsx";
|
||||
import { memo, useState } from "react";
|
||||
import { classNames } from "@/utils";
|
||||
import { Bolt } from "@mui/icons-material";
|
||||
import { CircularProgress } from "@mui/material";
|
||||
import clsx from "clsx";
|
||||
import { memo, useState } from "react";
|
||||
import FeatureChip from "./feature-chip";
|
||||
import { getColorForDelay } from "./utils";
|
||||
|
||||
@@ -38,7 +38,7 @@ export const DelayChip = memo(function DelayChip({
|
||||
<>
|
||||
<span
|
||||
className={classNames(
|
||||
"transition-opacity flex items-center px-[1px]",
|
||||
"flex items-center px-[1px] transition-opacity",
|
||||
loading ? "opacity-0" : "opacity-1",
|
||||
)}
|
||||
>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Chip, ChipProps } from "@mui/material";
|
||||
import { memo } from "react";
|
||||
import { Chip, ChipProps } from "@mui/material";
|
||||
|
||||
export const FeatureChip = memo(function FeatureChip(props: ChipProps) {
|
||||
return (
|
||||
|
@@ -1,3 +1,6 @@
|
||||
import { useAtom } from "jotai";
|
||||
import { memo } from "react";
|
||||
import { Virtualizer } from "virtua";
|
||||
import { proxyGroupAtom } from "@/store";
|
||||
import {
|
||||
ListItem,
|
||||
@@ -7,9 +10,6 @@ import {
|
||||
ListItemText,
|
||||
} from "@mui/material";
|
||||
import { useClashCore } from "@nyanpasu/interface";
|
||||
import { useAtom } from "jotai";
|
||||
import { memo } from "react";
|
||||
import { Virtualizer } from "virtua";
|
||||
|
||||
const IconRender = memo(function IconRender({ icon }: { icon: string }) {
|
||||
const src = icon.trim().startsWith("<svg")
|
||||
@@ -18,7 +18,7 @@ const IconRender = memo(function IconRender({ icon }: { icon: string }) {
|
||||
|
||||
return (
|
||||
<ListItemIcon>
|
||||
<img className="w-11 h-11" src={src} />
|
||||
<img className="h-11 w-11" src={src} />
|
||||
</ListItemIcon>
|
||||
);
|
||||
});
|
||||
|
@@ -1,13 +1,12 @@
|
||||
import clsx from "clsx";
|
||||
import { CSSProperties, memo, useMemo } from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import { Clash } from "@nyanpasu/interface";
|
||||
import { CSSProperties, memo, useMemo } from "react";
|
||||
import { PaperSwitchButton } from "../setting/modules/system-proxy";
|
||||
import DelayChip from "./delay-chip";
|
||||
import FeatureChip from "./feature-chip";
|
||||
import { filterDelay } from "./utils";
|
||||
|
||||
import clsx from "clsx";
|
||||
import styles from "./node-card.module.scss";
|
||||
import { filterDelay } from "./utils";
|
||||
|
||||
export const NodeCard = memo(function NodeCard({
|
||||
node,
|
||||
|
@@ -1,7 +1,5 @@
|
||||
import { Clash, useClashCore, useNyanpasu } from "@nyanpasu/interface";
|
||||
import { useBreakpoint } from "@nyanpasu/ui";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useAtom, useAtomValue } from "jotai";
|
||||
import { proxyGroupAtom, proxyGroupSortAtom } from "@/store";
|
||||
import {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
@@ -11,11 +9,13 @@ import {
|
||||
useState,
|
||||
useTransition,
|
||||
} from "react";
|
||||
import { classNames } from "@/utils";
|
||||
import { VList, VListHandle } from "virtua";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { filterDelay } from "./utils";
|
||||
import { proxyGroupAtom, proxyGroupSortAtom } from "@/store";
|
||||
import { classNames } from "@/utils";
|
||||
import { Clash, useClashCore, useNyanpasu } from "@nyanpasu/interface";
|
||||
import { useBreakpoint } from "@nyanpasu/ui";
|
||||
import NodeCard from "./node-card";
|
||||
import { filterDelay } from "./utils";
|
||||
|
||||
type RenderClashProxy = Clash.Proxy<string> & { renderLayoutKey: string };
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { useNyanpasu } from "@nyanpasu/interface";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { memo } from "react";
|
||||
import { useNyanpasu } from "@nyanpasu/interface";
|
||||
|
||||
export const ProxyGroupName = memo(function ProxyGroupName({
|
||||
name,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Radar } from "@mui/icons-material";
|
||||
import { Button, Tooltip, alpha, useTheme } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Radar } from "@mui/icons-material";
|
||||
import { alpha, Button, Tooltip, useTheme } from "@mui/material";
|
||||
|
||||
export const ScrollCurrentNode = ({ onClick }: { onClick?: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -11,7 +11,7 @@ export const ScrollCurrentNode = ({ onClick }: { onClick?: () => void }) => {
|
||||
<Tooltip title={t("Location")}>
|
||||
<Button
|
||||
size="small"
|
||||
className="!min-w-0 !size-8"
|
||||
className="!size-8 !min-w-0"
|
||||
sx={{
|
||||
backgroundColor: alpha(palette.primary.main, 0.1),
|
||||
}}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { proxyGroupSortAtom } from "@/store";
|
||||
import { Button, Menu, MenuItem, alpha, useTheme } from "@mui/material";
|
||||
import { useAtom } from "jotai";
|
||||
import { memo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { proxyGroupSortAtom } from "@/store";
|
||||
import { alpha, Button, Menu, MenuItem, useTheme } from "@mui/material";
|
||||
|
||||
export const SortSelector = memo(function SortSelector() {
|
||||
const { t } = useTranslation();
|
||||
|
@@ -37,7 +37,7 @@ const RuleItem = ({ index, value }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-2 pl-7 pr-7 flex">
|
||||
<div className="flex p-2 pl-7 pr-7">
|
||||
<div style={{ color: palette.text.secondary }} className="min-w-14">
|
||||
{index + 1}
|
||||
</div>
|
||||
@@ -48,10 +48,10 @@ const RuleItem = ({ index, value }: Props) => {
|
||||
</div>
|
||||
|
||||
<div className="flex gap-8">
|
||||
<div className="text-sm min-w-40">{value.type}</div>
|
||||
<div className="min-w-40 text-sm">{value.type}</div>
|
||||
|
||||
<div
|
||||
className="text-sm text-s"
|
||||
className="text-s text-sm"
|
||||
style={{ color: parseColor(value.proxy) }}
|
||||
>
|
||||
{value.proxy}
|
||||
|
@@ -1,17 +1,17 @@
|
||||
import ListItem from "@mui/material/ListItem";
|
||||
import ListItemButton from "@mui/material/ListItemButton";
|
||||
import { Item } from "./clash-web";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { alpha, useTheme } from "@mui/material/styles";
|
||||
import { ClashCore, Core } from "@nyanpasu/interface";
|
||||
import Clash from "@/assets/image/core/clash.png";
|
||||
import ClashMeta from "@/assets/image/core/clash.meta.png";
|
||||
import ClashRs from "@/assets/image/core/clash-rs.png";
|
||||
import ClashMeta from "@/assets/image/core/clash.meta.png";
|
||||
import Clash from "@/assets/image/core/clash.png";
|
||||
import FiberManualRecord from "@mui/icons-material/FiberManualRecord";
|
||||
import Update from "@mui/icons-material/Update";
|
||||
import Box from "@mui/material/Box";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import ListItem from "@mui/material/ListItem";
|
||||
import ListItemButton from "@mui/material/ListItemButton";
|
||||
import { alpha, useTheme } from "@mui/material/styles";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { ClashCore, Core } from "@nyanpasu/interface";
|
||||
import { Item } from "./clash-web";
|
||||
|
||||
export const getImage = (core: ClashCore) => {
|
||||
switch (core) {
|
||||
|
@@ -1,18 +1,18 @@
|
||||
import { ChangeEvent, useState } from "react";
|
||||
import Marquee from "react-fast-marquee";
|
||||
import ArrowForwardIos from "@mui/icons-material/ArrowForwardIos";
|
||||
import OpenInNewRounded from "@mui/icons-material/OpenInNewRounded";
|
||||
import { alpha, useTheme } from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import ButtonBase, { ButtonBaseProps } from "@mui/material/ButtonBase";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import { SwitchProps } from "@mui/material/Switch";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { open } from "@tauri-apps/api/shell";
|
||||
import Grid from "@mui/material/Unstable_Grid2";
|
||||
import ButtonBase, { ButtonBaseProps } from "@mui/material/ButtonBase";
|
||||
import Marquee from "react-fast-marquee";
|
||||
import ArrowForwardIos from "@mui/icons-material/ArrowForwardIos";
|
||||
import { alpha, useTheme } from "@mui/material";
|
||||
import { LoadingSwitch } from "@nyanpasu/ui";
|
||||
import { ChangeEvent, useState } from "react";
|
||||
import { open } from "@tauri-apps/api/shell";
|
||||
|
||||
export interface LabelSwitchProps extends SwitchProps {
|
||||
label: string;
|
||||
|
@@ -1,3 +1,8 @@
|
||||
import { ReactNode } from "react";
|
||||
import Marquee from "react-fast-marquee";
|
||||
import DeleteRounded from "@mui/icons-material/DeleteRounded";
|
||||
import EditRounded from "@mui/icons-material/EditRounded";
|
||||
import OpenInNewRounded from "@mui/icons-material/OpenInNewRounded";
|
||||
import Box from "@mui/material/Box";
|
||||
import Chip from "@mui/material/Chip";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
@@ -5,11 +10,6 @@ import Paper from "@mui/material/Paper";
|
||||
import { alpha, styled } from "@mui/material/styles";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { open } from "@tauri-apps/api/shell";
|
||||
import OpenInNewRounded from "@mui/icons-material/OpenInNewRounded";
|
||||
import DeleteRounded from "@mui/icons-material/DeleteRounded";
|
||||
import EditRounded from "@mui/icons-material/EditRounded";
|
||||
import Marquee from "react-fast-marquee";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
/**
|
||||
* @example
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { useLockFn, useMemoizedFn } from "ahooks";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { NotificationType, useNotification } from "@/hooks/use-notification";
|
||||
import { Typography } from "@mui/material";
|
||||
import { useNyanpasu } from "@nyanpasu/interface";
|
||||
import { BaseDialog, BaseDialogProps } from "@nyanpasu/ui";
|
||||
import { useLockFn, useMemoizedFn } from "ahooks";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import HotkeyInput from "./hotkey-input";
|
||||
|
||||
export interface HotkeyDialogProps extends Omit<BaseDialogProps, "title"> {}
|
||||
@@ -115,9 +115,9 @@ export default function HotkeyDialog({
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
<div className="grid grid-1 gap-3">
|
||||
<div className="grid-1 grid gap-3">
|
||||
{HOTKEY_FUNC.map((func) => (
|
||||
<div className="flex px-2 items-center justify-between" key={func}>
|
||||
<div className="flex items-center justify-between px-2" key={func}>
|
||||
<Typography>{t(func)}</Typography>
|
||||
<HotkeyInput
|
||||
func={func}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import clsx from "clsx";
|
||||
import { CSSProperties, useRef, useState } from "react";
|
||||
import { parseHotkey } from "@/utils/parse-hotkey";
|
||||
import { DeleteRounded } from "@mui/icons-material";
|
||||
import { alpha, IconButton, useTheme } from "@mui/material";
|
||||
import Kbd from "@nyanpasu/ui/materialYou/components/kbd";
|
||||
import clsx from "clsx";
|
||||
import { CSSProperties, useRef, useState } from "react";
|
||||
import styles from "./hotkey-input.module.scss";
|
||||
|
||||
export interface Props extends React.HTMLAttributes<HTMLInputElement> {
|
||||
@@ -31,10 +31,10 @@ export default function HotkeyInput({
|
||||
const [keys, setKeys] = useState(value || []);
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={clsx("relative w-[165px] min-h-[36px]", styles.wrapper)}>
|
||||
<div className={clsx("relative min-h-[36px] w-[165px]", styles.wrapper)}>
|
||||
<input
|
||||
className={clsx(
|
||||
"absolute top-0 left-0 w-full h-full z-[1] opacity-0",
|
||||
"absolute left-0 top-0 z-[1] h-full w-full opacity-0",
|
||||
styles.input,
|
||||
className,
|
||||
)}
|
||||
@@ -62,7 +62,7 @@ export default function HotkeyInput({
|
||||
/>
|
||||
<div
|
||||
className={clsx(
|
||||
"flex items-center flex-wrap w-full h-full min-h-[36px] box-border py-1 px-1 border border-solid rounded last:mr-0",
|
||||
"box-border flex h-full min-h-[36px] w-full flex-wrap items-center rounded border border-solid px-1 py-1 last:mr-0",
|
||||
styles.items,
|
||||
)}
|
||||
style={
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { memo, ReactNode } from "react";
|
||||
import {
|
||||
alpha,
|
||||
ButtonBase,
|
||||
@@ -7,7 +8,6 @@ import {
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { memo, ReactNode } from "react";
|
||||
|
||||
export interface PaperButtonProps extends ButtonBaseProps {
|
||||
label: string;
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { alpha, CircularProgress, useTheme } from "@mui/material";
|
||||
import { memo, ReactNode } from "react";
|
||||
import { alpha, CircularProgress, useTheme } from "@mui/material";
|
||||
import { PaperButton, PaperButtonProps } from "./nyanpasu-path";
|
||||
|
||||
export interface PaperSwitchButtonProps extends PaperButtonProps {
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { Button, List, ListItem, ListItemText } from "@mui/material";
|
||||
import { BaseCard, MenuItem, SwitchItem } from "@nyanpasu/ui";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { clash } from "./modules";
|
||||
import getSystem from "@/utils/get-system";
|
||||
import { pullupUWPTool } from "@nyanpasu/interface";
|
||||
import { useMessage } from "@/hooks/use-notification";
|
||||
import getSystem from "@/utils/get-system";
|
||||
import { Button, List, ListItem, ListItemText } from "@mui/material";
|
||||
import { pullupUWPTool } from "@nyanpasu/interface";
|
||||
import { BaseCard, MenuItem, SwitchItem } from "@nyanpasu/ui";
|
||||
import { clash } from "./modules";
|
||||
|
||||
const { createBooleanProps, createMenuProps } = clash;
|
||||
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import { BaseCard, ExpandMore } from "@nyanpasu/ui";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Box, List, ListItem, Tooltip } from "@mui/material";
|
||||
import { ClashCore, useClash, useNyanpasu } from "@nyanpasu/interface";
|
||||
import { useLockFn, useReactive } from "ahooks";
|
||||
import { motion } from "framer-motion";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMessage } from "@/hooks/use-notification";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import { motion } from "framer-motion";
|
||||
import { Box, List, ListItem } from "@mui/material";
|
||||
import { ClashCore, useClash, useNyanpasu } from "@nyanpasu/interface";
|
||||
import { BaseCard, ExpandMore } from "@nyanpasu/ui";
|
||||
import { ClashCoreItem } from "./modules/clash-core";
|
||||
|
||||
export const SettingClashCore = () => {
|
||||
@@ -55,7 +55,9 @@ export const SettingClashCore = () => {
|
||||
});
|
||||
} catch (e) {
|
||||
useMessage(
|
||||
"Switching failed, please check log and modify your profile file.",
|
||||
`Switching failed, you could see the details in the log. \nError: ${
|
||||
e instanceof Error ? e.message : String(e)
|
||||
}`,
|
||||
{
|
||||
type: "error",
|
||||
title: t("Error"),
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import { ChangeEvent, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMessage } from "@/hooks/use-notification";
|
||||
import Done from "@mui/icons-material/Done";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
@@ -11,8 +13,6 @@ import {
|
||||
} from "@mui/material";
|
||||
import { useClash, useNyanpasu } from "@nyanpasu/interface";
|
||||
import { BaseCard, Expand, MenuItem } from "@nyanpasu/ui";
|
||||
import { ChangeEvent, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type PortStrategy = "fixed" | "random" | "allow_fallback";
|
||||
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import { useClash, useNyanpasu } from "@nyanpasu/interface";
|
||||
import { BaseCard, BaseDialog } from "@nyanpasu/ui";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CLASH_FIELD from "@/assets/json/clash-field.json";
|
||||
import { ClashFieldItem, LabelSwitch } from "./modules/clash-field";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import Grid from "@mui/material/Unstable_Grid2";
|
||||
import { useClash, useNyanpasu } from "@nyanpasu/interface";
|
||||
import { BaseCard, BaseDialog } from "@nyanpasu/ui";
|
||||
import { ClashFieldItem, LabelSwitch } from "./modules/clash-field";
|
||||
|
||||
const FieldsControl = ({
|
||||
label,
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { List } from "@mui/material";
|
||||
import { BaseCard, NumberItem, SwitchItem } from "@nyanpasu/ui";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useClash, useNyanpasu } from "@nyanpasu/interface";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMessage } from "@/hooks/use-notification";
|
||||
import { List } from "@mui/material";
|
||||
import { useClash, useNyanpasu } from "@nyanpasu/interface";
|
||||
import { BaseCard, NumberItem, SwitchItem } from "@nyanpasu/ui";
|
||||
|
||||
export const SettingClashPort = () => {
|
||||
const { t } = useTranslation();
|
||||
|
@@ -1,3 +1,6 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import {
|
||||
Box,
|
||||
Chip,
|
||||
@@ -7,13 +10,10 @@ import {
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import Grid from "@mui/material/Unstable_Grid2";
|
||||
import { useClash, useNyanpasu } from "@nyanpasu/interface";
|
||||
import { BaseCard, BaseDialog, Expand } from "@nyanpasu/ui";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import { useMemo, useState } from "react";
|
||||
import { ClashWebItem, extractServer, openWebUrl, renderChip } from "./modules";
|
||||
import Grid from "@mui/material/Unstable_Grid2";
|
||||
|
||||
export const SettingClashWeb = () => {
|
||||
const { t } = useTranslation();
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user