Update On Fri Jul 26 20:36:04 CEST 2024

This commit is contained in:
github-action[bot]
2024-07-26 20:36:04 +02:00
parent 142e9a4419
commit a937fef22b
230 changed files with 3789 additions and 2096 deletions

1
.github/update.log vendored
View File

@@ -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

View 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
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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=

View File

@@ -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

View File

@@ -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)

View File

@@ -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{}
}

View 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>")
}
}

View 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

View File

@@ -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 == "" {

View File

@@ -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);
}
})();

View File

@@ -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}`;
};

View 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",
],
};

View File

@@ -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",
]

View File

@@ -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"

View File

@@ -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 {

View File

@@ -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()),
))
}

View File

@@ -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() {

View File

@@ -12,3 +12,4 @@ pub mod win_service;
pub mod win_uwp;
pub use self::clash::core::*;
pub mod migration;
pub mod service;

View 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)
}

View File

@@ -0,0 +1,4 @@
pub enum IpcState {
Connected,
Disconnected,
}

View 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))
});

View File

@@ -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")]

View File

@@ -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();

View File

@@ -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"))

View File

@@ -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

View File

@@ -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",

View File

@@ -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.

View File

@@ -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();

View File

@@ -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();

View File

@@ -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.

View File

@@ -1,5 +1,5 @@
import useSWR from "swr";
import { ofetch } from "ofetch";
import useSWR from "swr";
interface IPSBResponse {
organization: string;

View File

@@ -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",

View File

@@ -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) }}
/>
)}

View File

@@ -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"}

View File

@@ -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

View File

@@ -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();

View File

@@ -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,
)}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,
)}
>

View File

@@ -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>
);

View File

@@ -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"

View File

@@ -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();

View File

@@ -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>
);
});

View File

@@ -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(" ")}

View File

@@ -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>

View File

@@ -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);

View File

@@ -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();

View File

@@ -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();

View File

@@ -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";

View File

@@ -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();

View File

@@ -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;

View File

@@ -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>
);

View File

@@ -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;

View File

@@ -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,

View File

@@ -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;

View File

@@ -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) => {

View File

@@ -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;

View File

@@ -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);

View File

@@ -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,

View File

@@ -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>

View File

@@ -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)} />

View File

@@ -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>

View File

@@ -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"

View File

@@ -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;

View File

@@ -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>

View File

@@ -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();

View File

@@ -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";

View File

@@ -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")}

View File

@@ -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,
})}

View File

@@ -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,
})}

View File

@@ -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();

View File

@@ -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();

View File

@@ -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(

View File

@@ -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",
)}
>

View File

@@ -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 (

View File

@@ -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>
);
});

View File

@@ -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,

View File

@@ -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 };

View File

@@ -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,

View File

@@ -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),
}}

View File

@@ -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();

View File

@@ -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}

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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

View File

@@ -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}

View File

@@ -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={

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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"),

View File

@@ -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";

View File

@@ -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,

View File

@@ -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();

View File

@@ -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