-
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user