diff options
| author | Larvan2 <[email protected]> | 2026-07-05 17:36:35 +0800 |
|---|---|---|
| committer | Larvan2 <[email protected]> | 2026-07-05 17:36:35 +0800 |
| commit | 30dfb51a61a6d6083640d411da8c63cb076318ab (patch) | |
| tree | 457f95f9dafc6cf1196d35fdec5eb0d677667b23 /src/components | |
| parent | a7a842ee25a5b7fc5aa0cd20e9a3086d747332bc (diff) | |
Wire up the mihomo rules API's extra fields (disabled, hitCount, hitAt)
and the /rules/disable endpoint so each rule can be toggled on/off and
shows its hit count. Also surface the rule provider's ruleCount for
RuleSet-type rules, since their own size field is always -1.
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/rules/Rule.module.scss | 28 | ||||
| -rw-r--r-- | src/components/rules/Rule.tsx | 77 | ||||
| -rw-r--r-- | src/components/rules/Rules.tsx | 2 |
3 files changed, 102 insertions, 5 deletions
diff --git a/src/components/rules/Rule.module.scss b/src/components/rules/Rule.module.scss index 48a22e7..f13cab1 100644 --- a/src/components/rules/Rule.module.scss +++ b/src/components/rules/Rule.module.scss @@ -10,6 +10,10 @@ &:hover { background-color: var(--bg-near-transparent); } + + &.disabled { + opacity: 0.45; + } } .left { @@ -79,3 +83,27 @@ font-weight: 600; letter-spacing: 0.02em; } + +.hitInfo { + display: flex; + align-items: center; + gap: 4px; + color: var(--color-text-secondary); + cursor: default; +} + +.spacer { + flex: 1; +} + +.wrapSwitch { + display: flex; + align-items: center; + transform: scale(0.7); + transform-origin: right center; + + &.pending { + opacity: 0.6; + pointer-events: none; + } +} diff --git a/src/components/rules/Rule.tsx b/src/components/rules/Rule.tsx index e1fd047..e53ff23 100644 --- a/src/components/rules/Rule.tsx +++ b/src/components/rules/Rule.tsx @@ -1,6 +1,13 @@ +import cx from 'clsx'; +import { formatDistanceToNow } from 'date-fns'; import React from 'react'; +import { useTranslation } from 'react-i18next'; -import { FileText, Globe, Hash, Link, Shield, Zap } from '~/components/shared/FeatherIcons'; +import { RuleExtra } from '~/api/rules'; +import { Activity, FileText, Globe, Hash, Link, Shield, Zap } from '~/components/shared/FeatherIcons'; +import SwitchThemed from '~/components/SwitchThemed'; +import { useToggleRuleDisabled } from '~/modules/rules/hooks'; +import { ClashAPIConfig } from '~/types'; import s0 from './Rule.module.scss'; @@ -39,23 +46,66 @@ function getIconFor(type: string) { } } +type RuleProviderLookup = { + byName: Record<string, { ruleCount?: number }>; +}; + type Props = { id?: number; type?: string; payload?: string; proxy?: string; size?: number; + extra?: RuleExtra; + apiConfig?: ClashAPIConfig; + provider?: RuleProviderLookup; }; -function Rule({ type, payload, proxy, id, size }: Props) { +function getEntryCount({ + type, + payload, + size, + provider, +}: { + type: string; + payload: string; + size: number; + provider?: RuleProviderLookup; +}): number | undefined { + if ((type === 'GeoSite' || type === 'GeoIP') && size >= 0) { + return size; + } + if (type === 'RuleSet') { + return provider?.byName?.[payload]?.ruleCount; + } + return undefined; +} + +function Rule({ type, payload, proxy, id, size, extra, apiConfig, provider }: Props) { + const { t } = useTranslation(); const styleProxy = getStyleFor({ proxy }); + const { toggleRule, isPending } = useToggleRuleDisabled(apiConfig); + const disabled = extra?.disabled ?? false; + const entryCount = getEntryCount({ type, payload, size, provider }); + + const hitTitle = extra + ? extra.hitCount > 0 + ? t('rule_hit_tip', { + count: extra.hitCount, + time: formatDistanceToNow(new Date(extra.hitAt), { addSuffix: true }), + }) + : t('rule_never_hit') + : undefined; + return ( - <div className={s0.rule}> + <div className={cx(s0.rule, { [s0.disabled]: disabled })}> <div className={s0.left}>{id}</div> <div className={s0.right}> <div className={s0.payloadRow}> <div className={s0.payload}>{payload}</div> - {(type === 'GeoSite' || type === 'GeoIP') && <div className={s0.size}>size: {size}</div>} + {typeof entryCount === 'number' && ( + <div className={s0.size}>{t('rule_entry_count', { count: entryCount })}</div> + )} </div> <div className={s0.metaRow}> <div className={s0.typeTag}> @@ -65,6 +115,25 @@ function Rule({ type, payload, proxy, id, size }: Props) { <div className={s0.proxyTag} style={styleProxy}> {proxy} </div> + {extra && ( + <div className={s0.hitInfo} title={hitTitle}> + <Activity size={12} /> + <span>{extra.hitCount}</span> + </div> + )} + <div className={s0.spacer} /> + {extra && ( + <div + className={cx(s0.wrapSwitch, { [s0.pending]: isPending })} + title={disabled ? t('rule_enable') : t('rule_disable')} + > + <SwitchThemed + name={`rule-${id}`} + checked={!disabled} + onChange={(checked: boolean) => toggleRule(id, !checked)} + /> + </div> + )} </div> </div> </div> diff --git a/src/components/rules/Rules.tsx b/src/components/rules/Rules.tsx index c0b02ae..45c55fc 100644 --- a/src/components/rules/Rules.tsx +++ b/src/components/rules/Rules.tsx @@ -36,7 +36,7 @@ function Row({ index, style, data }: RowComponentProps<RulesRowProps>) { const r = rules[index]; return ( <div style={style}> - <Rule {...r} /> + <Rule {...r} apiConfig={apiConfig} provider={provider} /> </div> ); } |
