This commit is contained in:
me
2026-01-30 20:32:37 +08:00
parent 222449cb1b
commit a4e6557d1b
24 changed files with 2189 additions and 6235 deletions

View File

@@ -1,90 +1,274 @@
import { Theme, SpacingSystem, Typography } from "../theme/theme.slint";
import { Tabs, TabItem, TabContent } from "../components/tabs.slint";
import { Accordion, AccordionItemData, AccordionContent } from "../components/accordion.slint";
// Proxies Page - Proxy nodes management with tabs and groups
import { Theme, Typography, SpacingSystem } from "../theme/theme.slint";
import { Item } from "../components/item.slint";
import { Badge } from "../components/badge.slint";
import { Button } from "../components/button.slint";
import { Empty } from "../components/empty.slint";
import { Container } from "../layouts/container.slint";
import { ProxyGroup, ProxyNode } from "../types.slint";
export struct ProxyNode {
name: string,
type: string,
latency: string,
}
export component ProxiesPage inherits Rectangle {
in property <[ProxyGroup]> proxy-groups;
in-out property <string> proxy-mode: "rule"; // "rule" | "global" | "direct"
export struct ProxyGroup {
name: string,
nodes: [ProxyNode],
expanded: bool,
}
callback load-proxy-groups(string /* mode */);
callback select-proxy(string /* group-name */, string /* node-name */);
callback test-proxy-delay(string /* node-name */);
background: Theme.colors.background;
export component ProxiesPage {
in-out property <[ProxyGroup]> proxy-groups: [];
in-out property <int> current-mode: 0; // 0=Rule, 1=Global, 2=Direct
in property <string> selected-proxy: "";
callback mode-changed(int);
callback group-toggled(int, bool);
callback proxy-selected(string);
VerticalLayout {
padding: SpacingSystem.spacing.s4;
spacing: SpacingSystem.spacing.s4;
// Header with Tabs
HorizontalLayout {
spacing: SpacingSystem.spacing.s3;
alignment: space-between;
Text {
text: "Proxies";
color: Theme.colors.foreground;
font-size: Typography.sizes.xl;
font-weight: Typography.weights.bold;
vertical-alignment: center;
}
Rectangle {
horizontal-stretch: 1;
// Fixed header with tabs
Rectangle {
height: 48px;
background: Theme.colors.background;
HorizontalLayout {
padding: SpacingSystem.spacing.s4;
spacing: SpacingSystem.spacing.s4;
alignment: space-between;
Text {
text: "Proxies";
font-size: Typography.sizes.xl;
font-weight: Typography.weights.bold;
color: Theme.colors.foreground;
vertical-alignment: center;
}
// Mode tabs
HorizontalLayout {
spacing: SpacingSystem.spacing.s1;
Rectangle {
width: 80px;
height: 32px;
background: proxy-mode == "rule" ? Theme.colors.primary : Theme.colors.muted;
border-radius: SpacingSystem.radius.md;
TouchArea {
clicked => {
proxy-mode = "rule";
root.load-proxy-groups("rule");
}
}
Text {
text: "Rule";
font-size: Typography.sizes.sm;
font-weight: Typography.weights.medium;
color: proxy-mode == "rule" ? Theme.colors.primary-foreground : Theme.colors.muted-foreground;
horizontal-alignment: center;
vertical-alignment: center;
}
}
Rectangle {
width: 80px;
height: 32px;
background: proxy-mode == "global" ? Theme.colors.primary : Theme.colors.muted;
border-radius: SpacingSystem.radius.md;
TouchArea {
clicked => {
proxy-mode = "global";
root.load-proxy-groups("global");
}
}
Text {
text: "Global";
font-size: Typography.sizes.sm;
font-weight: Typography.weights.medium;
color: proxy-mode == "global" ? Theme.colors.primary-foreground : Theme.colors.muted-foreground;
horizontal-alignment: center;
vertical-alignment: center;
}
}
Rectangle {
width: 80px;
height: 32px;
background: proxy-mode == "direct" ? Theme.colors.primary : Theme.colors.muted;
border-radius: SpacingSystem.radius.md;
TouchArea {
clicked => {
proxy-mode = "direct";
root.load-proxy-groups("direct");
}
}
Text {
text: "Direct";
font-size: Typography.sizes.sm;
font-weight: Typography.weights.medium;
color: proxy-mode == "direct" ? Theme.colors.primary-foreground : Theme.colors.muted-foreground;
horizontal-alignment: center;
vertical-alignment: center;
}
}
}
}
}
// Tabs
Tabs {
tabs: [
{ title: "Rule" },
{ title: "Global" },
{ title: "Direct" },
];
current-index: root.current-mode;
tab-changed(index) => {
root.current-mode = index;
root.mode-changed(index);
}
// Tab Content
TabContent {
index: 0;
current-index: root.current-mode;
ScrollView {
// Scrollable content
Flickable {
viewport-height: content-layout.preferred-height + SpacingSystem.spacing.s4 * 2;
content-layout := VerticalLayout {
padding: SpacingSystem.spacing.s4;
spacing: SpacingSystem.spacing.s4;
// Empty state
if proxy-groups.length == 0: Empty {
height: 300px;
icon: "🌍";
title: "No Groups";
description: "Proxy groups will appear here";
}
// Proxy groups
for group in proxy-groups: Container {
VerticalLayout {
padding: SpacingSystem.spacing.s4;
spacing: SpacingSystem.spacing.s3;
Accordion {
items: root.proxy-groups.map(|g| {
return { title: g.name, expanded: g.expanded };
});
item-toggled(index, expanded) => {
root.group-toggled(index, expanded);
// Group header
HorizontalLayout {
spacing: SpacingSystem.spacing.s3;
alignment: space-between;
VerticalLayout {
spacing: SpacingSystem.spacing.s1;
Text {
text: group.name;
font-size: Typography.sizes.base;
font-weight: Typography.weights.semibold;
color: Theme.colors.foreground;
}
Text {
text: "Type: " + group.type + " | Current: " + group.now;
font-size: Typography.sizes.xs;
color: Theme.colors.muted-foreground;
}
}
for group[group-index] in root.proxy-groups: AccordionContent {
ProxyGroupContent {
nodes: group.nodes;
selected-proxy: root.selected-proxy;
node-selected(name) => {
root.proxy-selected(name);
Text {
text: group.nodes.length + " nodes";
font-size: Typography.sizes.xs;
color: Theme.colors.muted-foreground;
vertical-alignment: center;
}
}
// Divider
Rectangle {
height: 1px;
background: Theme.colors.border;
}
// Proxy nodes
VerticalLayout {
spacing: SpacingSystem.spacing.s2;
for node in group.nodes: Rectangle {
height: 60px;
background: node.name == group.now ? Theme.colors.accent : transparent;
border-radius: SpacingSystem.radius.md;
border-width: 1px;
border-color: Theme.colors.border;
HorizontalLayout {
padding: SpacingSystem.spacing.s3;
spacing: SpacingSystem.spacing.s3;
alignment: space-between;
// Node info
HorizontalLayout {
spacing: SpacingSystem.spacing.s3;
// Status indicator
Rectangle {
width: 8px;
height: 8px;
border-radius: 4px;
background: node.delay >= 0 && node.delay < 200 ? #22c55e :
node.delay >= 200 && node.delay < 500 ? #f59e0b :
node.delay >= 500 ? #ef4444 : Theme.colors.muted;
}
// Node details
VerticalLayout {
spacing: SpacingSystem.spacing.s1;
Text {
text: node.name;
font-size: Typography.sizes.sm;
font-weight: Typography.weights.medium;
color: Theme.colors.foreground;
}
HorizontalLayout {
spacing: SpacingSystem.spacing.s2;
Badge {
text: node.type;
variant: "outline";
}
if node.udp: Badge {
text: "UDP";
variant: "outline";
}
if node.delay >= 0: Text {
text: node.delay + "ms";
font-size: Typography.sizes.xs;
color: Theme.colors.muted-foreground;
}
}
}
}
// Actions
HorizontalLayout {
spacing: SpacingSystem.spacing.s2;
Button {
text: "🔍";
variant: "ghost";
size: "sm";
clicked => {
root.test-proxy-delay(node.name);
}
}
if node.name != group.now: Button {
text: "Select";
variant: "outline";
size: "sm";
clicked => {
root.select-proxy(group.name, node.name);
}
}
if node.name == group.now: Badge {
text: "Active";
variant: "default";
}
}
}
TouchArea {
clicked => {
if node.name != group.now {
root.select-proxy(group.name, node.name);
}
}
}
}
@@ -92,135 +276,6 @@ export component ProxiesPage {
}
}
}
TabContent {
index: 1;
current-index: root.current-mode;
Text {
text: "Global mode - All traffic goes through selected proxy";
color: Theme.colors.muted-foreground;
horizontal-alignment: center;
vertical-alignment: center;
}
}
TabContent {
index: 2;
current-index: root.current-mode;
Text {
text: "Direct mode - All traffic goes direct";
color: Theme.colors.muted-foreground;
horizontal-alignment: center;
vertical-alignment: center;
}
}
}
}
}
component ProxyGroupContent {
in property <[ProxyNode]> nodes: [];
in property <string> selected-proxy: "";
callback node-selected(string);
GridLayout {
spacing: SpacingSystem.spacing.s3;
for node[index] in root.nodes: ProxyNodeItem {
node-data: node;
selected: node.name == root.selected-proxy;
clicked => {
root.node-selected(node.name);
}
}
}
}
component ProxyNodeItem {
in property <ProxyNode> node-data;
in property <bool> selected: false;
callback clicked();
private property <bool> hovered: false;
min-width: 200px;
height: 60px;
states [
selected when root.selected: {
container.border-color: Theme.coloimary;
container.background: Theme.colors.primary.transparentize(0.9);
}
hovered when root.hovered && !root.selected: {
container.background: Theme.colors.muted;
}
]
container := Rectangle {
background: transparent;
border-radius: SpacingSystem.radius.md;
border-width: 1px;
border-color: Theme.colors.border;
animate background { duration: 200ms; }
animate border-color { duration: 200ms; }
HorizontalLayout {
padding: SpacingSystem.spacing.s3;
spacing: SpacingSystem.spacing.s3;
alignment: space-between;
VerticalLayout {
spacing: SpacingSystem.spacing.s1;
HorizontalLayout {
spacing: SpacingSystem.spacing.s2;
Text {
text: "✓";
color: root.selected ? Theme.colors.primary : Theme.colors.muted-foreground;
font-size: Typography.sizes.sm;
vertical-alignment: center;
}
Text {
text: root.node-data.name;
color: Theme.colors.foreground;
font-size: Typography.sizes.sm;
font-weight: Typography.weights.medium;
vertical-alignment: center;
}
}
Text {
text: root.node-data.type;
color: Theme.colors.muted-foreground;
font-size: Typography.sizes.xs;
}
}
Text {
text: root.node-data.latency;
color: Theme.colors.primary;
font-size: Typography.sizes.sm;
font-weight: Typography.weights.semibold;
vertical-alignment: center;
}
}
touch := TouchArea {
clicked => {
root.clicked();
}
moved => {
root.hovered = self.has-hover;
}
}
}
}