-
This commit is contained in:
131
ui/pages/connections.slint
Normal file
131
ui/pages/connections.slint
Normal file
@@ -0,0 +1,131 @@
|
||||
import { Theme, SpacingSystem, Typography } from "../theme/theme.slint";
|
||||
|
||||
export struct Connection {
|
||||
source: string,
|
||||
destination: string,
|
||||
protocol: string,
|
||||
upload: string,
|
||||
download: string,
|
||||
duration: string,
|
||||
}
|
||||
|
||||
export component ConnectionsPage {
|
||||
in property <[Connection]> connections: [];
|
||||
|
||||
VerticalLayout {
|
||||
padding: SpacingSystem.spacing.s4;
|
||||
spacing: SpacingSystem.spacing.s4;
|
||||
|
||||
// Header
|
||||
Text {
|
||||
text: "Connections";
|
||||
color: Theme.colors.foreground;
|
||||
font-size: Typography.sizes.xl;
|
||||
font-weight: Typography.weights.bold;
|
||||
}
|
||||
|
||||
// Connections List
|
||||
ScrollView {
|
||||
VerticalLayout {
|
||||
spacing: SpacingSystem.spacing.s2;
|
||||
|
||||
for connection in root.connections: ConnectionItem {
|
||||
connection-data: connection;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component ConnectionItem {
|
||||
in property <Connection> connection-data;
|
||||
|
||||
private property <bool> hovered: false;
|
||||
|
||||
height: 60px;
|
||||
|
||||
states [
|
||||
hovered when root.hovered: {
|
||||
container.background: Theme.colors.muted.transparentize(0.5);
|
||||
}
|
||||
]
|
||||
|
||||
container := Rectangle {
|
||||
background: transparent;
|
||||
border-radius: SpacingSystem.radius.md;
|
||||
border-width: 1px;
|
||||
border-color: Theme.colors.border;
|
||||
|
||||
animate background { duration: 150ms; }
|
||||
|
||||
HorizontalLayout {
|
||||
padding: SpacingSystem.spacing.s3;
|
||||
spacing: SpacingSystem.spacing.s3;
|
||||
alignment: space-between;
|
||||
|
||||
// Icon
|
||||
Rectangle {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: SpacingSystem.radius.md;
|
||||
background: Theme.colors.primary.transparentize(0.9);
|
||||
|
||||
Text {
|
||||
text: "🔗";
|
||||
font-size: Typography.sizes.lg;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
|
||||
// Connection Info
|
||||
VerticalLayout {
|
||||
spacing: SpacingSystem.spacing.s1;
|
||||
|
||||
Text {
|
||||
text: root.connection-data.source + " → " + root.connection-data.destination;
|
||||
color: Theme.colors.foreground;
|
||||
font-size: Typography.sizes.sm;
|
||||
font-weight: Typography.weights.medium;
|
||||
overflow: elide;
|
||||
}
|
||||
|
||||
HorizontalLayout {
|
||||
spacing: SpacingSystem.spacing.s3;
|
||||
|
||||
Text {
|
||||
text: root.connection-data.protocol;
|
||||
color: Theme.colors.muted-foreground;
|
||||
font-size: Typography.sizes.xs;
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "↑ " + root.connection-data.upload;
|
||||
color: Theme.colors.primary;
|
||||
font-size: Typography.sizes.xs;
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "↓ " + root.connection-data.download;
|
||||
color: Theme.colors.secondary;
|
||||
font-size: Typography.sizes.xs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Duration
|
||||
Text {
|
||||
text: root.connection-data.duration;
|
||||
color: Theme.colmuted-foreground;
|
||||
font-size: Typography.sizes.xs;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
|
||||
touch := TouchArea {
|
||||
moved => {
|
||||
root.hovered = self.has-hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
519
ui/pages/home.slint
Normal file
519
ui/pages/home.slint
Normal file
@@ -0,0 +1,519 @@
|
||||
import { Theme, SpacingSystem, Typography } from "../theme/theme.slint";
|
||||
import { Card } from "../components/card.slint";
|
||||
import { Badge } from "../components/badge.slint";
|
||||
import { Button } from "../components/button.slint";
|
||||
import { Switch } from "../components/switch.slint";
|
||||
import { Label, LabelSize, LabelColor } from "../components/label.slint";
|
||||
import { Progress } from "../components/progress.slint";
|
||||
|
||||
export component HomePage {
|
||||
callback refresh-profile();
|
||||
callback toggle-tun-mode(bool);
|
||||
callback toggle-system-proxy(bool);
|
||||
callback open-port-settings();
|
||||
|
||||
in property <string> profile-name: "NanoCloud";
|
||||
in property <string> profile-node: "免费-日本1-Ver.7";
|
||||
in property <float> profile-usage: 1.26;
|
||||
in property <float> profile-total: 100;
|
||||
in property <string> profile-expiry: "2025-11-11";
|
||||
in property <string> profile-updated: "2025-10-12 10:05";
|
||||
|
||||
in property <string> location-ip: "47.238.198.100";
|
||||
in property <string> location-region: "日本 · 东京";
|
||||
|
||||
in property <bool> tun-mode-enabled: false;
|
||||
in property <bool> system-proxy-enabled: false;
|
||||
in property <int> proxy-port: 7890;
|
||||
|
||||
VerticalLayout {
|
||||
padding: SpacingSystem.spacing.s4;
|
||||
spacing: SpacingSystem.spacing.s4;
|
||||
|
||||
// Header
|
||||
Text {
|
||||
text: "Home";
|
||||
color: Theme.colors.foreground;
|
||||
font-size: Typography.sizes.xl;
|
||||
font-weight: Typography.weights.bold;
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
VerticalLayout {
|
||||
spacing: SpacingSystem.spacing.s3;
|
||||
|
||||
// Profile Card
|
||||
ProfileCard {
|
||||
profile-name: root.profile-name;
|
||||
profile-node: root.profile-node;
|
||||
usage: root.profile-usage;
|
||||
total: root.profile-total;
|
||||
expiry: root.profile-expiry;
|
||||
updated: root.profile-updated;
|
||||
|
||||
refresh => {
|
||||
root.refresh-profile();
|
||||
}
|
||||
}
|
||||
|
||||
// Location Card
|
||||
LocationCard {
|
||||
ip: root.location-ip;
|
||||
region: root.location-region;
|
||||
}
|
||||
|
||||
// Settings Card
|
||||
gsCard {
|
||||
tun-enabled: root.tun-mode-enabled;
|
||||
proxy-enabled: root.system-proxy-enabled;
|
||||
port: root.proxy-port;
|
||||
|
||||
tun-toggled(enabled) => {
|
||||
root.toggle-tun-mode(enabled);
|
||||
}
|
||||
|
||||
proxy-toggled(enabled) => {
|
||||
root.toggle-system-proxy(enabled);
|
||||
}
|
||||
|
||||
port-clicked => {
|
||||
root.open-port-settings();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component ProfileCard {
|
||||
in property <string> profile-name: "";
|
||||
in property <string> profile-node: "";
|
||||
in property <float> usage: 0;
|
||||
in property <float> total: 100;
|
||||
in property <string> expiry: "";
|
||||
in property <string> updated: "";
|
||||
|
||||
callback refresh();
|
||||
|
||||
Card {
|
||||
VerticalLayout {
|
||||
spacing: SpacingSystem.spacing.s4;
|
||||
|
||||
// Header
|
||||
HorizontalLayout {
|
||||
spacing: SpacingSystem.spacing.s3;
|
||||
padding-bottom: SpacingSystem.ss4;
|
||||
|
||||
// Icon
|
||||
Rectangle {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
background: Theme.colors.primary.transparentize(0.9);
|
||||
|
||||
Text {
|
||||
text: "☁";
|
||||
color: Theme.colors.primary;
|
||||
font-size: 24px;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
|
||||
// Info
|
||||
VerticalLayout {
|
||||
spacing: SpacingSystem.spacing.s1;
|
||||
|
||||
HorizontalLayout {
|
||||
spacing: SpacingSystem.spacing.s2;
|
||||
|
||||
Text {
|
||||
text: root.profile-name;
|
||||
color: Theme.colors.foreground;
|
||||
font-size: Typography.sizes.lg;
|
||||
font-weight: Typography.weights.bold;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalLayout {
|
||||
spacing: SpacingSystem.spacing.s2;
|
||||
|
||||
Text {
|
||||
text: "🌐";
|
||||
font-size: Typography.sizes.xs;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.profile-node;
|
||||
color: Theme.colors.muted-foreground;
|
||||
font-size: Typography.sizes.xs;
|
||||
font-weight: Typography.weights.medium;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
Badge {
|
||||
text: "Vmess";
|
||||
variant: "outline";
|
||||
}
|
||||
|
||||
Badge {
|
||||
text: "UDP";
|
||||
variant: "outline";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectan horizontal-stretch: 1;
|
||||
}
|
||||
|
||||
// Refresh button
|
||||
Button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
variant: "ghost";
|
||||
text: "↻";
|
||||
|
||||
clicked => {
|
||||
root.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Separator
|
||||
Rectangle {
|
||||
height: 1px;
|
||||
background: Theme.colors.border.transparentize(0.6);
|
||||
}
|
||||
|
||||
// Stats
|
||||
HorizontalLayout {
|
||||
spacing: SpacingSystem.spacing.s6;
|
||||
|
||||
VerticalLayout {
|
||||
spacing: SpacingSystem.spacing.s1;
|
||||
|
||||
HorizontalLayout {
|
||||
spacing: SpacingSystem.spacing.s1;
|
||||
|
||||
Text {
|
||||
text: "☁";
|
||||
color: Theme.colors.muted-foreground;
|
||||
font-size: Typography.sizes.xs;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "已使用 / 总量";
|
||||
color: Theme.colors.muted-foreground;
|
||||
font-size: Typography.sizes.xs;
|
||||
font-weight: Typography.weights.semibold;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.usage + "GB / " + root.total + "GB";
|
||||
color: Theme.colors.foreground;
|
||||
font-size: Typography.sizes.sm;
|
||||
font-weight: Typography.weights.medium;
|
||||
}
|
||||
}
|
||||
|
||||
VerticalLayout {
|
||||
spacing: SpacingSystem.spacing.s1;
|
||||
|
||||
HorizontalLayout {
|
||||
spacing: SpacingSystem.spacing.s1;
|
||||
|
||||
Text {
|
||||
text: "✓";
|
||||
color: Theme.colors.muted-foreground;
|
||||
font-size: Typography.sizes.xs;
|
||||
vertical-aliter;
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "到期时间";
|
||||
color: Theme.colors.muted-foreground;
|
||||
font-size: Typography.sizes.xs;
|
||||
font-weight: Typography.weights.semibold;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.expiry;
|
||||
color: Theme.colors.foreground;
|
||||
font-size: Typography.sizes.sm;
|
||||
font-weight: Typography.weights.medium;
|
||||
}
|
||||
}
|
||||
|
||||
VerticalLayout {
|
||||
spacing: SpacingSystem.spacing.s1;
|
||||
|
||||
HorizontalLayout {
|
||||
spacing: SpacingSystem.spacing.s1;
|
||||
|
||||
Text {
|
||||
text: "✓";
|
||||
color: Theme.colors.muted-foreground;
|
||||
font-size: Typography.sizes.xs;
|
||||
vertical-: center;
|
||||
n
|
||||
Text {
|
||||
text: "更新时间";
|
||||
color: Theme.colors.muted-foreground;
|
||||
font-size: Typography.sizes.xs;
|
||||
font-weight: Typography.weights.semibold;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.updated;
|
||||
color: Theme.colors.foreground;
|
||||
font-size: Typography.sizes.sm;
|
||||
font-weight: Typography.weights.medium;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component LocationCard {
|
||||
in property <string> ip: "";
|
||||
in property <string> region: "";
|
||||
|
||||
Card {
|
||||
VerticalLayout {
|
||||
spacing: SpacingSystem.spacing.s4;
|
||||
|
||||
// IP and Location
|
||||
HorizontalLayout {
|
||||
spacing: SpacingSystem.spacing.s6;
|
||||
VerticalLayout {
|
||||
spacing: SpacingSystem.spacing.s1;
|
||||
|
||||
HorizontalLayout {
|
||||
spacing: SpacingSystem.spacing.s1;
|
||||
|
||||
Text {
|
||||
text: "🌐";
|
||||
font-size: Typography.sizes.xs;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "IP";
|
||||
color: Theme.colors.muted-foreground;
|
||||
font-size: Typography.sizes.xs;
|
||||
font-weight: Typography.weights.semibold;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.ip;
|
||||
color: Theme.colors.foreground;
|
||||
font-size: Typography.sizes.sm;
|
||||
font-weight: Typography.weights.medium;
|
||||
}
|
||||
}
|
||||
|
||||
VerticalLayout {
|
||||
: SpacingSystem.spacing.s1;
|
||||
|
||||
HorizontalLayout {
|
||||
spacing: SpacingSystem.spacing.s1;
|
||||
|
||||
Text {
|
||||
text: "🌐";
|
||||
font-size: Typography.sizes.xs;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "所属地址";
|
||||
color: Theme.colors.muted-foreground;
|
||||
font-size: Typography.sizes.xs;
|
||||
font-weight: Typography.weights.semibold;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.region;
|
||||
color: Theme.colors.foreground;
|
||||
font-size: Typography.sizes.sm;
|
||||
font-weight: Typography.weights.medium;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Latency
|
||||
VerticalLayout {
|
||||
spacing: SpacingSystem.spacing.s2;
|
||||
|
||||
HorizontalLayout {
|
||||
spacing: SpacingSystem.spacing.s1;
|
||||
|
||||
Text {
|
||||
text: "✓";
|
||||
color: Theme.colors.muted-foreground;
|
||||
font-size: Typography.sizes.xs;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "延迟";
|
||||
color: Theme.colors.muted-foreground;
|
||||
font-size: Typography.sizes.xs;
|
||||
font-weight: Typography.weights.semibold;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
spacing: SpacingSystem.spacing.s2;
|
||||
Row {
|
||||
LatencyItem { service: "Github"; latency: "122ms"; }
|
||||
LatencyItem { service: "Google"; latency: "45ms"; }
|
||||
LatencyItem { service: "Youtube"; latency: "67ms"; }
|
||||
LatencyItem { service: "Twitter"; latency: "89ms"; }
|
||||
}
|
||||
Row {
|
||||
LatencyItem { serviceetflix"; latency: "34ms"; }
|
||||
LatencyItem { service: "Steam"; latency: "156ms"; }
|
||||
LatencyItem { service: "Spotify"; latency: "78ms"; }
|
||||
LatencyItem { service: "Discord"; latency: "92ms"; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component LatencyItem {
|
||||
in property <string> service: "";
|
||||
in property <string> latency: "";
|
||||
|
||||
HorizontalLayout {
|
||||
spacing: SpacingSystem.spacing.s1;
|
||||
|
||||
Text {
|
||||
text: root.service;
|
||||
color: Theme.colors.muted-foreg;
|
||||
font-size: Typography.sizes.xs;
|
||||
font-weight: Typography.weights.medium;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.latency;
|
||||
color: Theme.colors.foreground;
|
||||
font-size: Typography.sizes.xs;
|
||||
font-weight: Typography.weights.semibold;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component SettingsCard {
|
||||
in property <bool> tun-enabled: false;
|
||||
in property <bool> proxy-enabled: false;
|
||||
in property <int> port: 7890;
|
||||
|
||||
callback tun-toggled(bool);
|
||||
callback proxy-toggled(bool);
|
||||
callback port-clicked();
|
||||
|
||||
Card {
|
||||
VerticalLayout {
|
||||
spacing: 0;
|
||||
|
||||
// TUN Mode
|
||||
SettingItem {
|
||||
label: "Tun 模式";
|
||||
|
||||
Switch {
|
||||
checked: root.tun-enabled;
|
||||
toggled(checked) => {
|
||||
root.tun-toggled(checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 1px;
|
||||
background: Theme.colors.border.transparentize(0.6);
|
||||
}
|
||||
|
||||
// System Proxy
|
||||
SettingItem {
|
||||
label: "系统代理";
|
||||
|
||||
Switch {
|
||||
checked: root.proxy-enabled;
|
||||
toggled(checked) => {
|
||||
root.proxy-toggled(checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 1px;
|
||||
background: Theme.colors.border.transparentize(0.6);
|
||||
}
|
||||
|
||||
// Proxy Port
|
||||
SettingItem {
|
||||
label: "代理端口";
|
||||
|
||||
But text: root.port;
|
||||
variant: "link";
|
||||
|
||||
clicked => {
|
||||
root.port-clicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component SettingItem {
|
||||
in property <string> label: "";
|
||||
|
||||
height: 40px;
|
||||
|
||||
HorizontalLayout {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
spacing: SpacingSystem.spacing.s3;
|
||||
alignment: space-between;
|
||||
|
||||
HorizontalLayout {
|
||||
spacing: SpacingSystem.spacing.s2;
|
||||
|
||||
Text {
|
||||
text: "✓";
|
||||
color: Theme.colors.muted-foreground;
|
||||
font-size: Typography.sizes.sm;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.label;
|
||||
color: Theme.colors.foreground;
|
||||
font-size: Typography.sizes.sm;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
|
||||
@children
|
||||
}
|
||||
}
|
||||
170
ui/pages/logs.slint
Normal file
170
ui/pages/logs.slint
Normal file
@@ -0,0 +1,170 @@
|
||||
import { Theme, SpacingSystem, Typography } from "../theme/theme.slint";
|
||||
import { Tabs, TabItem, TabContent } from "../components/tabs.slint";
|
||||
|
||||
export struct LogEntry {
|
||||
time: string,
|
||||
level: string, // "debug", "info", "warn", "error"
|
||||
message: string,
|
||||
}
|
||||
|
||||
export component LogsPage {
|
||||
in property <[LogEntry]> mihomo-logs: [];
|
||||
in property <[LogEntry]> app-logs: [];
|
||||
in-out property <int> current-tab: 0;
|
||||
|
||||
callback tab-changed(int);
|
||||
|
||||
VerticalLayout {
|
||||
padding: SpacingSystem.spacing.s4;
|
||||
spacing: SpacingSystem.spacing.s4;
|
||||
|
||||
// Header with Tabs
|
||||
HorizontalLayout {
|
||||
spacing: SpacingSystem.spacing.s3;
|
||||
alignment: space-between;
|
||||
|
||||
Text {
|
||||
text: "Logs";
|
||||
color: Theme.colors.foreground;
|
||||
font-size: Typography.sizes.xl;
|
||||
font-weight: Typography.weights.bold;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
horizontal-stretch: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Tabs
|
||||
Tabs {
|
||||
tabs: [
|
||||
{ title: "Mihomo Logs" },
|
||||
{ title: "App Logs" },
|
||||
];
|
||||
current-index: root.current-tab;
|
||||
|
||||
tab-changed(index) => {
|
||||
root.current-tab = index;
|
||||
root.tab-changed(index);
|
||||
}
|
||||
|
||||
// Mihomo Logs
|
||||
TabContent {
|
||||
index: 0;
|
||||
current-index: root.current-tab;
|
||||
|
||||
LogView {
|
||||
logs: root.mihomo-logs;
|
||||
}
|
||||
}
|
||||
|
||||
// App Logs
|
||||
TabContent {
|
||||
index: 1;
|
||||
current-index: root.current-tab;
|
||||
|
||||
LogView {
|
||||
logs: root.app-logs;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component LogView {
|
||||
in property <[LogEntry]> logs: [];
|
||||
|
||||
Rectangle {
|
||||
background: Theme.colors.background;
|
||||
border-radius: SpacingSystem.radius.md;
|
||||
border-width: 1px;
|
||||
border-color: Theme.colors.border;
|
||||
|
||||
ScrollView {
|
||||
VerticalLayout {
|
||||
padding: SpacingSystem.spacing.s2;
|
||||
spacing: 0;
|
||||
|
||||
for log[index] in root.logs: LogRow {
|
||||
log-entry: log;
|
||||
odd: Math.mod(index, 2) == 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component LogRow {
|
||||
in property <LogEntry> log-entry;
|
||||
in property ool> odd: false;
|
||||
|
||||
private property <bool> hovered: false;
|
||||
|
||||
private property <color> level-color: log-entry.level == "error" ? #ef4444 :
|
||||
log-entry.level == "warn" ? #eab308 :
|
||||
log-entry.level == "info" ? #3b82f6 :
|
||||
log-entry.level == "debug" ? #6b7280 :
|
||||
Theme.colors.muted-foreground;
|
||||
|
||||
height: 24px;
|
||||
|
||||
states [
|
||||
hovered when root.hovered: {
|
||||
container.background: Theme.colors.muted.transparentize(0.5);
|
||||
}
|
||||
odd when root.odd && !root.hovered: {
|
||||
container.background: Theme.colors.muted.transparentize(0.8);
|
||||
}
|
||||
]
|
||||
|
||||
container := Rectangle {
|
||||
background: transparent;
|
||||
border-radius: SpacingSystem.radius.sm;
|
||||
|
||||
animate background { duration: 150ms; }
|
||||
|
||||
HorizontalLayout {
|
||||
padding-left: SpacingSystem.spacing.s2;
|
||||
padding-right: SpacingSystem.spacing.s2;
|
||||
spacing: SpacingSystem.spacing.s2;
|
||||
|
||||
// Time
|
||||
Text {
|
||||
text: root.log-entry.time;
|
||||
color: Theme.colors.muted-foreground;
|
||||
font-size: 11px;
|
||||
font-family: "monospace";
|
||||
vertical-alignment: center;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
// Level
|
||||
Text {
|
||||
text: root.log-entry.level;
|
||||
color: root.level-color;
|
||||
font-size: 11px;
|
||||
font-family: "monospace";
|
||||
font-weight: Typography.weights.bold;
|
||||
vertical-alignment: center;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
// Message
|
||||
Text {
|
||||
text: root.log-entry.message;
|
||||
color: Theme.colors.foreground;
|
||||
font-size: 11px;
|
||||
font-family: "ospace";
|
||||
vertical-alignment: center;
|
||||
overflow: elide;
|
||||
}
|
||||
}
|
||||
|
||||
touch := TouchArea {
|
||||
moved => {
|
||||
root.hovered = self.has-hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
204
ui/pages/profiles.slint
Normal file
204
ui/pages/profiles.slint
Normal file
@@ -0,0 +1,204 @@
|
||||
import { Theme, SpacingSystem, Typography } from "../theme/theme.slint";
|
||||
import { Card } from "../components/card.slint";
|
||||
import { Button } from "../components/button.slint";
|
||||
import { Badge } from "../components/badge.slint";
|
||||
import { Progress } from "../components/progress.slint";
|
||||
|
||||
export struct ProfileData {
|
||||
id: string,
|
||||
name: string,
|
||||
type: string, // "local" or "remote"
|
||||
description: string,
|
||||
usage: float, // For remote profiles
|
||||
total: float, // For remote profiles
|
||||
updated: string,
|
||||
}
|
||||
|
||||
export component ProfilesPage {
|
||||
in property <[ProfileData]> profiles: [];
|
||||
in property <string> selected-profile-id: "";
|
||||
|
||||
callback add-profile();
|
||||
callback refresh-all();
|
||||
callback select-profile(string);
|
||||
callback edit-profile(string);
|
||||
callback delete-profile(string);
|
||||
callback refresh-profile(string);
|
||||
|
||||
VerticalLayout {
|
||||
padding: SpacingSystem.spacing.s4;
|
||||
spacing: SpacingSystem.spacing.s4;
|
||||
|
||||
// Header
|
||||
HorizontalLayout {
|
||||
spacing: SpacingSystem.spacing.s3;
|
||||
alignment: space-between;
|
||||
|
||||
Text {
|
||||
text: "Profiles";
|
||||
color: Theme.colors.foreground;
|
||||
font-size: Typography.sizes.xl;
|
||||
font-weight: Typography.weights.bold;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
HorizontalLayout {
|
||||
spacing: SpacingSystem.spacing.s2;
|
||||
|
||||
Button {
|
||||
text: "+";
|
||||
variant: "ghost";
|
||||
|
||||
clicked => {
|
||||
root.add-profile();
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "↻";
|
||||
variant: "ghost";
|
||||
|
||||
clicked => {
|
||||
root.refresh-all();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Profile Grid
|
||||
ScrollView {
|
||||
GridLayout {
|
||||
spacing: SpacingSystem.spacing.s3;
|
||||
|
||||
for profile in root.profiles: ProfileItem {
|
||||
profile-data: profile;
|
||||
selected: profile.id == root.selected-profile-id;
|
||||
|
||||
clicked => {
|
||||
root.select-profile(profile.id);
|
||||
}
|
||||
|
||||
refresh-clicked => {
|
||||
root.refresh-profile(profile.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component ProfileItem {
|
||||
in property <ProfileData> profile-data;
|
||||
in property <bool> selected: false;
|
||||
|
||||
callback clicked();
|
||||
callback refresh-clicked();
|
||||
|
||||
private property <bool> hovered: false;
|
||||
|
||||
min-width: 288px; // 18rem
|
||||
height: 96px;
|
||||
|
||||
states [
|
||||
selected when root.selected: {
|
||||
container.border-color: Theme.colors.primary.transparentize(0.5);
|
||||
container.background: Theme.colors.accent.transparentize(0.4);
|
||||
}
|
||||
]
|
||||
|
||||
container := Card {
|
||||
border-width: 2px;
|
||||
|
||||
VerticalLa spacing: SpacingSystem.spacing.s2;
|
||||
|
||||
// Header
|
||||
HorizontalLayout {
|
||||
spacing: SpacingSystem.spacing.s2;
|
||||
alignment: space-between;
|
||||
|
||||
HorizontalLayout {
|
||||
spacing: SpacingSystem.spacing.s2;
|
||||
|
||||
Text {
|
||||
text: root.profile-data.type == "remote" ? "☁" : "📄";
|
||||
font-size: Typography.sizes.lg;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
{
|
||||
text: root.profile-data.name;
|
||||
color: Theme.colors.foreground;
|
||||
font-size: Typography.sizes.base;
|
||||
font-weight: Typography.weights.semibold;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
|
||||
if root.profile-data.type == "remote": Button {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
text: "↻";
|
||||
variant: "ghost";
|
||||
|
||||
clicked => {
|
||||
root.refresh-clicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Description
|
||||
Text {
|
||||
text: root.profile-data.description;
|
||||
color: Theme.colors.muted-foreground;
|
||||
font-size: Typography.sizes.xs;
|
||||
overflow: elide;
|
||||
}
|
||||
|
||||
// Progress bar for remote profiles
|
||||
if root.profile-data.type == "remote": VerticalLayout {
|
||||
spacing: SpacingSystem.spacing.s1;
|
||||
|
||||
Progress {
|
||||
value: (root.profile-data.usage / root.profile-data.total) * 100;
|
||||
}
|
||||
|
||||
HorizontalLayout {
|
||||
alignment: space-between;
|
||||
|
||||
Text {
|
||||
text: "Updated: " + root.profile-data.updated;
|
||||
color: Theme.colors.muted-foreground;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.profile-data.usage + " / " + root.profile-data.total + " GB";
|
||||
color: Theme.colors.oreground;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Local profile info
|
||||
if root.profile-data.type == "local": HorizontalLayout {
|
||||
alignment: space-between;
|
||||
|
||||
Text {
|
||||
text: "Local Profile";
|
||||
color: Theme.colors.muted-foreground;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
touch := TouchArea {
|
||||
clicked => {
|
||||
root.clicked();
|
||||
}
|
||||
|
||||
moved => {
|
||||
root.hovered = self.has-hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
226
ui/pages/proxies.slint
Normal file
226
ui/pages/proxies.slint
Normal file
@@ -0,0 +1,226 @@
|
||||
import { Theme, SpacingSystem, Typography } from "../theme/theme.slint";
|
||||
import { Tabs, TabItem, TabContent } from "../components/tabs.slint";
|
||||
import { Accordion, AccordionItemData, AccordionContent } from "../components/accordion.slint";
|
||||
import { Item } from "../components/item.slint";
|
||||
|
||||
export struct ProxyNode {
|
||||
name: string,
|
||||
type: string,
|
||||
latency: string,
|
||||
}
|
||||
|
||||
export struct ProxyGroup {
|
||||
name: string,
|
||||
nodes: [ProxyNode],
|
||||
expanded: bool,
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
VerticalLayout {
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
336
ui/pages/settings.slint
Normal file
336
ui/pages/settings.slint
Normal file
@@ -0,0 +1,336 @@
|
||||
import { Theme, SpacingSystem, Typography } from "../theme/theme.slint";
|
||||
import { Card } from "../components/card.slint";
|
||||
import { Switch } from "../components/switch.slint";
|
||||
import { Button } from "../components/button.slint";
|
||||
import { Select, SelectOption } from "../components/select.slint";
|
||||
import { Separator, Orientation } from "../components/separator.slint";
|
||||
|
||||
export component SettingsPage {
|
||||
// App Settings
|
||||
in property <bool> auto-start: false;
|
||||
in property <bool> silent-start: false;
|
||||
in property <bool> clash-core: false;
|
||||
|
||||
// Clash Settings
|
||||
in property <bool> tun-mode: false;
|
||||
in property <bool> allow-lan: false;
|
||||
in property <bool> ipv6: false;
|
||||
in property <bool> unified-delay: false;
|
||||
in property <int> log-level-index: 1;
|
||||
in property <int> proxy-port: 7890;
|
||||
|
||||
// Misc
|
||||
in property <string> app-dir: "C:/Program Files/Clash";
|
||||
in property <string> config-dir: "C:/Users/User/.config/clash";
|
||||
in property <string> core-dir: "C:/Program Files/Clash/core";
|
||||
in property <string> app-version: "1.0.0";
|
||||
|
||||
callback toggle-auto-start(bool);
|
||||
callback toggle-silent-start(bool);
|
||||
callback toggle-clash-core(bool);
|
||||
callback toggle-tun-mode(bool);
|
||||
callback toggle-allow-lan(bool);
|
||||
callback toggle-ipv6(bool);
|
||||
callback toggle-unified-delay(bool);
|
||||
callback log-level-changed(int);
|
||||
callback open-port-settings();
|
||||
callback open-tun-config();
|
||||
callback open-directory(string);
|
||||
|
||||
VerticalLayout {
|
||||
padding: SpacingSystem.spacing.s4;
|
||||
spacing: SpacingSystem.spacing.s4;
|
||||
|
||||
// Header
|
||||
Text {
|
||||
text: "Settings";
|
||||
color: Theme.colors.foreground;
|
||||
font-size: Typography.sizes.xl;
|
||||
font-weight: Typography.weights.bold;
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
VerticalLayout {
|
||||
spacing: SpacingSystem.spacing.s3;
|
||||
|
||||
// App Settings Section
|
||||
Card {
|
||||
VerticalLayout {
|
||||
spacing: SpacingSystem.spacing.s3;
|
||||
|
||||
Text {
|
||||
text: "App Settings";
|
||||
color: Theme.colors.foreground;
|
||||
font-size: Typography.sizes.base;
|
||||
font-weight: Typography.weights.semibold;
|
||||
}
|
||||
|
||||
SettingRow {
|
||||
icon: "🚀";
|
||||
label: "Auto Start";
|
||||
|
||||
Switch {
|
||||
checked: root.auto-start;
|
||||
toggled(checked) => {
|
||||
root.toggle-auto-start(checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Separator { orientation: Orientation.horizontal; }
|
||||
|
||||
SettingRow {
|
||||
icon: "🔇";
|
||||
label: "Silent Start";
|
||||
|
||||
Switch {
|
||||
checked: root.silent-start;
|
||||
toggled(checked) => {
|
||||
root.toggle-silent-start(checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Separator { orientation: Orientation.horizontal; }
|
||||
|
||||
SettingRow {
|
||||
icon: "⚙";
|
||||
label: "Clash Core";
|
||||
|
||||
Switch {
|
||||
checked: root.clash-core;
|
||||
toggled(checked) => {
|
||||
root.toggle-clash-core(checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clash Settings Section
|
||||
Card {
|
||||
VerticalLayout {
|
||||
spacing: SpacingSystem.spacing.s3;
|
||||
|
||||
Text {
|
||||
text: "Clash Settings";
|
||||
color: Theme.colors.foreground;
|
||||
font-size: Typography.sizes.base;
|
||||
font-weight: Typography.weights.semibold;
|
||||
}
|
||||
|
||||
SettingRow {
|
||||
icon: "🌐";
|
||||
label: "TUN Mode";
|
||||
|
||||
HorizontalLayout {
|
||||
spacing: SpacingSystem.spacing.s2;
|
||||
|
||||
Switch
|
||||
checked: root.tun-mode;
|
||||
toggled(checked) => {
|
||||
root.toggle-tun-mode(checked);
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
text: "⚙";
|
||||
variant: "ghost";
|
||||
|
||||
clicked => {
|
||||
root.open-tun-config();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Separator { orientation: Orientation.horizontal; }
|
||||
|
||||
SettingRow {
|
||||
icon: "📡";
|
||||
label: "Allow LAN";
|
||||
|
||||
Switch {
|
||||
checked: root.allow-lan;
|
||||
toggled(checked) => {
|
||||
root.toggle-allow-lan(checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Separator { orientation: Orientation.horizontal; }
|
||||
|
||||
SettingRow {
|
||||
icon: "🌍";
|
||||
label: "IPv6";
|
||||
|
||||
Switch {
|
||||
checked: root.ipv6;
|
||||
toggled(checked) => {
|
||||
root.toggle-ipv6(checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Separator { orientation: Orientation.horizontal; }
|
||||
|
||||
SettingRow {
|
||||
icon: "⏱";
|
||||
label: "Unified Delay";
|
||||
|
||||
Switch {
|
||||
checked: root.unified-delay;
|
||||
toggled(checked) => {
|
||||
root.toggle-unified-delay(checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Separator { orientation: Orientation.horizontal; }
|
||||
|
||||
SettingRow {
|
||||
icon: "📝";
|
||||
label: "Log Level";
|
||||
|
||||
Select {
|
||||
options: [
|
||||
{ label: "Debug", valueg" },
|
||||
{ label: "Info", value: "info" },
|
||||
{ label: "Warn", value: "warn" },
|
||||
{ label: "Error", value: "error" },
|
||||
];
|
||||
selected-index: root.log-level-index;
|
||||
|
||||
selected(index, value) => {
|
||||
root.log-level-changed(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Separator { orientation: Orientation.horizontal; }
|
||||
|
||||
SettingRow {
|
||||
icon: "🔌";
|
||||
label: "Proxy Port";
|
||||
|
||||
Button {
|
||||
text: root.proxy-port;
|
||||
variant: "link";
|
||||
|
||||
clicked => {
|
||||
root.open-port-settings();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Miscellaneous Section
|
||||
Card {
|
||||
VerticalLayout {
|
||||
spacing: SpacingSystem.spacing.s3;
|
||||
|
||||
Text {
|
||||
text: "Miscellaneous";
|
||||
color: Theme.colors.foreground;
|
||||
font-size: Typography.sizes.base;
|
||||
font-weight: Typography.weights.semibold;
|
||||
}
|
||||
|
||||
SettingRow {
|
||||
icon: "📁";
|
||||
l: "App Directory";
|
||||
|
||||
Button {
|
||||
text: "Open";
|
||||
variant: "link";
|
||||
|
||||
clicked => {
|
||||
root.open-directory(root.app-dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Separator { orientation: Orientation.horizontal; }
|
||||
|
||||
SettingRow {
|
||||
icon: "📁";
|
||||
label: "Config Directory";
|
||||
|
||||
Button {
|
||||
text: "Open";
|
||||
variant: "link";
|
||||
|
||||
clicked => {
|
||||
root.open-directory(root.config-dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Separator { orientation: Orientation.horizontal; }
|
||||
|
||||
SettingRow {
|
||||
icon: "📁";
|
||||
label: "Core Directory";
|
||||
|
||||
Button {
|
||||
text: "Open";
|
||||
variant: "link";
|
||||
|
||||
clicked => {
|
||||
root.open-directory(root.core-dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Separator { orientation: Orientation.horizontal; }
|
||||
|
||||
SettingRow {
|
||||
icon: "ℹ";
|
||||
label: "App Version";
|
||||
|
||||
Text {
|
||||
text: root.app-version;
|
||||
color: Theme.colors.muted-foreground;
|
||||
font-size: Typography.sizes.sm;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component SettingRow {
|
||||
in property <string> icon: "";
|
||||
in property <string> label: "";
|
||||
|
||||
height: 40px;
|
||||
|
||||
HorizontalLayout {
|
||||
spacing: SpacingSystem.spacing.s3;
|
||||
alignment: space-between;
|
||||
|
||||
HorizontalLayout {
|
||||
spacing: SpacingSystem.spacing.s2;
|
||||
|
||||
Text {
|
||||
text: root.icon;
|
||||
font-size: Typography.sizes.base;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.label;
|
||||
color: Theme.colors.foreground;
|
||||
font-size: Typography.sizes.sm;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
|
||||
@children
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user