diff --git a/Cargo.lock b/Cargo.lock index 86f3c5e..0842c8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1023,6 +1023,46 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +[[package]] +name = "drm" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bc8c5c6c2941f70a55c15f8d9f00f9710ebda3ffda98075f996a0e6c92756f" +dependencies = [ + "bitflags 2.10.0", + "bytemuck", + "drm-ffi", + "drm-fourcc", + "libc", + "rustix 0.38.44", +] + +[[package]] +name = "drm-ffi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e41459d99a9b529845f6d2c909eb9adf3b6d2f82635ae40be8de0601726e8b" +dependencies = [ + "drm-sys", + "rustix 0.38.44", +] + +[[package]] +name = "drm-fourcc" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" + +[[package]] +name = "drm-sys" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bafb66c8dbc944d69e15cfcc661df7e703beffbaec8bd63151368b06c5f9858c" +dependencies = [ + "libc", + "linux-raw-sys 0.6.5", +] + [[package]] name = "either" version = "1.15.0" @@ -1454,6 +1494,28 @@ dependencies = [ "slab", ] +[[package]] +name = "gbm" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce852e998d3ca5e4a97014fb31c940dc5ef344ec7d364984525fd11e8a547e6a" +dependencies = [ + "bitflags 2.10.0", + "drm", + "drm-fourcc", + "gbm-sys", + "libc", +] + +[[package]] +name = "gbm-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13a5f2acc785d8fb6bf6b7ab6bfb0ef5dad4f4d97e8e70bb8e470722312f76f" +dependencies = [ + "libc", +] + [[package]] name = "gethostname" version = "1.1.0" @@ -1700,6 +1762,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + [[package]] name = "hermit-abi" version = "0.5.2" @@ -1718,6 +1786,26 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "i-slint-backend-linuxkms" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8fd06c00fbdac3dd490cf5c10da7daad3820d775060a19ea277d8ab944a160b" +dependencies = [ + "calloop 0.14.3", + "drm", + "gbm", + "glutin", + "i-slint-common", + "i-slint-core", + "i-slint-renderer-skia", + "input", + "memmap2", + "nix", + "raw-window-handle", + "xkbcommon", +] + [[package]] name = "i-slint-backend-selector" version = "1.14.1" @@ -1725,6 +1813,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e138660c634d6bbdf98bb2d0cfa487cda28e032e133a2a2c974f1cc494198765" dependencies = [ "cfg-if", + "i-slint-backend-linuxkms", "i-slint-backend-winit", "i-slint-common", "i-slint-core", @@ -2114,6 +2203,25 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "input" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbdc09524a91f9cacd26f16734ff63d7dc650daffadd2b6f84d17a285bd875a9" +dependencies = [ + "bitflags 2.10.0", + "input-sys", + "libc", + "log", + "udev", +] + +[[package]] +name = "input-sys" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd4f5b4d1c00331c5245163aacfe5f20be75b564c7112d45893d4ae038119eb0" + [[package]] name = "integer-sqrt" version = "0.1.5" @@ -2134,6 +2242,17 @@ dependencies = [ "syn", ] +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "itertools" version = "0.13.0" @@ -2299,6 +2418,16 @@ dependencies = [ "redox_syscall 0.7.0", ] +[[package]] +name = "libudev-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "linebender_resource_handle" version = "0.1.1" @@ -2335,6 +2464,12 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +[[package]] +name = "linux-raw-sys" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a385b1be4e5c3e362ad2ffa73c392e53f031eaa5b7d648e64cd87f27f6063d7" + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -2598,6 +2733,18 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -3278,7 +3425,7 @@ checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi", + "hermit-abi 0.5.2", "pin-project-lite", "rustix 1.1.3", "windows-sys 0.61.2", @@ -4087,6 +4234,7 @@ checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" dependencies = [ "as-raw-xcb-connection", "bytemuck", + "drm", "fastrand", "js-sys", "memmap2", @@ -4489,6 +4637,18 @@ dependencies = [ "serde", ] +[[package]] +name = "udev" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4e37e9ea4401fc841ff54b9ddfc9be1079b1e89434c1a6a865dd68980f7e9f" +dependencies = [ + "io-lifetimes", + "libc", + "libudev-sys", + "pkg-config", +] + [[package]] name = "uds_windows" version = "1.1.0" @@ -5263,6 +5423,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -5314,6 +5483,21 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -5362,6 +5546,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -5380,6 +5570,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -5398,6 +5594,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -5428,6 +5630,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -5446,6 +5654,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -5464,6 +5678,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -5482,6 +5702,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -5638,6 +5864,17 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" +[[package]] +name = "xkbcommon" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a974f48060a14e95705c01f24ad9c3345022f4d97441b8a36beb7ed5c4a02d" +dependencies = [ + "libc", + "memmap2", + "xkeysym", +] + [[package]] name = "xkbcommon-dl" version = "0.4.2" diff --git a/Cargo.toml b/Cargo.toml index 6de112b..7442393 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,9 +3,12 @@ name = "slint-sample" version = "0.0.1" edition = "2024" -[dependencies] +[target.'cfg(target_os = "macos")'.dependencies] slint = { version = "1.14", default-features = false, features = ["backend-winit", "renderer-femtovg-wgpu", "compat-1-2"] } +[target.'cfg(windows)'.dependencies] +slint = { version = "1.14", default-features = false, features = ["backend-winit", "renderer-skia", "compat-1-2"] } + [build-dependencies] slint-build = "1.14" diff --git a/build.rs b/build.rs index c984be2..103ac7e 100644 --- a/build.rs +++ b/build.rs @@ -1,3 +1,3 @@ fn main() { - slint_build::compile("ui/demo.slint").unwrap(); + slint_build::compile("ui/app-full.slint").unwrap(); } diff --git a/src/main.rs b/src/main.rs index 96bdc0e..ee32dbc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,59 +1,48 @@ slint::include_modules!(); -use slint::{ComponentHandle, Model, ModelRc, VecModel}; -use std::rc::Rc; +use slint::ComponentHandle; +use std::time::Duration; fn main() -> Result<(), slint::PlatformError> { - // Full version with all features - let ui = Demo::new()?; + let ui = App::new()?; - // Toast management - let toasts: Rc> = Rc::new(VecModel::default()); - ui.set_toasts(ModelRc::from(toasts.clone())); + // Initialize state + ui.set_mihomo_status(StatusState::Stopped); + ui.set_mihomo_loading(false); - // Handle add-task callback + // Setup toggle callback let ui_weak = ui.as_weak(); - let toasts_clone = toasts.clone(); - ui.on_add_task(move |task_title| { + ui.on_toggle_mihomo(move || { let ui = ui_weak.unwrap(); + let current_status = ui.get_mihomo_status(); - // Show success toast - toasts_clone.push(ToastMessage { - message: format!("Task '{}' added successfully!", task_title).into(), - variant: "success".into(), - show: true, - }); + ui.set_mihomo_loading(true); - // Auto-dismiss after 3 seconds - let toasts_clone2 = toasts_clone.clone(); - let index = toasts_clone.row_count() - 1; - slint::Timer::single_shot(std::time::Duration::from_secs(3), move || { - if index < toasts_clone2.row_count() { - toasts_clone2.remove(index); + // Simulate async operation + let ui_weak2 = ui_weak.clone(); + slint::Timer::single_shot(Duration::from_millis(1500), move || { + let ui = ui_weak2.unwrap(); + + match current_status { + StatusState::Stopped => { + ui.set_mihomo_status(StatusState::Running); + println!("Mihomo started"); + } + StatusState::Running => { + ui.set_mihomo_status(StatusState::Stopped); + println!("Mihomo stopped"); + } + _ => {} } + + ui.set_mihomo_loading(false); }); - // Clear input - ui.set_new_task_title("".into()); + // Set to starting state + if current_status == StatusState::Stopped { + ui.set_mihomo_status(StatusState::Starting); + } }); - // Handle show-toast callback - let toasts_clone = toasts.clone(); - ui.on_show_toast(move |message, variant| { - toasts_clone.push(ToastMessage { - message: message.clone(), - variant: variant.clone(), - show: true, - }); - - // Auto-dismiss after 3 seconds - let toasts_clone2 = toasts_clone.clone(); - let index = toasts_clone.row_count() - 1; - slint::Timer::single_shot(std::time::Duration::from_secs(3), move || { - if index < toasts_clone2.row_count() { - toasts_clone2.remove(index); - } - }); - }); ui.run() } diff --git a/ui/app-complete.slint b/ui/app-complete.slint new file mode 100644 index 0000000..e69de29 diff --git a/ui/app-full.slint b/ui/app-full.slint new file mode 100644 index 0000000..6d44ded --- /dev/null +++ b/ui/app-full.slint @@ -0,0 +1,243 @@ +import { Theme } from "./theme/theme.slint"; +import { Button } from "./components/button.slint"; +import { StatusIndicator, StatusState } from "./components/status-indicator.slint"; +import { HomePage, ProfilesPage, ProxiesPage, LogsPage, ConnectionsPage, SettingsPage } from "./pages-complete.slint"; + +component NavButton { + in property icon: ""; + in property title: ""; + in property active: false; + + callback clicked(); + + private property hovered: false; + + height: 40px; + + Rectangle { + background: active ? Theme.colors.primary.transparentize(0.9) : + hovered ? Theme.colors.muted.transparentize(0.5) : + transparent; + border-radius: 8px; + + HorizontalLayout { + padding-left: 12px; + padding-right: 12px; + spacing: 12px; + + Text { + text: root.icon; + color: active ? Theme.colors.primary : Theme.colors.muted-foreground; + font-size: 18px; + vertical-alignment: center; + width: 20px; + } + + Text { + text: root.title; + color: active ? Theme.colors.foreground : Theme.colors.muted-foreground; + font-size: 14px; + font-weight: 500; + vertical-alignment: center; + } + } + + TouchArea { + clicked => { root.clicked(); } + moved => { root.hovered = self.has-hover; } + } + } +} + +export component App inherits Window { + title: "Clash Manager"; + preferred-width: 1200px; + preferred-height: 800px; + background: Theme.colors.background; + + in-out property current-page: 0; + in-out property mihomo-status: StatusState.stopped; + in-out property mihomo-loading: false; + + callback toggle-mihomo(); + + HorizontalLayout { + // Sidebar + Rectangle { + width: 240px; + background: Theme.colors.background; + border-width: 1px; + border-color: Theme.colors.border; + + VerticalLayout { + padding: 16px; + spacing: 16px; + + // Header + HorizontalLayout { + spacing: 12px; + + Rectangle { + width: 32px; + height: 32px; + border-radius: 8px; + background: Theme.colors.primary; + + Text { + text: "C"; + color: Theme.colors.primary-foreground; + font-size: 18px; + font-weight: 700; + horizontal-alignment: center; + vertical-alignment: center; + } + } + + VerticalLayout { + spacing: 2px; + + Text { + text: "Clash Manager"; + color: Theme.colors.foreground; + font-size: 14px; + font-weight: 600; + } + + Text { + text: "v1.0.0"; + color: Theme.colors.muted-foreground; + font-size: 12px; + } + } + } + + // Navigation + VerticalLayout { + spacing: 4px; + + Text { + text: "NAVIGATION"; + color: Theme.colors.muted-foreground; + font-size: 10px; + font-weight: 600; + } + + NavButton { + icon: "🏠"; + title: "Home"; + active: root.current-page == 0; + clicked => { root.current-page = 0; } + } + + NavButton { + icon: "🌐"; + title: "Proxies"; + active: root.current-page == 1; + clicked => { root.current-page = 1; } + } + + NavButton { + icon: "📋"; + title: "Profiles"; + active: root.current-page == 2; + clicked => { root.current-page = 2; } + } + + NavButton { + icon: "🔗"; + title: "Connections"; + active: root.current-page == 3; + clicked => { root.current-page = 3; } + } + + NavButton { + icon: "📝"; + title: "Logs"; + active: root.current-page == 4; + clicked => { root.current-page = 4; } + } + + NavButton { + icon: "⚙"; + title: "Settings"; + active: root.current-page == 5; + clicked => { root.current-page = 5; } + } + } + + Rectangle { + vertical-stretch: 1; + } + + // Status Footer + Rectangle { + background: mihomo-status == StatusState.starting ? #eab30820 : + mihomo-status == StatusState.running ? #22c55e20 : + transparent; + border-radius: 8px; + + HorizontalLayout { + padding: 12px; + spacing: 12px; + alignment: space-between; + + HorizontalLayout { + spacing: 12px; + + StatusIndicator { + state: root.mihomo-status; + } + + VerticalLayout { + spacing: 2px; + + Text { + text: "Clash"; + color: mihomo-status == StatusState.running ? #22c55e : + mihomo-status == StatusState.starting ? #eab308 : + #9ca3af; + font-size: 12px; + font-weight: 600; + } + + Text { + text: mihomo-status == StatusState.running ? "Running" : + mihomo-status == StatusState.starting ? "Starting..." : + "Stopped"; + color: Theme.colors.muted-foreground; + font-size: 10px; + } + } + } + + Button { + width: 32px; + height: 32px; + text: mihomo-loading ? "..." : (mihomo-status == StatusState.running ? "||" : ">"); + variant: "ghost"; + disabled: mihomo-loading; + + clicked => { + root.toggle-mihomo(); + } + } + } + } + } + } + + // Content Area + Rectangle { + background: Theme.colors.background; + horizontal-stretch: 1; + vertical-stretch: 1; + + if root.current-page == 0: HomePage {} + if root.current-page == 1: ProxiesPage {} + if root.current-page == 2: ProfilesPage {} + if root.current-page == 3: ConnectionsPage {} + if root.current-page == 4: LogsPage {} + if root.current-page == 5: SettingsPage {} + } + } +} diff --git a/ui/app-nav-backup.slint b/ui/app-nav-backup.slint new file mode 100644 index 0000000..69ad254 --- /dev/null +++ b/ui/app-nav-backup.slint @@ -0,0 +1,366 @@ +import { Theme } from "./theme/theme.slint"; +import { Button } from "./components/button.slint"; +import { Card } from "./components/card.slint"; +import { Switch } from "./components/switch.slint"; +import { StatusIndicator, StatusState } from "./components/status-indicator.slint"; +import { ScrollView } from "std-widgets.slint"; + +// Component definitions must come before usage +component NavButton { + in property icon: ""; + in property title: ""; + in property active: false; + + callback clicked(); + + private property hovered: false; + + height: 40px; + + Rectangle { + background: active ? Theme.colors.primary.transparentize(0.9) : + hovered ? Theme.colors.muted.transparentize(0.5) : + transparent; + border-radius: 8px; + + HorizontalLayout { + padding-left: 12px; + padding-right: 12px; + spacing: 12px; + + Text { + text: root.icon; + color: active ? Theme.colors.primary : Theme.colors.muted-foreground; + font-size: 18px; + vertical-alignment: center; + width: 20px; + } + + Text { + text: root.title; + color: active ? Theme.colors.foreground : Theme.colors.muted-foreground; + font-size: 14px; + font-weight: 500; + vertical-alignment: center; + } + } + + TouchArea { + clicked => { root.clicked(); } + moved => { root.hovered = self.has-hover; } + } + } +} + +component HomePage { + VerticalLayout { + padding: 24px; + spacing: 16px; + + Text { + text: "Home"; + font-size: 24px; + font-weight: 700; + color: Theme.colors.foreground; + } + + ScrollView { + VerticalLayout { + spacing: 16px; + + Card { + VerticalLayout { + spacing: 16px; + + Text { + text: "Welcome to Clash Manager"; + font-size: 18px; + font-weight: 600; + color: Theme.colors.foreground; + } + + Text { + text: "Click the navigation buttons on the left to switch between pages."; + color: Theme.colors.muted-foreground; + font-size: 14px; + } + } + } + + Card { + VerticalLayout { + spacing: 12px; + + Text { + text: "Quick Settings"; + font-size: 16px; + font-weight: 600; + color: Theme.colors.foreground; + } + + HorizontalLayout { + spacing: 12px; + alignment: space-between; + + Text { + text: "TUN Mode"; + color: Theme.colors.foreground; + vertical-alignment: center; + } + + Switch { + checked: false; + toggled(checked) => { + debug("TUN mode:", checked); + } + } + } + + HorizontalLayout { + spacing: 12px; + alignment: space-between; + + Text { + text: "System Proxy"; + color: Theme.colors.foreground; + vertical-alignment: center; + } + + Switch { + checked: false; + toggled(checked) => { + debug("System proxy:", checked); + } + } + } + } + } + } + } + } +} + +component SimplePage { + in property page-title: "Page"; + + VerticalLayout { + padding: 24px; + spacing: 16px; + + Text { + text: root.page-title; + font-size: 24px; + font-weight: 700; + color: Theme.colors.foreground; + } + + Card { + VerticalLayout { + spacing: 12px; + + Text { + text: root.page-title + " page content"; + font-size: 16px; + font-weight: 600; + color: Theme.colors.foreground; + } + + Text { + text: "This page is under construction. Full implementation coming soon!"; + color: Theme.colors.muted-foreground; + } + } + } + } +} + +export component App inherits Window { + title: "Clash Manager"; + preferred-width: 1200px; + preferred-height: 800px; + background: Theme.colors.background; + + in-out property current-page: 0; + in-out property mihomo-status: StatusState.stopped; + in-out property mihomo-loading: false; + + callback toggle-mihomo(); + + HorizontalLayout { + // Sidebar + Rectangle { + width: 240px; + background: Theme.colors.background; + border-width: 1px; + border-color: Theme.colors.border; + + VerticalLayout { + padding: 16px; + spacing: 16px; + + // Header + HorizontalLayout { + spacing: 12px; + + Rectangle { + width: 32px; + height: 32px; + border-radius: 8px; + background: Theme.colors.primary; + + Text { + text: "C"; + color: Theme.colors.primary-foreground; + font-size: 18px; + font-weight: 700; + horizontal-alignment: center; + vertical-alignment: center; + } + } + + VerticalLayout { + spacing: 2px; + + Text { + text: "Clash Manager"; + color: Theme.colors.foreground; + font-size: 14px; + font-weight: 600; + } + + Text { + text: "v1.0.0"; + color: Theme.colors.muted-foreground; + font-size: 12px; + } + } + } + + // Navigation + VerticalLayout { + spacing: 4px; + + Text { + text: "NAVIGATION"; + color: Theme.colors.muted-foreground; + font-size: 10px; + font-weight: 600; + } + + NavButton { + icon: "🏠"; + title: "Home"; + active: root.current-page == 0; + clicked => { root.current-page = 0; } + } + + NavButton { + icon: "🌐"; + title: "Proxies"; + active: root.current-page == 1; + clicked => { root.current-page = 1; } + } + + NavButton { + icon: "📋"; + title: "Profiles"; + active: root.current-page == 2; + clicked => { root.current-page = 2; } + } + + NavButton { + icon: "🔗"; + title: "Connections"; + active: root.current-page == 3; + clicked => { root.current-page = 3; } + } + + NavButton { + icon: "📝"; + title: "Logs"; + active: root.current-page == 4; + clicked => { root.current-page = 4; } + } + + NavButton { + icon: "⚙"; + title: "Settings"; + active: root.current-page == 5; + clicked => { root.current-page = 5; } + } + } + + Rectangle { + vertical-stretch: 1; + } + + // Status Footer + Rectangle { + background: mihomo-status == StatusState.starting ? #eab30820 : + mihomo-status == StatusState.running ? #22c55e20 : + transparent; + border-radius: 8px; + + HorizontalLayout { + padding: 12px; + spacing: 12px; + alignment: space-between; + + HorizontalLayout { + spacing: 12px; + + StatusIndicator { + state: root.mihomo-status; + } + + VerticalLayout { + spacing: 2px; + + Text { + text: "Clash"; + color: mihomo-status == StatusState.running ? #22c55e : + mihomo-status == StatusState.starting ? #eab308 : + #9ca3af; + font-size: 12px; + font-weight: 600; + } + + Text { + text: mihomo-status == StatusState.running ? "Running" : + mihomo-status == StatusState.starting ? "Starting..." : + "Stopped"; + color: Theme.colors.muted-foreground; + font-size: 10px; + } + } + } + + Button { + width: 32px; + height: 32px; + text: mihomo-loading ? "..." : (mihomo-status == StatusState.running ? "||" : ">"); + variant: "ghost"; + disabled: mihomo-loading; + + clicked => { + root.toggle-mihomo(); + } + } + } + } + } + } + + // Content Area + Rectangle { + background: Theme.colors.background; + + if root.current-page == 0: HomePage {} + if root.current-page == 1: SimplePage { page-title: "Proxies"; } + if root.current-page == 2: SimplePage { page-title: "Profiles"; } + if root.current-page == 3: SimplePage { page-title: "Connections"; } + if root.current-page == 4: SimplePage { page-title: "Logs"; } + if root.current-page == 5: SimplePage { page-title: "Settings"; } + } + } +} diff --git a/ui/app-nav.slint b/ui/app-nav.slint new file mode 100644 index 0000000..69ad254 --- /dev/null +++ b/ui/app-nav.slint @@ -0,0 +1,366 @@ +import { Theme } from "./theme/theme.slint"; +import { Button } from "./components/button.slint"; +import { Card } from "./components/card.slint"; +import { Switch } from "./components/switch.slint"; +import { StatusIndicator, StatusState } from "./components/status-indicator.slint"; +import { ScrollView } from "std-widgets.slint"; + +// Component definitions must come before usage +component NavButton { + in property icon: ""; + in property title: ""; + in property active: false; + + callback clicked(); + + private property hovered: false; + + height: 40px; + + Rectangle { + background: active ? Theme.colors.primary.transparentize(0.9) : + hovered ? Theme.colors.muted.transparentize(0.5) : + transparent; + border-radius: 8px; + + HorizontalLayout { + padding-left: 12px; + padding-right: 12px; + spacing: 12px; + + Text { + text: root.icon; + color: active ? Theme.colors.primary : Theme.colors.muted-foreground; + font-size: 18px; + vertical-alignment: center; + width: 20px; + } + + Text { + text: root.title; + color: active ? Theme.colors.foreground : Theme.colors.muted-foreground; + font-size: 14px; + font-weight: 500; + vertical-alignment: center; + } + } + + TouchArea { + clicked => { root.clicked(); } + moved => { root.hovered = self.has-hover; } + } + } +} + +component HomePage { + VerticalLayout { + padding: 24px; + spacing: 16px; + + Text { + text: "Home"; + font-size: 24px; + font-weight: 700; + color: Theme.colors.foreground; + } + + ScrollView { + VerticalLayout { + spacing: 16px; + + Card { + VerticalLayout { + spacing: 16px; + + Text { + text: "Welcome to Clash Manager"; + font-size: 18px; + font-weight: 600; + color: Theme.colors.foreground; + } + + Text { + text: "Click the navigation buttons on the left to switch between pages."; + color: Theme.colors.muted-foreground; + font-size: 14px; + } + } + } + + Card { + VerticalLayout { + spacing: 12px; + + Text { + text: "Quick Settings"; + font-size: 16px; + font-weight: 600; + color: Theme.colors.foreground; + } + + HorizontalLayout { + spacing: 12px; + alignment: space-between; + + Text { + text: "TUN Mode"; + color: Theme.colors.foreground; + vertical-alignment: center; + } + + Switch { + checked: false; + toggled(checked) => { + debug("TUN mode:", checked); + } + } + } + + HorizontalLayout { + spacing: 12px; + alignment: space-between; + + Text { + text: "System Proxy"; + color: Theme.colors.foreground; + vertical-alignment: center; + } + + Switch { + checked: false; + toggled(checked) => { + debug("System proxy:", checked); + } + } + } + } + } + } + } + } +} + +component SimplePage { + in property page-title: "Page"; + + VerticalLayout { + padding: 24px; + spacing: 16px; + + Text { + text: root.page-title; + font-size: 24px; + font-weight: 700; + color: Theme.colors.foreground; + } + + Card { + VerticalLayout { + spacing: 12px; + + Text { + text: root.page-title + " page content"; + font-size: 16px; + font-weight: 600; + color: Theme.colors.foreground; + } + + Text { + text: "This page is under construction. Full implementation coming soon!"; + color: Theme.colors.muted-foreground; + } + } + } + } +} + +export component App inherits Window { + title: "Clash Manager"; + preferred-width: 1200px; + preferred-height: 800px; + background: Theme.colors.background; + + in-out property current-page: 0; + in-out property mihomo-status: StatusState.stopped; + in-out property mihomo-loading: false; + + callback toggle-mihomo(); + + HorizontalLayout { + // Sidebar + Rectangle { + width: 240px; + background: Theme.colors.background; + border-width: 1px; + border-color: Theme.colors.border; + + VerticalLayout { + padding: 16px; + spacing: 16px; + + // Header + HorizontalLayout { + spacing: 12px; + + Rectangle { + width: 32px; + height: 32px; + border-radius: 8px; + background: Theme.colors.primary; + + Text { + text: "C"; + color: Theme.colors.primary-foreground; + font-size: 18px; + font-weight: 700; + horizontal-alignment: center; + vertical-alignment: center; + } + } + + VerticalLayout { + spacing: 2px; + + Text { + text: "Clash Manager"; + color: Theme.colors.foreground; + font-size: 14px; + font-weight: 600; + } + + Text { + text: "v1.0.0"; + color: Theme.colors.muted-foreground; + font-size: 12px; + } + } + } + + // Navigation + VerticalLayout { + spacing: 4px; + + Text { + text: "NAVIGATION"; + color: Theme.colors.muted-foreground; + font-size: 10px; + font-weight: 600; + } + + NavButton { + icon: "🏠"; + title: "Home"; + active: root.current-page == 0; + clicked => { root.current-page = 0; } + } + + NavButton { + icon: "🌐"; + title: "Proxies"; + active: root.current-page == 1; + clicked => { root.current-page = 1; } + } + + NavButton { + icon: "📋"; + title: "Profiles"; + active: root.current-page == 2; + clicked => { root.current-page = 2; } + } + + NavButton { + icon: "🔗"; + title: "Connections"; + active: root.current-page == 3; + clicked => { root.current-page = 3; } + } + + NavButton { + icon: "📝"; + title: "Logs"; + active: root.current-page == 4; + clicked => { root.current-page = 4; } + } + + NavButton { + icon: "⚙"; + title: "Settings"; + active: root.current-page == 5; + clicked => { root.current-page = 5; } + } + } + + Rectangle { + vertical-stretch: 1; + } + + // Status Footer + Rectangle { + background: mihomo-status == StatusState.starting ? #eab30820 : + mihomo-status == StatusState.running ? #22c55e20 : + transparent; + border-radius: 8px; + + HorizontalLayout { + padding: 12px; + spacing: 12px; + alignment: space-between; + + HorizontalLayout { + spacing: 12px; + + StatusIndicator { + state: root.mihomo-status; + } + + VerticalLayout { + spacing: 2px; + + Text { + text: "Clash"; + color: mihomo-status == StatusState.running ? #22c55e : + mihomo-status == StatusState.starting ? #eab308 : + #9ca3af; + font-size: 12px; + font-weight: 600; + } + + Text { + text: mihomo-status == StatusState.running ? "Running" : + mihomo-status == StatusState.starting ? "Starting..." : + "Stopped"; + color: Theme.colors.muted-foreground; + font-size: 10px; + } + } + } + + Button { + width: 32px; + height: 32px; + text: mihomo-loading ? "..." : (mihomo-status == StatusState.running ? "||" : ">"); + variant: "ghost"; + disabled: mihomo-loading; + + clicked => { + root.toggle-mihomo(); + } + } + } + } + } + } + + // Content Area + Rectangle { + background: Theme.colors.background; + + if root.current-page == 0: HomePage {} + if root.current-page == 1: SimplePage { page-title: "Proxies"; } + if root.current-page == 2: SimplePage { page-title: "Profiles"; } + if root.current-page == 3: SimplePage { page-title: "Connections"; } + if root.current-page == 4: SimplePage { page-title: "Logs"; } + if root.current-page == 5: SimplePage { page-title: "Settings"; } + } + } +} diff --git a/ui/app-simple-old.slint b/ui/app-simple-old.slint new file mode 100644 index 0000000..b4177d3 --- /dev/null +++ b/ui/app-simple-old.slint @@ -0,0 +1,884 @@ +import { Theme, SpacingSystem, Typography } from "./theme/theme.slint"; +import { Button } from "./components/button.slint"; +import { Card } from "./components/card.slint"; +import { Switch } from "./components/switch.slint"; +import { Badge } from "./components/badge.slint"; +import { StatusIndicator, StatusState } from "./components/status-indicator.slint"; +import { Progress } from "./components/progress.slint"; + +export component App inherits Window { + title: "Clash Manager"; + preferred-width: 1200px; + preferred-height: 800px; + background: Theme.colors.background; + + in-out property current-page: 0; + in-out property mihomo-status: StatusState.stopped; + in-out property mihomo-loading: false; + + callback toggle-mihomo(); + callback navigate(int); + + HorizontalLayout { + // Sidebar + sidebar := Rectangle { + width: 240px; + background: Theme.colors.background; + border-width: 1px; + border-color: Theme.colors.border; + + VerticalLayout { + padding: 16px; + spacing: 16px; + + // Header + HorizontalLayout { + spacing: 12px; + + Rectangle { + width: 32px; + height: 32px; + border-radius: 8px; + background: Theme.colors.primary; + + Text { + text: "C"; + color: Theme.colors.primary-foreground; + font-size: 18px; + font-weight: 700; + horizontal-alignment: center; + vertical-alignment: center; + } + } + + VerticalLayout { + spacing: 2px; + + Text { + text: "Clash Manager"; + color: Theme.colors.foreground; + font-size: 14px; + font-weight: 600; + } + + Text { + text: "v1.0.0"; + color: Theme.colors.muted-foreground; + font-size: 12px; + } + } + } + + // Navigation + VerticalLayout { + spacing: 4px; + + Text { + text: "NAVIGATION"; + color: Theme.colors.muted-foreground; + font-size: 10px; + font-weight: 600; + } + + NavButton { + icon: "🏠"; + title: "Home"; + active: root.current-page == 0; + clicked => { root.current-page = 0; root.navigate(0); } + } + + NavButton { + icon: "🌐"; + title: "Proxies"; + active: root.current-page == 1; + clicked => { rourrent-page = 1; root.navigate(1); } + } + + NavButton { + icon: "📋"; + title: "Profiles"; + active: root.current-page == 2; + clicked => { root.current-page = 2; root.navigate(2); } + } + + NavButton { + icon: "🔗"; + title: "Connections"; + active: root.current-page == 3; + clicked => { root.current-page = 3; root.navigate(3); } + } + + NavButton { + icon: "📝"; + title: "Logs"; + active: root.current-page == 4; + clicked => { root.current-page = 4; root.navigate(4); } + } + + NavButton { + icon: "⚙"; + title: "Settings"; + active: root.current-page == 5; + clicked => { root.current-page = 5; root.navigate(5); } + } + } + + Rectangle { + vertical-stretch: 1; + } + + // Status Footer + Rectangle { + background: mihomo-status == StatusState.starting ? #eab30820 : + mihomo-status == StatusState.running ? #22c55e20 : + transparent; + border-radius: 8px; + + HorizontalLayout { + padding: 12px; + spacing: 12px; + alignment: space-between; + + HorizontalLayout { + spacing: 12px; + + StatusIndicator { + state: root.mihomo-status; + } + + VerticalLayout { + spacing: 2px; + + Text { + text: "Clash"; + color: mihomo-status == StatusState.running ? #22c55e : + mihomo-status == StatusState.starting ? #eab308 : + #9ca3af; + font-size: 12px; + font-weight: 600; + } + + Text { + text: mihomo-status == StatusState.running ? "Running" : + mihomo-status == StatusState.starting ? "Starting..." : + "Stopped"; + color: Theme.colors.muted-foreground; + font-size: 10px; + } + } + } + + Button { + width: 32px; + height: 32px; + text: mihomo-loading ? "..." : (mihomo-status == StatusState.running ? "||" : ">"); + variant: "ghost"; + disabled: mihomo-loading; + + clicked => { + root.toggle-mihomo(); + } + } + } + } + } + } + + // Content Area + content := Rectangle { + background: Theme.colors.background; + + // Home Page + if root.current-page == 0: HomePage { + toggle-mihomo => { root.toggle-mihomo(); } + } + // Proxies Page + if root.current-page == 1: ProxiesPage {} + + // Profiles Page + if root.current-page == 2: ProfilesPage {} + + // Connections Page + if root.current-page == 3: ConnectionsPage {} + + // Logs Page + if root.current-page == 4: LogsPage {} + + // Settings Page + if root.current-page == 5: SettingsPage {} + } + } +} + +component NavButton { + in property icon: ""; + in property title: ""; + in property active: false; + + callback clicked(); + + private property hovered: false; + + height: 40px; + + container := Rectangle { + background: active ? Theme.colors.primary.transparentize(0.9) : + hovered ? Theme.colors.muted.transparentize(0.5) : + transparent; + border-radius: 8px; + + HorizontalLayout { + padding-left: 12px; + padding-right: 12px; + spacing: 12px; + + Text { + text: root.icon; + color: active ? Theme.colors.primary : Theme.colors.muted-foregr font-size: 18px; + vertical-alignment: center; + width: 20px; + } + + Text { + text: root.title; + color: active ? Theme.colors.foreground : Theme.colors.muted-foreground; + font-size: 14px; + font-weight: 500; + vertical-alignment: center; + } + } + + TouchArea { + clicked => { root.clicked(); } + moved => { root.hovered = self.has-hover; } + } + } +} + +component HomePage { + toggle-mihomo(); + + VerticalLayout { + padding: 24px; + spacing: 16px; + + Text { + text: "Home"; + font-size: 24px; + font-weight: 700; + color: Theme.colors.foreground; + } + + ScrollView { + VerticalLayout { + spacing: 16px; + + // Profile Card + Card { + VerticalLayout { + spacing: 16px; + + HorizontalLayout { + spacing: 12px; + + 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; + } + } + + VerticalLayout { + spacing: 4px; + + HorizontalLayout { + spacing: 8px; + + Text { + text: "NanoCloud"; + color: Theme.colors.foreground; + font-size: 18px; + font-weight: 700; + vertical-alignment: center; + } + } + + HorizontalLayout { + spacing: 8px; + + Text { + text: "🌐 Free-Japan1-Ver.7"; + color: Theme.colors.muted-foreground; + font-size: 12px; + vertical-alignment: center; + } + + Badge { + text: "Vmess"; + variant: "outline"; + } + + Badge { + text: "UDP"; + variant: "outline"; + } + } + } + } + + Rectangle { + height: 1px; + background: Theme.colors.border.transparentize(0.6); + } + + HorizontalLayout { + spacing: 24px; + + VerticalLayout { + spacing: 4px; + + Text { + text: "☁ Usage / Total"; + color: Theme.colors.muted-foreground; + font-size: 11px; + font-weight: 600; + } + + Text { + text: "1.26GB / 100GB"; + color: Theme.colors.foreground; + font-size: 13px; + font-weight: 500; + } + n + VerticalLayout { + spacing: 4px; + + Text { + text: "✓ Expiry Date"; + color: Theme.colors.muted-foreground; + font-size: 11px; + font-weight: 600; + } + + Text { + text: "2025-11-11"; + color: Theme.colors.foreground; + font-size: 13px; + font-weight: 500; + } + } + + VerticalLayout { + spacing: 4px; + + Text { + text: "✓ Last Update"; + color: Theme.colors.muted-foreground; + font-size: 11px; + font-weight: 600; + } + + Text { + text: "2025-10-12 10:05"; + color: Theme.colors.foreground; + font-size: 13px; + font-weight: 500; + } + } + } + } + } + + // Location Card + Card { + VerticalLayout { + spacing: 16px; + + HorizontalLayout { + spacing: 24px; + + VerticalLayout { + spacing: 4px; + + Text { + text: "🌐 IP Address"; + color: Theme.colors.muted-foreground; + font-size: 11px; + font-weight: 600; + } + + Text { + text: "47.238.198.100"; + color: Theme.colors.foreground; + font-size: 13px; + font-weight: 500; + } + } + + VerticalLayout { + spacing: 4px; + + Text { + text: "🌐 Location"; + color: Theme.colors.muted-foreground; + font-size: 11px; + font-weight: 600; + } + + Text { + text: "Japan · Tokyo"; + color: Theme.colors.foreground; + font-size: 13px; + font-weight: 500; + } + } + } + } + } + + // Settings Card + Card { + VerticalLayout { + spacing: 0; + + SettingRow { + label: "TUN Mode"; + Switch { + checked: false; + toggled(checked) => { debug("TUN mode:", checked); } + } + } + + Rectangle { + height: 1px; + background: Theme.colors.border.transparentize(0.6); + } + + SettingRow { + label: "System Proxy"; + Switch { + checked: false; + toggled(checked) => { debug("System proxy:", checked); } + } + } + + Rectangle { + height: 1px; + background: Themelors.border.transparentize(0.6); + } + + SettingRow { + label: "Proxy Port"; + Text { + text: "7890"; + color: Theme.colors.primary; + font-size: 14px; + vertical-alignment: center; + } + } + } + } + } + } + } +} + +component SettingRow { + in property label: ""; + + height: 48px; + + Horout { + spacing: 12px; + alignment: space-between; + + HorizontalLayout { + spacing: 8px; + + Text { + text: "✓"; + color: Theme.colors.muted-foreground; + font-size: 14px; + vertical-alignment: center; + } + + Text { + text: root.label; + color: Theme.colors.foreground; + font-size: 14px; + vertical-alignment: center; + } + } + + @children + } +} + +component ProxiesPage { + Vertica { + padding: 24px; + spacing: 16px; + + Text { + text: "Proxies"; + font-size: 24px; + font-weight: 700; + color: Theme.colors.foreground; + } + + Card { + Text { + text: "Proxy management page - Coming soon"; + color: Theme.colors.muted-foreground; + } + } + } +} + +component ProfilesPage { + VerticalLayout { + padding: 24px; + spacing: 16px; + + Text { + text: "Profiles"; + font-size: 24px; + font-weight: 700; + color: Theme.colors.foreground; + } + + ScrollView { + GridLayout { + spacing: 16px; + + Card { + min-width: 280px; + height: 120px; + + VerticalLayout { + spacing: 8px; + + HorizontalLayout { + spacing: 8px; + + Text { + text: "☁"; + font-size: 18px; + vertical-alignment: center; + } + + Text { + text: "NanoCloud"; + color: Theme.colors.foreground; + font-size: 16px; + font-weight: 600; + vertical-alignment: center; + } + } + + Text { + text: "Free tier subscription - Japan servers"; + color: Theme.colors.muted-foreground; + font-size: 12px; + } + + Progress { + value: 1.26; + } + + HorizontalLayout { + alignment: space-between; + + Text { + text: "Updated: Just now"; + color: Theme.colors.muted-foreground; + font-size: 10px; + } + + Text { + text: "1.26 / 100 GB"; + color: Theme.colors.foreground; + font-size: 10px; + } + } + } + } + + Card { + min-width: 280px; + height: 120px; + + VerticalLayout { + spacing: 8px; + + HorizontalLayout { + spacing: 8px; + + Text { + text: "📄"; + font-size: 18px; + vertical-alignment: center; + } + + Text { + text: "Local Config"; + color: Theme.colors.foreground; + font-size: 16px; + font-weight: 600; + vertical-alignment: center; + } + } + + Text { + text: "Custom local configuration file"; + color: Theme.colors.muted-foreground; + font-size: 12px; + } + + Text { + text: "Local Profile"; + color: Theme.colors.muted-foreground; + font-size: 10px; + } + } + } + } + } + } +} + +component ConnectionsPage { + VerticalLayout { + padding: 24px; + spacing: 16px; + + Text { + text: "Connections"; + font-size: 24px; + font-weight: 700; + color: Theme.colors.foreground; + } + + ScrollView { + VerticalLayout { + spacing: 8px; + + for Card { + height: 60px; + + HorizontalLayout { + spacing: 12px; + + Rectangle { + width: 40px; + height: 40px; + border-radius: 8px; + background: Theme.colors.primary.transparentize(0.9); + + Text { + text: "🔗"; + font-size: 18px; + horizontal-alignment: center; + vertical-alignment: center; + } + } + + VerticalLayout { + spacing: 4px; + + Text { + text: "192.168.1.100:54321 → github.com:443"; + color: Theme.colors.foreground; + font-size: 14px; + font-weight: 500; + } + + HorizontalLayout { + spacing: 12px; + + Text { + text: "HTTPS"; + color: Theme.colors.muted-foreground; + font-size: 12px; + } + + Text { + text: "↑ 1.2 KB/s"; + color: Theme.colors.primary; + font-size: 12px; + } + + Text { + text: "↓ 45.6 KB/s"; + color: Theme.colors.secondary; + font-size: 12px; + } + } + } + } + } + } + } + } +} + +component LogsPage { + VerticalLayout { + padding: 24px; + spacing: 16px; + + Text { + text: "Logs"; + font-size: 24px; + font-weight: 700; + color: Theme.colors.foreground; + } + + Card { + ScrollView { + VerticalLayout { + spacing: 0; + + for i in 20: LogRow { + time: "10:23:45"; + level: i < 5 ? "info" : (i < 10 ? "debug" : (i < 15 ? "warn" : "error")); + message: "Sample log message " + i; + } + } + } + } + } +} + +component LogRow { + in property time: ""; + in property level: ""; + in property message: ""; + + private property level-color: level == "error" ? #ef4444 : + level == "warn" ? #eab308 : + level == "info" ? #3b82f6 : + #6b7280; + + : 24px; + + HorizontalLayout { + padding-left: 8px; + padding-right: 8px; + spacing: 8px; + + Text { + text: root.time; + color: Theme.colors.muted-foreground; + font-size: 11px; + font-family: "monospace"; + vertical-alignment: center; + width: 80px; + } + + Text { + text: root.level; + color: root.level-color; + font-size: 11px; + font-family: "monospace"; + font-weight: 700; + vertical-alignment: center; + width: 60px; + } + + Text { + text: root.message; + color: Theme.colors.foreground; + font-size: 11px; + font-family: "monospace"; + vertical-alignment: center; + } + } +} + +component SettingsPage { + VerticalLayout { + padding: 24px; + spacing: 16px; + + Text { + text: "Settings"; + font-size: 24px; + font-weight: 700; + color: Theme.colors.foreground; + } + + ScrollView { + VerticalLayout { + spacing: 16px; + + Card { + VerticalLayout { + spacing: 12px; + + Text { + text: "App Settings"; + font-size: 16px; + font-weight: 600; + color: Theme.colors.foreground; + } + + SettingRow { + label: "Auto Start"; + Switch { + checked: false; + toggled(checked) debug("Auto start:", checked); } + } + } + + SettingRow { + label: "Silent Start"; + Switch { + checked: false; + toggled(checked) => { debug("Silent start:", checked); } + } + } + } + } + + Card { + VerticalLayout { + spacing: 12px; + + Text { + t Settings"; + font-size: 16px; + font-weight: 600; + color: Theme.colors.foreground; + } + + SettingRow { + label: "Allow LAN"; + Switch { + checked: false; + toggled(checked) => { debug("Allow LAN:", checked); } + } + } + + SettingRow { + label: "IPv6"; + Switch { + checked: false; + toggled(checked) => { debug("IPv6:", checked); } + } + } + } + } + } + } + } +} diff --git a/ui/app-simple.slint b/ui/app-simple.slint new file mode 100644 index 0000000..9055472 --- /dev/null +++ b/ui/app-simple.slint @@ -0,0 +1,5 @@ +import { Theme, SpacingSystem, Typography } from "./theme/theme.slint"; +import { Button } from "./components/button.slint"; +import { Card } from "./components/card.slint"; +import { Switch } from "./components/switch.slint"; +import { StatusIndicator, StatusState } from "./components/status-indicator.slint"; diff --git a/ui/app-simple.slint.bak b/ui/app-simple.slint.bak new file mode 100644 index 0000000..b4177d3 --- /dev/null +++ b/ui/app-simple.slint.bak @@ -0,0 +1,884 @@ +import { Theme, SpacingSystem, Typography } from "./theme/theme.slint"; +import { Button } from "./components/button.slint"; +import { Card } from "./components/card.slint"; +import { Switch } from "./components/switch.slint"; +import { Badge } from "./components/badge.slint"; +import { StatusIndicator, StatusState } from "./components/status-indicator.slint"; +import { Progress } from "./components/progress.slint"; + +export component App inherits Window { + title: "Clash Manager"; + preferred-width: 1200px; + preferred-height: 800px; + background: Theme.colors.background; + + in-out property current-page: 0; + in-out property mihomo-status: StatusState.stopped; + in-out property mihomo-loading: false; + + callback toggle-mihomo(); + callback navigate(int); + + HorizontalLayout { + // Sidebar + sidebar := Rectangle { + width: 240px; + background: Theme.colors.background; + border-width: 1px; + border-color: Theme.colors.border; + + VerticalLayout { + padding: 16px; + spacing: 16px; + + // Header + HorizontalLayout { + spacing: 12px; + + Rectangle { + width: 32px; + height: 32px; + border-radius: 8px; + background: Theme.colors.primary; + + Text { + text: "C"; + color: Theme.colors.primary-foreground; + font-size: 18px; + font-weight: 700; + horizontal-alignment: center; + vertical-alignment: center; + } + } + + VerticalLayout { + spacing: 2px; + + Text { + text: "Clash Manager"; + color: Theme.colors.foreground; + font-size: 14px; + font-weight: 600; + } + + Text { + text: "v1.0.0"; + color: Theme.colors.muted-foreground; + font-size: 12px; + } + } + } + + // Navigation + VerticalLayout { + spacing: 4px; + + Text { + text: "NAVIGATION"; + color: Theme.colors.muted-foreground; + font-size: 10px; + font-weight: 600; + } + + NavButton { + icon: "🏠"; + title: "Home"; + active: root.current-page == 0; + clicked => { root.current-page = 0; root.navigate(0); } + } + + NavButton { + icon: "🌐"; + title: "Proxies"; + active: root.current-page == 1; + clicked => { rourrent-page = 1; root.navigate(1); } + } + + NavButton { + icon: "📋"; + title: "Profiles"; + active: root.current-page == 2; + clicked => { root.current-page = 2; root.navigate(2); } + } + + NavButton { + icon: "🔗"; + title: "Connections"; + active: root.current-page == 3; + clicked => { root.current-page = 3; root.navigate(3); } + } + + NavButton { + icon: "📝"; + title: "Logs"; + active: root.current-page == 4; + clicked => { root.current-page = 4; root.navigate(4); } + } + + NavButton { + icon: "⚙"; + title: "Settings"; + active: root.current-page == 5; + clicked => { root.current-page = 5; root.navigate(5); } + } + } + + Rectangle { + vertical-stretch: 1; + } + + // Status Footer + Rectangle { + background: mihomo-status == StatusState.starting ? #eab30820 : + mihomo-status == StatusState.running ? #22c55e20 : + transparent; + border-radius: 8px; + + HorizontalLayout { + padding: 12px; + spacing: 12px; + alignment: space-between; + + HorizontalLayout { + spacing: 12px; + + StatusIndicator { + state: root.mihomo-status; + } + + VerticalLayout { + spacing: 2px; + + Text { + text: "Clash"; + color: mihomo-status == StatusState.running ? #22c55e : + mihomo-status == StatusState.starting ? #eab308 : + #9ca3af; + font-size: 12px; + font-weight: 600; + } + + Text { + text: mihomo-status == StatusState.running ? "Running" : + mihomo-status == StatusState.starting ? "Starting..." : + "Stopped"; + color: Theme.colors.muted-foreground; + font-size: 10px; + } + } + } + + Button { + width: 32px; + height: 32px; + text: mihomo-loading ? "..." : (mihomo-status == StatusState.running ? "||" : ">"); + variant: "ghost"; + disabled: mihomo-loading; + + clicked => { + root.toggle-mihomo(); + } + } + } + } + } + } + + // Content Area + content := Rectangle { + background: Theme.colors.background; + + // Home Page + if root.current-page == 0: HomePage { + toggle-mihomo => { root.toggle-mihomo(); } + } + // Proxies Page + if root.current-page == 1: ProxiesPage {} + + // Profiles Page + if root.current-page == 2: ProfilesPage {} + + // Connections Page + if root.current-page == 3: ConnectionsPage {} + + // Logs Page + if root.current-page == 4: LogsPage {} + + // Settings Page + if root.current-page == 5: SettingsPage {} + } + } +} + +component NavButton { + in property icon: ""; + in property title: ""; + in property active: false; + + callback clicked(); + + private property hovered: false; + + height: 40px; + + container := Rectangle { + background: active ? Theme.colors.primary.transparentize(0.9) : + hovered ? Theme.colors.muted.transparentize(0.5) : + transparent; + border-radius: 8px; + + HorizontalLayout { + padding-left: 12px; + padding-right: 12px; + spacing: 12px; + + Text { + text: root.icon; + color: active ? Theme.colors.primary : Theme.colors.muted-foregr font-size: 18px; + vertical-alignment: center; + width: 20px; + } + + Text { + text: root.title; + color: active ? Theme.colors.foreground : Theme.colors.muted-foreground; + font-size: 14px; + font-weight: 500; + vertical-alignment: center; + } + } + + TouchArea { + clicked => { root.clicked(); } + moved => { root.hovered = self.has-hover; } + } + } +} + +component HomePage { + toggle-mihomo(); + + VerticalLayout { + padding: 24px; + spacing: 16px; + + Text { + text: "Home"; + font-size: 24px; + font-weight: 700; + color: Theme.colors.foreground; + } + + ScrollView { + VerticalLayout { + spacing: 16px; + + // Profile Card + Card { + VerticalLayout { + spacing: 16px; + + HorizontalLayout { + spacing: 12px; + + 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; + } + } + + VerticalLayout { + spacing: 4px; + + HorizontalLayout { + spacing: 8px; + + Text { + text: "NanoCloud"; + color: Theme.colors.foreground; + font-size: 18px; + font-weight: 700; + vertical-alignment: center; + } + } + + HorizontalLayout { + spacing: 8px; + + Text { + text: "🌐 Free-Japan1-Ver.7"; + color: Theme.colors.muted-foreground; + font-size: 12px; + vertical-alignment: center; + } + + Badge { + text: "Vmess"; + variant: "outline"; + } + + Badge { + text: "UDP"; + variant: "outline"; + } + } + } + } + + Rectangle { + height: 1px; + background: Theme.colors.border.transparentize(0.6); + } + + HorizontalLayout { + spacing: 24px; + + VerticalLayout { + spacing: 4px; + + Text { + text: "☁ Usage / Total"; + color: Theme.colors.muted-foreground; + font-size: 11px; + font-weight: 600; + } + + Text { + text: "1.26GB / 100GB"; + color: Theme.colors.foreground; + font-size: 13px; + font-weight: 500; + } + n + VerticalLayout { + spacing: 4px; + + Text { + text: "✓ Expiry Date"; + color: Theme.colors.muted-foreground; + font-size: 11px; + font-weight: 600; + } + + Text { + text: "2025-11-11"; + color: Theme.colors.foreground; + font-size: 13px; + font-weight: 500; + } + } + + VerticalLayout { + spacing: 4px; + + Text { + text: "✓ Last Update"; + color: Theme.colors.muted-foreground; + font-size: 11px; + font-weight: 600; + } + + Text { + text: "2025-10-12 10:05"; + color: Theme.colors.foreground; + font-size: 13px; + font-weight: 500; + } + } + } + } + } + + // Location Card + Card { + VerticalLayout { + spacing: 16px; + + HorizontalLayout { + spacing: 24px; + + VerticalLayout { + spacing: 4px; + + Text { + text: "🌐 IP Address"; + color: Theme.colors.muted-foreground; + font-size: 11px; + font-weight: 600; + } + + Text { + text: "47.238.198.100"; + color: Theme.colors.foreground; + font-size: 13px; + font-weight: 500; + } + } + + VerticalLayout { + spacing: 4px; + + Text { + text: "🌐 Location"; + color: Theme.colors.muted-foreground; + font-size: 11px; + font-weight: 600; + } + + Text { + text: "Japan · Tokyo"; + color: Theme.colors.foreground; + font-size: 13px; + font-weight: 500; + } + } + } + } + } + + // Settings Card + Card { + VerticalLayout { + spacing: 0; + + SettingRow { + label: "TUN Mode"; + Switch { + checked: false; + toggled(checked) => { debug("TUN mode:", checked); } + } + } + + Rectangle { + height: 1px; + background: Theme.colors.border.transparentize(0.6); + } + + SettingRow { + label: "System Proxy"; + Switch { + checked: false; + toggled(checked) => { debug("System proxy:", checked); } + } + } + + Rectangle { + height: 1px; + background: Themelors.border.transparentize(0.6); + } + + SettingRow { + label: "Proxy Port"; + Text { + text: "7890"; + color: Theme.colors.primary; + font-size: 14px; + vertical-alignment: center; + } + } + } + } + } + } + } +} + +component SettingRow { + in property label: ""; + + height: 48px; + + Horout { + spacing: 12px; + alignment: space-between; + + HorizontalLayout { + spacing: 8px; + + Text { + text: "✓"; + color: Theme.colors.muted-foreground; + font-size: 14px; + vertical-alignment: center; + } + + Text { + text: root.label; + color: Theme.colors.foreground; + font-size: 14px; + vertical-alignment: center; + } + } + + @children + } +} + +component ProxiesPage { + Vertica { + padding: 24px; + spacing: 16px; + + Text { + text: "Proxies"; + font-size: 24px; + font-weight: 700; + color: Theme.colors.foreground; + } + + Card { + Text { + text: "Proxy management page - Coming soon"; + color: Theme.colors.muted-foreground; + } + } + } +} + +component ProfilesPage { + VerticalLayout { + padding: 24px; + spacing: 16px; + + Text { + text: "Profiles"; + font-size: 24px; + font-weight: 700; + color: Theme.colors.foreground; + } + + ScrollView { + GridLayout { + spacing: 16px; + + Card { + min-width: 280px; + height: 120px; + + VerticalLayout { + spacing: 8px; + + HorizontalLayout { + spacing: 8px; + + Text { + text: "☁"; + font-size: 18px; + vertical-alignment: center; + } + + Text { + text: "NanoCloud"; + color: Theme.colors.foreground; + font-size: 16px; + font-weight: 600; + vertical-alignment: center; + } + } + + Text { + text: "Free tier subscription - Japan servers"; + color: Theme.colors.muted-foreground; + font-size: 12px; + } + + Progress { + value: 1.26; + } + + HorizontalLayout { + alignment: space-between; + + Text { + text: "Updated: Just now"; + color: Theme.colors.muted-foreground; + font-size: 10px; + } + + Text { + text: "1.26 / 100 GB"; + color: Theme.colors.foreground; + font-size: 10px; + } + } + } + } + + Card { + min-width: 280px; + height: 120px; + + VerticalLayout { + spacing: 8px; + + HorizontalLayout { + spacing: 8px; + + Text { + text: "📄"; + font-size: 18px; + vertical-alignment: center; + } + + Text { + text: "Local Config"; + color: Theme.colors.foreground; + font-size: 16px; + font-weight: 600; + vertical-alignment: center; + } + } + + Text { + text: "Custom local configuration file"; + color: Theme.colors.muted-foreground; + font-size: 12px; + } + + Text { + text: "Local Profile"; + color: Theme.colors.muted-foreground; + font-size: 10px; + } + } + } + } + } + } +} + +component ConnectionsPage { + VerticalLayout { + padding: 24px; + spacing: 16px; + + Text { + text: "Connections"; + font-size: 24px; + font-weight: 700; + color: Theme.colors.foreground; + } + + ScrollView { + VerticalLayout { + spacing: 8px; + + for Card { + height: 60px; + + HorizontalLayout { + spacing: 12px; + + Rectangle { + width: 40px; + height: 40px; + border-radius: 8px; + background: Theme.colors.primary.transparentize(0.9); + + Text { + text: "🔗"; + font-size: 18px; + horizontal-alignment: center; + vertical-alignment: center; + } + } + + VerticalLayout { + spacing: 4px; + + Text { + text: "192.168.1.100:54321 → github.com:443"; + color: Theme.colors.foreground; + font-size: 14px; + font-weight: 500; + } + + HorizontalLayout { + spacing: 12px; + + Text { + text: "HTTPS"; + color: Theme.colors.muted-foreground; + font-size: 12px; + } + + Text { + text: "↑ 1.2 KB/s"; + color: Theme.colors.primary; + font-size: 12px; + } + + Text { + text: "↓ 45.6 KB/s"; + color: Theme.colors.secondary; + font-size: 12px; + } + } + } + } + } + } + } + } +} + +component LogsPage { + VerticalLayout { + padding: 24px; + spacing: 16px; + + Text { + text: "Logs"; + font-size: 24px; + font-weight: 700; + color: Theme.colors.foreground; + } + + Card { + ScrollView { + VerticalLayout { + spacing: 0; + + for i in 20: LogRow { + time: "10:23:45"; + level: i < 5 ? "info" : (i < 10 ? "debug" : (i < 15 ? "warn" : "error")); + message: "Sample log message " + i; + } + } + } + } + } +} + +component LogRow { + in property time: ""; + in property level: ""; + in property message: ""; + + private property level-color: level == "error" ? #ef4444 : + level == "warn" ? #eab308 : + level == "info" ? #3b82f6 : + #6b7280; + + : 24px; + + HorizontalLayout { + padding-left: 8px; + padding-right: 8px; + spacing: 8px; + + Text { + text: root.time; + color: Theme.colors.muted-foreground; + font-size: 11px; + font-family: "monospace"; + vertical-alignment: center; + width: 80px; + } + + Text { + text: root.level; + color: root.level-color; + font-size: 11px; + font-family: "monospace"; + font-weight: 700; + vertical-alignment: center; + width: 60px; + } + + Text { + text: root.message; + color: Theme.colors.foreground; + font-size: 11px; + font-family: "monospace"; + vertical-alignment: center; + } + } +} + +component SettingsPage { + VerticalLayout { + padding: 24px; + spacing: 16px; + + Text { + text: "Settings"; + font-size: 24px; + font-weight: 700; + color: Theme.colors.foreground; + } + + ScrollView { + VerticalLayout { + spacing: 16px; + + Card { + VerticalLayout { + spacing: 12px; + + Text { + text: "App Settings"; + font-size: 16px; + font-weight: 600; + color: Theme.colors.foreground; + } + + SettingRow { + label: "Auto Start"; + Switch { + checked: false; + toggled(checked) debug("Auto start:", checked); } + } + } + + SettingRow { + label: "Silent Start"; + Switch { + checked: false; + toggled(checked) => { debug("Silent start:", checked); } + } + } + } + } + + Card { + VerticalLayout { + spacing: 12px; + + Text { + t Settings"; + font-size: 16px; + font-weight: 600; + color: Theme.colors.foreground; + } + + SettingRow { + label: "Allow LAN"; + Switch { + checked: false; + toggled(checked) => { debug("Allow LAN:", checked); } + } + } + + SettingRow { + label: "IPv6"; + Switch { + checked: false; + toggled(checked) => { debug("IPv6:", checked); } + } + } + } + } + } + } + } +} diff --git a/ui/app.slint b/ui/app.slint new file mode 100644 index 0000000..2c4b6b4 --- /dev/null +++ b/ui/app.slint @@ -0,0 +1,211 @@ +import { Theme } from "./theme/theme.slint"; +import { AppSidebar, NavItem } from "./components/app-sidebar.slint"; +import { StatusState } from "./components/status-indicator.slint"; +import { HomePage } from "./pages/home.slint"; +import { ProfilesPage, ProfileData } from "./pages/profiles.slint"; +import { ProxiesPage, ProxyGroup, ProxyNode } from "./pages/proxies.slint"; +import { LogsPage, LogEntry } from "./pages/logs.slint"; +import { ConnectionsPage, Connection } from "./pages/connections.slint"; +import { SettingsPage } from "./pages/settings.slint"; + +export component App inherits Window { + title: "Clash Manager"; + preferred-width: 1200px; + preferred-height: 800px; + background: Theme.colors.background; + + // Navigation state + in-out property current-page: 0; + + // Mihomo status + in-out property mihomo-status: StatusState.stopped; + in-out property mihomo-loading: false; + + // Home page data + in-out property profile-name: "NanoCloud"; + in-out property profile-node: "Free-Japan1-Ver.7"; + in-out property profile-usage: 1.26; + in-out property profile-total: 100; + in-out property profile-expiry: "2025-11-11"; + in-out property profile-updated: "2025-10-12 10:05"; + in-out property location-ip: "47.238.198.100"; + in-out property location-region: "Japan · Tokyo"; + in-out property tun-mode-enabled: false; + in-out property system-proxy-enabled: false; + in-out property proxy-port: 7890; + + // Profiles page data + in-out property <[ProfileData]> profiles: []; + in-out property selected-profile-id: ""; + + // Proxies page data + in-out property <[ProxyGroup]> proxy-groups: []; + in-out property proxy-mode: 0; + in-out property selected-proxy: ""; + + // Logs page data + in-out property <[LogEntry]> mihomo-logs: []; + in-out property <[LogEntry]> app-logs: []; + in-out property log-tab: 0; + + // Connections page data + in-out property <[Connection]> connections: []; + + // Settings page data + in-out property auto-start: false; + in-out property silent-start: false; + in-out property clash-core: false; + in-out property allow-lan: false; + in-out property ipv6: false; + in-out property unified-delay: false; + in-out property log-level-index: 1; + in-out property app-dir: "C:/Program Files/Clash"; + in-out property config-dir: "C:/Users/User/.config/clash"; + in-out property core-dir: "C:/Program Files/Clash/core"; + in-out property app-version: "1.0.0"; + + // Callbacks + callback navigate(int); + callback toggle-mihomo(); + callback refresh-profile(); + callback toggle-tun-mode(bool); + callback toggle-system-proxy(bool); + callback open-port-settings(); + callback add-profile(); + callback refresh-all-profiles(); + callback select-profile(string); + callback edit-profile(string); + callback delete-profile(string); + callback refresh-single-profile(string); + callback proxy-mode-changed(int); + callback proxy-group-toggled(int, bool); + callback proxy-selected(string); + callback log-tab-changed(int); + callback toggle-auto-start(bool); + callback toggle-silent-start(bool); + callback toggle-clash-core(bool); + callback toggle-allow-lan(bool); + callback toggle-ipv6(bool); + callback toggle-unified-delay(bool); + callback log-level-changed(int); + callback open-tun-config(); + callback open-directory(string); + + HorizontalLayout { + // Sidebar + AppSidebar { + nav-items: [ + { title: "Home", icon: "🏠" }, + { title: "Proxies", icon: "🌐" }, + { title: "Profiles", icon: "📋" }, + { title: "Connections", icon: "🔗" }, + { title: "Logs", icon: "📝" }, + { title: "Settings", icon: "⚙" }, + ]; + current-page: root.current-page; + mihomo-status: root.mihomo-status; + mihomo-loading: root.mihomo-loading; + + navigate(index) => { + root.current-page = index; + root.navigate(index); + } + + toggle-mihomo => { + root.toggle-mihomo(); + } + } + + // Content Area + Rectangle { + background: Theme.colors.background; + + // Home Page + if root.current-page == 0: HomePage { + 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; + location-ip: root.location-ip; + location-region: root.location-region; + tun-mode-enabled: root.tun-mode-enabled; + system-proxy-enabled: root.system-proxy-enabled; + proxy-port: root.proxy-port; + + refresh-profile => { root.refresh-profile(); } + toggle-tun-mode(enabled) => { root.toggle-tun-mode(enabled); } + toggle-system-proxy(enabled) => { root.toggle-system-proxy(enabled); } + open-port-settings => { root.open-port-settings(); } + } + + // Proxies Page + if root.current-page == 1: ProxiesPage { + proxy-groups: root.proxy-groups; + current-mode: root.proxy-mode; + selected-proxy: root.selected-proxy; + + mode-changed(mode) => { root.proxy-mode-changed(mode); } + group-toggled(index, expanded) => { root.proxy-group-toggled(index, expanded); } + proxy-selected(name) => { root.proxy-selected(name); } + } + + // Profiles Page + if root.current-page == 2: ProfilesPage { + profiles: root.profiles; + selected-profile-id: root.selected-profile-id; + + adrofile => { root.add-profile(); } + refresh-all => { root.refresh-all-profiles(); } + select-profile(id) => { root.select-profile(id); } + edit-profile(id) => { root.edit-profile(id); } + delete-profile(id) => { root.delete-profile(id); } + refresh-profile(id) => { root.refresh-single-profile(id); } + } + + // Connections Page + if root.current-page == 3: ConnectionsPage { + connections: root.connections; + } + + // Logs Page + if root.current-page == 4: LogsPage { + mihomo-logs: root.mihomo-logs; + app-logs: root.app-logs; + current-tab: root.log-tab; + + tab-changed(index) => { root.log-tab-changed(index); } + } + + // Settings Page + if root.current-page == 5: SettingsPage { + auto-start: root.auto-start; + silent-start: root.silent-start; + clash-core: root.clash-core; + tun-mode: root.tun-mode-enabled; + allow-lan: root.allow-lan; + ipv6: root.ipv6; + unified-delay: root.unified-delay; + log-level-index: root.log-level-index; + proxy-port: root.proxy-port; + app-dir: root.app-dir; + config-dir: root.config-dir; + core-dir: root.core-dir; + app-version: root.app-version; + + toggle-auto-start(enabled) => { root.toggle-auto-start(enabled); } + toggle-silent-start(enabled) => { root.toggle-silent-start(enabled); } + toggle-clash-core(enabled) => { root.toggle-clash-core(enabled); } + toggle-tun-mode(enabled) => { root.toggle-tun-mode(enabled); } + toggle-allow-lan(enabled) => { root.toggle-allow-lan(enabled); } + toggle-ipv6(enabled) => { root.toggle-ipv6(enabled); } + toggle-unified-delay(enabled) => { root.toggle-unified-delay(enabled); } + log-level-changed(index) => { root.log-level-changed(index); } + open-port-settings => { root.open-port-settings(); } + open-tun-config => { root.open-tun-config(); } + open-directory(dir) => { root.open-directory(dir); } + } + } + } +} diff --git a/ui/appwindow.slint b/ui/appwindow.slint deleted file mode 100644 index 0ead565..0000000 --- a/ui/appwindow.slint +++ /dev/null @@ -1,26 +0,0 @@ -import { VerticalBox, Button } from "std-widgets.slint"; - -export component AppWindow inherits Window { - in-out property greeting: "Hello, World!"; - - width: 400px; - height: 300px; - title: "Slint Hello World"; - - VerticalBox { - alignment: center; - - Text { - text: greeting; - font-size: 24px; - horizontal-alignment: center; - } - - Button { - text: "Click Me!"; - clicked => { - greeting = "Hello from Slint!"; - } - } - } -} diff --git a/ui/components/accordion.slint b/ui/components/accordion.slint new file mode 100644 index 0000000..d360179 --- /dev/null +++ b/ui/components/accordion.slint @@ -0,0 +1,116 @@ +import { Theme, SpacingSystem, Typography, Animations } from "../theme/theme.slint"; + +export struct AccordionItemData { + title: string, + expanded: bool, +} + +export component Accordion { + in-out property <[AccordionItemData]> items: []; + in property allow-multiple: false; + + callback item-toggled(int, bool); + + VerticalLayout { + spacing: 0; + + for item[index] in root.items: AccordionItem { + title: item.title; + expanded: item.expanded; + is-last: index == root.items.length - 1; + + toggled => { + root.item-toggled(index, !item.expanded); + } + } + } +} + +component AccordionItem { + in property title: ""; + in property expanded: false; + in property is-last: false; + + callback toggled(); + + private property hovered: false; + + VerticalLayout { + spacing: 0; + + // Header + header := Rectangle { + height: 48px; + background: root.hovered ? Theme.colors.muted : transparent; + + animate background { duration: Animations.durations.fast; } + + HorizontalLayout { + padding-left: SpacingSystem.spacing.s4; + padding-right: SpacingSystem.spacing.s4; + spacing: SpacingSystem.spacing.s4; + alignment: space-between; + + Text { + text: root.title; + color: Theme.colors.foreground; + font-size: Typography.sizes.sm; + font-weight: Typography.weights.medium; + vertical-alignment: center; + } + + chevron := Text { + text: "›"; + color: Theme.colors.muted-foreground; + font-size: Typography.sizes.lg; + font-weight: Typography.weights.bold; + vertical-alignment: center; + width: 20px; + horizontal-alignment: center; + + rotation-angle: root.expanded ? 90deg : 0deg; + rotation-origin-x: self.width / 2; + rotation-origin-y: self.height / 2; + + animate rotation-angle { duration: Animations.durations.normal; easing: Animations.ease-out; } + } + } + + touch := TouchArea { + clicked => { + root.toggled(); + } + + moved => { + root.hovered = self.has-hover; + } + } + } + + // Content + if root.expanded: content-wrapper := Rectangle { + background: transparent; + + VerticalLayout { + padding: SpacingSystem.spacing.s4; + padding-top: 0; + padding-bottom: SpacingSystem.spacing.s4; + + @children + } + } + + // Border + if !root.is-last: Rectangle { + height: 1px; + background: Theme.colors.border; + } + } +} + +// Wrapper for accordion content +export component AccordionContent { + VerticalLayout { + @children + } +} diff --git a/ui/components/app-sidebar.slint b/ui/components/app-sidebar.slint new file mode 100644 index 0000000..c29ffbf --- /dev/null +++ b/ui/components/app-sidebar.slint @@ -0,0 +1,238 @@ +import { Theme, SpacingSystem, Typography, Animations } from "../theme/theme.slint"; +import { Button } from "./button.slint"; +import { StatusIndicator, StatusState } from "./status-indicator.slint"; + +export struct NavItem { + title: string, + icon: string, +} + +export component AppSidebar { + in property <[NavItem]> nav-items: []; + in-out property current-page: 0; + in property mihomo-status: StatusState.stopped; + in property mihomo-loading: false; + + callback navigate(int); + callback toggle-mihomo(); + + width: 240px; + + Rectangle { + background: Theme.colors.background; + border-width: 1px; + border-color: Theme.colors.border; + + VerticalLayout { + padding: SpacingSystem.spacing.s4; + spacing: SpacingSystem.spacing.s4; + + // Header + HorizontalLayout { + spacing: SpacingSystem.spacing.s3; + + Rectangle { + width: 32px; + height: 32px; + border-radius: SpacingSystem.radius.md; + background: Theme.colors.primary; + + Text { + text: "C"; + color: Theme.colors.primary-foreground; + font-size: Typography.sizes.lg; + font-weight: Typography.weights.bold; + horizontal-alignment: center; + vertical-alignment: center; + } + } + + VerticalLayout { + spacing: 2px; + + Text { + text: "Clash Manager"; + color: Theme.colors.foreground; + font-size: Typography.sizes.sm; + font-weight: Typography.weights.semibold; + } + + Text { + text: "v1.0.0"; + color: Theme.colors.muted-foreground; + font-size: Typography.sizes.xs; + } + } + } + + // Navigation + VerticalLayout { + spacing: SpacingSystem.spacing.s1; + + Text { + text: "NAVIGATION"; + color: Theme.colors.muted-foreground; + font-size: 10px; + font-weight: Typography.weights.semibold; + } + + for item[index] in root.nav-items: NavButton { + icon: item.icon; + title: item.title; + active: index == root.current-page; + + clicked => { + root.navigate(index); + } + } + } + + Rectangle { + vertical-stretch: 1; + } + + // Mihomo Status Footer + MihomoStatusBar { + status: root.mihomo-status; + loading: root.mihomo-loading; + + toggle-clicked => { + root.toggle-mihomo(); + } + } + } + } +} + +export component NavButton { + in property icon: ""; + in property title: ""; + in property active: false; + + callback clicked(); + + private property hovered: false; + + height: 40px; + + states [ + active when root.active: { + container.background: Theme.colors.primary.transparentize(0.9); + icon-text.color: Theme.colors.primary; + title-text.color: Theme.colors.foreground; + } + hovered when root.hovered && !root.active: { + container.background: Theme.colors.muted.transparentize(0.5); + } + ] + + container := Rectangle { + background: transparent; + border-radius: SpacingSystem.radius.md; + + animate background { duration: Animations.durations.fast; } + + HorizontalLayout { + padding-left: SpacingSystem.spacing.s3; + padding-right: SpacingSystem.spacing.s3; + spacing: SpacingSystem.spacing.s3; + + icon-text := Text { + text: root.icon; + color: Theme.colors.muted-foreground; + font-size: Typography.sizes.lg; + vertical-alignment: center; + width: 20px; + + animate color { duration: Animations.durations.fast; } + } + + title-text := Text { + text: root.title; + color: Theme.colors.muted-foreground; + font-size: Typography.sizes.sm; + font-weight: Typography.weights.medium; + vertical-alignment: center; + + animate color { duration: Animations.durations.fast; } + } + } + + touch := TouchArea { + clicked => { + root.clicked(); + } + + moved => { + root.hovered = self.has-hover; + } + } + } +} + +export component MihomoStatusBar { + in property status: StatusState.stopped; + in property loading: false; + + callback toggle-clicked(); + + private property status-text: status == StatusState.running ? "Running" : + status == StatusState.starting ? "Starting..." : + "Stopped"; + + private property status-color: status == StatusState.running ? #22c55e : + status == StatusState.starting ? #eab308 : + #9ca3af; + + Rectangle { + background: status == StatusState.starting ? #eab30820 : + status == StatusState.running ? #22c55e20 : + transparent; + border-radius: SpacingSystem.radius.md; + + animate background { duration: Animations.durations.normal; } + + HorizontalLayout { + padding: SpacingSystem.spacing.s3; + spacing: SpacingSystem.spacing.s3; + alignment: space-between; + + HorizontalLayout { + spacing: SpacingSystem.spacing.s3; + + StatusIndicator { + state: root.status; + } + + VerticalLayout { + spacing: 2px; + + Text { + text: "Clash"; + color: root.status-color; + font-size: Typography.sizes.xs; + font-weight: Typography.weights.semibold; + } + + Text { + text: root.status-text; + color: Theme.colors.muted-foreground; + font-size: 10px; + } + } + } + + Button { + width: 32px; + height: 32px; + text: root.loading ? "..." : (status == StatusState.running ? "||" : ">"); + variant: "ghost"; + disabled: root.loading; + + clicked => { + root.toggle-clicked(); + } + } + } + } +} diff --git a/ui/components/label.slint b/ui/components/label.slint new file mode 100644 index 0000000..d061e01 --- /dev/null +++ b/ui/components/label.slint @@ -0,0 +1,44 @@ +import { Theme, Typography } from "../theme/theme.slint"; + +export enum LabelSize { + xs, + sm, + base, + lg, + xl, +} + +export enum LabelColor { + foreground, + muted, + primary, + secondary, + destructive, +} + +export component Label { + in property text: ""; + in property size: LabelSize.base; + in property color-variant: LabelColor.foreground; + in property font-weight: Typography.weights.normal; + + private property font-size: size == LabelSize.xs ? Typography.sizes.xs : + size == LabelSize.sm ? Typography.sizes.sm : + size == LabelSize.lg ? Typography.sizes.lg : + size == LabelSize.xl ? Typography.sizes.xl : + Typography.sizes.base; + + private property text-color: color-variant == LabelColor.muted ? Theme.colors.muted-foreground : + color-variant == LabelColor.primary ? Theme.colors.primary : + color-variant == LabelColor.secondary ? Theme.colors.secondary : + color-variant == LabelColor.destructive ? Theme.colors.destructive : + Theme.colors.foreground; + + Text { + text: root.text; + color: root.text-color; + font-size: root.font-size; + font-weight: root.font-weight; + vertical-alignment: center; + } +} diff --git a/ui/components/progress.slint b/ui/components/progress.slint new file mode 100644 index 0000000..bca53ae --- /dev/null +++ b/ui/components/progress.slint @@ -0,0 +1,36 @@ +import { Theme, SpacingSystem, Typography, Animations } from "../theme/theme.slint"; + +export component Progress { + in property value: 0; // 0-100 + in property show-label: false; + + min-height: 8px; + + VerticalLayout { + spacing: SpacingSystem.spacing.s1; + + // Progress bar + track := Rectangle { + height: 8px; + background: Theme.colors.primary.transparentize(0.8); + border-radius: SpacingSystem.radius.full; + + indicator := Rectangle { + width: parent.width * Math.max(0, Math.min(100, root.value)) / 100; + height: parent.height; + background: Theme.colors.primary; + border-radius: SpacingSystem.radius.full; + + animate width { duration: Animations.durations.normal; easing: Animations.ease-out; } + } + } + + // Optional label + if root.show-label: label := Text { + text: Math.round(root.value) + "%"; + color: Theme.colors.muted-foreground; + font-size: Typography.sizes.xs; + horizontal-alignment: right; + } + } +} diff --git a/ui/components/select.slint b/ui/components/select.slint new file mode 100644 index 0000000..5690d70 --- /dev/null +++ b/ui/components/select.slint @@ -0,0 +1,174 @@ +import { Theme, SpacingSystem, Typography, Animations } from "../theme/theme.slint"; + +export struct SelectOption { + label: string, + value: string, +} + +export component Select { + in property <[SelectOption]> options: []; + in-out property selected-index: -1; + in property placeholder: "Select..."; + in property enabled: true; + + callback selected(int, string); + + private property open: false; + private property hovered: false; + + min-width: 120px; + min-height: 36px; + + states [ + disabled when !root.enabled: { + trigger.opacity: 0.5; + } + ] + + VerticalLayout { + // Trigger button + trigger := Rectangle { + height: 36px; + background: Theme.colors.background; + border-radius: SpacingSystem.radius.md; + border-width: 1px; + border-color: Theme.colors.input; + + drop-shadow-blur: 1px; + drop-shadow-color: #00000010; + drop-shadow-offset-y: 1px; + + HorizontalLayout { + padding-left: SpacingSystem.spacing.s3; + padding-right: SpacingSystem.spacing.s3; + spacing: SpacingSystem.spacing.s2; + alignment: space-between; + + value-text := Text { + text: root.selected-index >= 0 && root.selected-index < root.options.length + ? root.options[root.selected-index].label + : root.placeholder; + color: root.selected-index >= 0 + ? Theme.colors.foreground + : Theme.colors.muted-foreground; + font-size: Typography.sizes.sm; + vertical-alignment: center; + } + + chevron := Text { + text: "›"; + color: Theme.colors.muted-foreground; + font-size: Typography.sizes.base; + font-weight: Typography.weights.bold; + vertical-alignment: center; + width: 16px; + horizontal-alignment: center; + + rotation-angle: root.open ? -90deg : 90deg; + rotation-origin-x: self.width / 2; + rotation-origin-y: self.height / 2; + + animate rotation-angle { duration: Animations.durations.fast; easing: Animations.ease-out; } + } + } + + touch := TouchArea { + enabled: root.enabled; + + clicked => { + root.open = !root.open; + } + + moved => { + root.hovered = self.has-hover; + } + } + } + + // Dropdown menu + if root.open: dropdown := Rectangle { + y: trigger.height + 4px; + background: Theme.colors.background; + border-radius: SpacingSystem.radius.md; + border-width: 1px; + border-color: Theme.colors.border; + + drop-shadow-blur: 8px; + drop-shadow-color: #00000020; + drop-shadow-offset-y: 4px; + + VerticalLayout { + padding: SpacingSystem.spacing.s1; + + for option[index] in root.options: SelectItem { + label: option.label; + selected: index == root.selected-index; + + clicked => { + root.selected-index = index; + root.selected(index, option.value); + root.open = false; + } + } + } + } + } +} + +component SelectItem { + in property label: ""; + in property selected: false; + + callback clicked(); + + private property hovered: false; + + height: 32px; + + states [ + selected when root.selected: { + container.background: Theme.colors.accent; + } + hovered when root.hovered && !root.selected: { + container.background: Theme.colors.accent.transparentize(0.5); + } + ] + + container := Rectanglen background: transparent; + border-radius: SpacingSystem.radius.sm; + + animate background { duration: Animations.durations.fast; } + + HorizontalLayout { + padding-left: SpacingSystem.spacing.s2; + padding-right: SpacingSystem.spacing.s2; + spacing: SpacingSystem.spacing.s2; + + if root.selected: checkmark := Text { + text: "✓"; + color: Theme.colors.primary; + font-size: Typography.sizes.sm; + font-weight: Typography.weights.bold; + vertical-alignment: center; + width: 16px; + } + + Text { + text: root.label; + color: Theme.colors.foreground; + font-size: Typography.sizes.sm; + vertical-alignment: center; + } + } + + touch := TouchArea { + clicked => { + root.clicked(); + } + + moved => { + root.hovered = self.has-hover; + } + } + } +} diff --git a/ui/components/separator.slint b/ui/components/separator.slint new file mode 100644 index 0000000..5836d46 --- /dev/null +++ b/ui/components/separator.slint @@ -0,0 +1,20 @@ +import { Theme } from "../theme/theme.slint"; + +export enum Orientation { + horizontal, + vertical, +} + +export component Separator { + in property orientation: Orientation.horizontal; + + if orientation == Orientation.horizontal: Rectangle { + height: 1px; + background: Theme.colors.border; + } + + if orientation == Orientation.vertical: Rectangle { + width: 1px; + background: Theme.colors.border; + } +} diff --git a/ui/components/status-indicator.slint b/ui/components/status-indicator.slint new file mode 100644 index 0000000..e1682fe --- /dev/null +++ b/ui/components/status-indicator.slint @@ -0,0 +1,57 @@ +import { Theme, Animations } from "../theme/theme.slint"; + +export enum StatusState { + stopped, + starting, + running, +} + +export component StatusIndicator { + in property state: StatusState.stopped; + + private property dot-color: state == StatusState.running ? #22c55e : + state == StatusState.starting ? #eab308 : + #9ca3af; + + private property should-animate: state == StatusState.running || state == StatusState.starting; + + width: 10px; + height: 10px; + + // Breathing animation ring + if root.should-animate: ping := Rectangle { + width: parent.width; + height: parent.height; + border-radius: 5px; + background: root.dot-color.transparentize(0.25); + + // Animate scale and opacity for breathing effect + states [ + pulse: { + ping.width: parent.width * 2; + ping.height: parent.height * 2; + ping.x: -parent.width / 2; + ping.y: -parent.height / 2; + ping.opacity: 0; + + in { + animate width, height, x, y, opacity { + duration: 1500ms; + easing: cubic-bezier(0, 0, 0.2, 1); + iteration-count: -1; + } + } + } + ] + } + + // Main dot + dot := Rectangle { + width: parent.width; + height: parent.height; + border-radius: 5px; + background: root.dot-color; + + animate background { duration: Animations.durations.normal; } + } +} diff --git a/ui/components/switch.slint b/ui/components/switch.slint new file mode 100644 index 0000000..37886fe --- /dev/null +++ b/ui/components/switch.slint @@ -0,0 +1,77 @@ +import { Theme, SpacingSystem, Animations } from "../theme/theme.slint"; + +export component Switch { + in property checked: false; + in property enabled: true; + + callback toggled(bool); + + private property pressed: false; + private property hovered: false; + + min-width: 32px; + min-height: 18px; + + states [ + disabled when !root.enabled: { + track.opacity: 0.5; + thumb.opacity: 0.5; + } + checked when root.checked: { + track.background: Theme.colors.primary; + thumb.x: root.width - thumb.width - 2px; + } + unchecked when !root.checked: { + track.background: Theme.colors.input; + thumb.x: 2px; + } + ] + + track := Rectangle { + width: 32px; + height: 18px; + border-radius: 9px; + background: Theme.colors.input; + border-width: 1px; + border-color: transparent; + + animate background { duration: Animations.durations.fast; easing: Animations.ease-out; } + + thumb := Rectangle { + width: 14px; + height: 14px; + y: (parent.height - self.height) / 2; + x: 2px; + border-radius: 7px; + background: Theme.colors.background; + + drop-shadow-blur: 2px; + drop-shadow-color: #00000040; + drop-shadow-offset-y: 1px; + + animate x { duration: Animations.durations.fast; easing: Animations.ease-out; } + } + } + + touch := TouchArea { + enabled: root.enabled; + + clicked => { + if (root.enabled) { + root.toggled(!root.checked); + } + } + + pointer-event(event) => { + if (event.kind == PointerEventKind.down) { + root.pressed = true; + } else if (event.kind == PointerEventKind.up || event.kind == PointerEventKind.cancel) { + root.pressed = false; + } + } + + moved => { + root.hovered = self.has-hover; + } + } +} diff --git a/ui/components/tabs.slint b/ui/components/tabs.slint new file mode 100644 index 0000000..603e750 --- /dev/null +++ b/ui/components/tabs.slint @@ -0,0 +1,125 @@ +import { Theme, SpacingSystem, Typography, Animations } from "../theme/theme.slint"; + +export struct TabItem { + title: string, +} + +export component Tabs { + in property <[TabItem]> tabs: []; + in-out property current-index: 0; + + callback tab-changed(int); + + min-height: 200px; + + VerticalLayout { + spacing: SpacingSystem.spacing.s2; + + // Tab List + tab-list := HorizontalLayout { + height: 36px; + spacing: 0; + + Rectangle { + background: Theme.colors.muted; + border-radius: SpacingSystem.radius.lg; + padding: 3px; + + HorizontalLayout { + spacing: 0; + padding: 0; + + for tab[index] in root.tabs: TabTrigger { + text: tab.title; + active: index == root.current-index; + clicked => { + root.current-index = index; + root.tab-changed(index); + } + } + } + } + } + + // Tab Content Area + content-area := Rectangle { + // Content will be provided by @children in parent component + @children + } + } +} + +component TabTrigger { + in property text: ""; + in property active: false; + + callback clicked(); + + private property hovered: false; + + min-width: 60px; + height: 30px; + + states [ + active when root.active: { + container.background: Theme.colors.background; + label.color: Theme.colors.foreground; + container.border-color: Theme.colors.input; + } + inactive when !root.active: { + container.background: transparent; + label.color: Theme.colors.muted-foreground; + container.border-color: transparent; + } + hovered when root.hovered && !root.active: { + label.color: Theme.colors.foreground; + } + ] + + container := Rectangle { + border-radius: SpacingSystem.radius.md; + background: transparent; + border-width: 1px; + border-color: transparent; + + drop-shadow-blur: root.active ? 2px : 0; + drop-shadow-color: root.active ? #00000010 : transparent; + drop-shadow-offset-y: root.active ? 1px : 0; + + animate background { duration: Animations.durations.fast; } + animate border-color { duration: Animations.durations.fast; } + + label := Text { + text: root.text; + color: Theme.colors.muted-foreground; + font-size: Typography.sizes.sm; + font-weight: Typography.weights.medium; + horizontal-alignment: center; + vertical-alignment: center; + + animate color { duration: Animations.durations.fast; } + } + } + + touch := TouchArea { + clicked => { + root.clicked(); + } + + moved => { + root.hovered = self.has-hover; + } + } +} + +// TabContent component for wrapping content +export component TabContent { + in property index: 0; + in property current-index: 0; + + visible: root.index == root.current-index; + + VerticalLayout { + @children + } +} diff --git a/ui/components/tooltip.slint b/ui/components/tooltip.slint new file mode 100644 index 0000000..01b1a0d --- /dev/null +++ b/ui/components/tooltip.slint @@ -0,0 +1,42 @@ +import { Theme, SpacingSystem, Typography, Animations } from "../theme/theme.slint"; + +export component Tooltip { + in property text: ""; + in property show: false; + + if root.show: popup := Rectangle { + y: -self.height - 8px; + x: (parent.width - self.width) / 2; + width: tooltip-text.width + SpacingSystem.spacing.s3 * 2; + height: tooltip-text.height + SpacingSystem.spacing.s2 * 2; + background: Theme.colors.foreground; + border-radius: SpacingSystem.radius.sm; + + drop-shadow-blur: 8px; + drop-shadow-color: #00000030; + drop-shadow-offset-y: 2px; + + opacity: root.show ? 1 : 0; + animate opacity { duration: Animations.durations.fast; } + + tooltip-text := Text { + text: root.text; + color: Theme.colors.background; + font-size: Typography.sizes.xs; + horizontal-alignment: center; + vertical-alignment: center; + x: SpacingSystem.spacing.s3; + y: SpacingSystem.spacing.s2; + } + + // Arrow + arrow := Path { + y: parent.height; + x: (parent.width - 8px) / 2; + width: 8px; + height: 4px; + fill: Theme.colors.foreground; + commands: "M 0 0 L 4 4 L 8 0 Z"; + } + } +} diff --git a/ui/pages-complete-backup.slint b/ui/pages-complete-backup.slint new file mode 100644 index 0000000..f2d3fc5 --- /dev/null +++ b/ui/pages-complete-backup.slint @@ -0,0 +1,967 @@ +import { Theme, SpacingSystem, Typography } from "./theme/theme.slint"; +import { Button } from "./components/button.slint"; +import { Card } from "./components/card.slint"; +import { Switch } from "./components/switch.slint"; +import { Badge } from "./components/badge.slint"; +import { Progress } from "./components/progress.slint"; +import { ScrollView } from "std-widgets.slint"; + +// Home Page - Redesigned with better UI +export component HomePage { + VerticalLayout { + padding: 24px; + spacing: 20px; + + Text { + text: "Home"; + font-size: 28px; + font-weight: 700; + color: Theme.colors.foreground; + } + + ScrollView { + VerticalLayout { + spacing: 20px; + + // Profile Card + Card { + VerticalLayout { + spacing: 16px; + + HorizontalLayout { + spacing: 16px; + + Rectangle { + width: 56px; + height: 56px; + border-radius: 12px; + background: Theme.colors.primary.transparentize(0.9); + + Text { + text: "☁"; + color: Theme.colors.primary; + font-size: 28px; + horizontal-alignment: center; + vertical-alignment: center; + } + } + + VerticalLayout { + spacing: 6px; + + HorizontalLayout { + spacing: 10px; + + Text { + text: "NanoCloud"; + color: Theme.colors.foreground; + font-size: 20px; + font-weight: 700; + vertical-alignment: center; + } + } + + HorizontalLayout { + spacing: 8px; + + Text { + text: "🌐 Free-Japan1-Ver.7"; + color: Theme.colors.muted-foreground; + font-size: 13px; + vertical-alignment: center; + } + + Badge { + text: "Vmess"; + variant: "outline"; + } + + Badge { + text: "UDP"; + variant: "secondary"; + } + } + } + + Rectangle { + horizontal-stretch: 1; + } + + Button { + text: "↻"; + variant: "ghost"; + width: 40px; + height: 40px; + } + } + + Rectangle { + height: 1px; + background: Theme.colors.border; + } + + GridLayout { + spacing: 20px; + Row { + VerticalLayout { + spacing: 6px; + + Text { + text: "Usage / Total"; + color: Theme.colors.muted-foreground; + font-size: 12px; + font-weight: 600; + } + + Text { + text: "1.26GB / 100GB"; + color: Theme.colors.foreground; + font-size: 16px; + font-weight: 600; + } + + Progress { + value: 1.26; + } + } + + VerticalLayout { + spacing: 6px; + + Text { + text: "Expiry Date"; + color: Theme.colors.muted-foreground; + font-size: 12px; + font-weight: 600; + } + + Text { + text: "2025-11-11"; + color: Theme.colors.foreground; + font-size: 16px; + font-weight: 600; + } + } + + VerticalLayout { + spacing: 6px; + + Text { + text: "Last Update"; + color: Theme.colors.muted-foreground; + font-size: 12px; + font-weight: 600; + } + + Text { + text: "2025-10-12"; + color: Theme.colors.foreground; + font-size: 16px; + font-weight: 600; + } + } + } + } + } + } + + // Location Card + Card { + VerticalLayout { + spacing: 16px; + + Text { + text: "Location Info"; + font-size: 16px; + font-weight: 600; + color: Theme.colors.foreground; + } + + HorizontalLayout { + spacing: 32px; + + VerticalLayout { + spacing: 6px; + + Text { + text: "IP Address"; + color: Theme.colors.muted-foreground; + font-size: 12px; + font-weight: 600; + } + + Text { + text: "47.238.198.100"; + color: Theme.colors.foreground; + font-size: 14px; + font-weight: 500; + } + } + + VerticalLayout { + spacing: 6px; + + Text { + text: "Location"; + color: Theme.colors.muted-foreground; + font-size: 12px; + font-weight: 600; + } + + Text { + text: "Japan · Tokyo"; + color: Theme.colors.foreground; + font-size: 14px; + font-weight: 500; + } + } + } + } + } + + // Quick Settings Card + Card { + VerticalLayout { + spacing: 0; + + Text { + text: "Quick Settings"; + font-size: 16px; + font-weight: 600; + color: Theme.colors.foreground; + padding-bottom: 12px; + } + + SettingRow { + label: "TUN Mode"; + Switch { + checked: false; + } + } + + Rectangle { + height: 1px; + background: Theme.colors.border; + } + + SettingRow { + label: "System Proxy"; + Switch { + checked: false; + } + } + + Rectangle { + height: 1px; + background: Theme.colors.border; + } + + SettingRow { + label: "Proxy Port"; + Text { + text: "7890"; + color: Theme.colors.primary; + font-size: 14px; + font-weight: 600; + vertical-alignment: center; + } + } + } + } + } + } + } +} + +component SettingRow { + in property label: ""; + + height: 52px; + + HorizontalLayout { + spacing: 12px; + alignment: space-between; + + Text { + text: root.label; + color: Theme.colors.foreground; + font-size: 14px; + font-weight: 500; + vertical-alignment: center; + } + + @children + } +} + +// Profiles Page +export component ProfilesPage { + VerticalLayout { + padding: 24px; + spacing: 20px; + + HorizontalLayout { + spacing: 12px; + alignment: space-between; + + Text { + text: "Profiles"; + font-size: 28px; + font-weight: 700; + color: Theme.colors.foreground; + vertical-alignment: center; + } + + HorizontalLayout { + spacing: 8px; + + Button { + text: "+ Add"; + variant: "default"; + } + + Button { + text: "↻"; + variant: "ghost"; + } + } + } + + ScrollView { + GridLayout { + spacing: 16px; + Row { + ProfileCard { + name: "NanoCloud"; + type-icon: "☁"; + description: "Free tier subscription - Japan servers"; + is-remote: true; + usage: 1.26; + total: 100.0; + updated: "Just now"; + } + + ProfileCard { + name: "Local Config"; + type-icon: "📄"; + description: "Custom local configuration file"; + is-remote: false; + updated: "2025-01-15"; + } + + ProfileCard { + name: "Premium VPN"; + type-icon: "☁"; + description: "Premium subscription with global servers"; + is-remote: true; + usage: 45.8; + total: 500.0; + updated: "2 hours ago"; + } + } + } + } + } +} + +component ProfileCard { + in property name: ""; + in property type-icon: ""; + in property description: ""; + in property is-remote: false; + in property usage: 0; + in property total: 100; + in property updated: ""; + + min-width: 300px; + height: 140px; + + Card { + VerticalLayout { + spacing: 10px; + + HorizontalLayout { + spacing: 10px; + + Text { + text: root.type-icon; + font-size: 20px; + vertical-alignment: center; + } + + Text { + text: root.name; + color: Theme.colors.foreground; + font-size: 16px; + font-weight: 600; + vertical-alignment: center; + } + + Rectangle { + horizontal-stretch: 1; + } + + if root.is-remote: Button { + text: "↻"; + variant: "ghost"; + width: 28px; + height: 28px; + } + } + + Text { + text: root.description; + color: Theme.colors.muted-foreground; + font-size: 13px; + overflow: elide; + } + + if root.is-remote: VerticalLayout { + spacing: 6px; + + Progress { + value: (root.usage / root.total) * 100; + } + + HorizontalLayout { + alignment: space-between; + + Text { + text: "Updated: " + root.updated; + color: Theme.colors.muted-foreground; + font-size: 11px; + } + + Text { + text: root.usage + " / " + root.total + " GB"; + color: Theme.colors.foreground; + font-size: 11px; + font-weight: 600; + } + } + } + + if !root.is-remote: Text { + text: "Local Profile"; + color: Theme.colors.muted-foreground; + font-size: 11px; + } + } + } +} + +// Logs Page +export component LogsPage { + VerticalLayout { + padding: 24px; + spacing: 20px; + + Text { + text: "Logs"; + font-size: 28px; + font-weight: 700; + color: Theme.colors.foreground; + } + + Card { + height: 500px; + + ScrollView { + VerticalLayout { + spacing: 0; + + LogRow { + time: "10:23:45"; + level: "info"; + message: "Mihomo core started successfully"; + } + + LogRow { + time: "10:23:46"; + level: "info"; + message: "HTTP proxy listening on 127.0.0.1:7890"; + } + + LogRow { + time: "10:23:47"; + level: "debug"; + message: "Loading configuration from config.yaml"; + } + + LogRow { + time: "10:23:48"; + level: "info"; + message: "Profile loaded: NanoCloud"; + } + + LogRow { + time: "10:23:50"; + level: "warn"; + message: "DNS resolution timeout for example.com"; + } + + LogRow { + time: "10:23:51"; + level: "error"; + message: "Failed to connect to proxy server"; + } + + LogRow { + time: "10:23:52"; + level: "info"; + message: "Retrying connection..."; + } + + LogRow { + time: "10:23:53"; + level: "info"; + message: "Connection established successfully"; + } + } + } + } + } +} + +component LogRow { + in property time: ""; + in property level: ""; + in property message: ""; + + private property level-color: level == "error" ? #ef4444 : + level == "warn" ? #eab308 : + level == "info" ? #3b82f6 : + #6b7280; + + height: 28px; + + HorizontalLayout { + padding-left: 12px; + padding-right: 12px; + spacing: 12px; + + Text { + text: root.time; + color: Theme.colors.muted-foreground; + font-size: 12px; + font-family: "monospace"; + vertical-alignment: center; + width: 80px; + } + + Text { + text: root.level; + color: root.level-color; + font-size: 12px; + font-family: "monospace"; + font-weight: 700; + vertical-alignment: center; + width: 70px; + } + + Text { + text: root.message; + color: Theme.colors.foreground; + font-size: 12px; + font-family: "monospace"; + vertical-alignment: center; + } + } +} + +// Connections Page +export component ConnectionsPage { + VerticalLayout { + padding: 24px; + spacing: 20px; + + Text { + text: "Connections"; + font-size: 28px; + font-weight: 700; + color: Theme.colors.foreground; + } + + ScrollView { + VerticalLayout { + spacing: 12px; + + ConnectionItem { + source: "192.168.1.100:54321"; + destination: "github.com:443"; + protocol: "HTTPS"; + upload: "1.2 KB/s"; + download: "45.6 KB/s"; + duration: "00:02:15"; + } + + ConnectionItem { + source: "192.168.1.100:54322"; + destination: "google.com:443"; + protocol: "HTTPS"; + upload: "0.5 KB/s"; + download: "12.3 KB/s"; + duration: "00:01:30"; + } + + ConnectionItem { + source: "192.168.1.100:54323"; + destination: "youtube.com:443"; + protocol: "HTTPS"; + upload: "2.1 KB/s"; + download: "256.7 KB/s"; + duration: "00:05:42"; + } + } + } + } +} + +component ConnectionItem { + in property source: ""; + in property destination: ""; + in property protocol: ""; + in property upload: ""; + in property download: ""; + in property duration: ""; + + height: 72px; + + Card { + HorizontalLayout { + spacing: 16px; + + Rectangle { + width: 48px; + height: 48px; + border-radius: 10px; + background: Theme.colors.primary.transparentize(0.9); + + Text { + text: "🔗"; + font-size: 20px; + horizontal-alignment: center; + vertical-alignment: center; + } + } + + VerticalLayout { + spacing: 6px; + + Text { + text: root.source + " → " + root.destination; + color: Theme.colors.foreground; + font-size: 14px; + font-weight: 500; + overflow: elide; + } + + HorizontalLayout { + spacing: 16px; + + Text { + text: root.protocol; + color: Theme.colors.muted-foreground; + font-size: 12px; + } + + Text { + text: "↑ " + root.upload; + color: Theme.colors.primary; + font-size: 12px; + font-weight: 600; + } + + Text { + text: "↓ " + root.download; + color: #22c55e; + font-size: 12px; + font-weight: 600; + } + + Text { + text: root.duration; + color: Theme.colors.muted-foreground; + font-size: 12px; + } + } + } + } + } +} + +// Settings Page +export component SettingsPage { + VerticalLayout { + padding: 24px; + spacing: 20px; + + Text { + text: "Settings"; + font-size: 28px; + font-weight: 700; + color: Theme.colors.foreground; + } + + ScrollView { + VerticalLayout { + spacing: 16px; + + Card { + VerticalLayout { + spacing: 0; + + Text { + text: "App Settings"; + font-size: 16px; + font-weight: 600; + color: Theme.colors.foreground; + padding-bottom: 12px; + } + + SettingRow { + label: "Auto Start"; + Switch { + checked: false; + } + } + + Rectangle { + height: 1px; + background: Theme.colors.border; + } + + SettingRow { + label: "Silent Start"; + Switch { + checked: false; + } + } + + Rectangle { + height: 1px; + background: Theme.colors.border; + } + + SettingRow { + label: "Clash Core"; + Switch { + checked: true; + } + } + } + } + + Card { + VerticalLayout { + spacing: 0; + + Text { + text: "Clash Settings"; + font-size: 16px; + font-weight: 600; + color: Theme.colors.foreground; + padding-bottom: 12px; + } + + SettingRow { + label: "Allow LAN"; + Switch { + checked: false; + } + } + + Rectangle { + height: 1px; + background: Theme.colors.border; + } + + SettingRow { + label: "IPv6"; + Switch { + checked: false; + } + } + + Rectangle { + height: 1px; + background: Theme.colors.border; + } + + SettingRow { + label: "Unified Delay"; + Switch { + checked: true; + } + } + } + } + + Card { + VerticalLayout { + spacing: 12px; + + Text { + text: "About"; + font-size: 16px; + font-weight: 600; + color: Theme.colors.foreground; + } + + HorizontalLayout { + spacing: 12px; + alignment: space-between; + + Text { + text: "App Version"; + color: Theme.colors.muted-foreground; + font-size: 14px; + } + + Text { + text: "1.0.0"; + color: Theme.colors.foreground; + font-size: 14px; + font-weight: 600; + } + } + } + } + } + } + } +} + +// Proxies Page +export component ProxiesPage { + VerticalLayout { + padding: 24px; + spacing: 20px; + + Text { + text: "Proxies"; + font-size: 28px; + font-weight: 700; + color: Theme.colors.foreground; + } + + ScrollView { + VerticalLayout { + spacing: 16px; + + Card { + VerticalLayout { + spacing: 12px; + + Text { + text: "Hong Kong"; + font-size: 16px; + font-weight: 600; + color: Theme.colors.foreground; + } + + GridLayout { + spacing: 12px; + Row { + ProxyNode { + name: "HK-01"; + type-text: "Vmess"; + latency: "45ms"; + } + + ProxyNode { + name: "HK-02"; + type-text: "Vmess"; + latency: "52ms"; + } + + ProxyNode { + name: "HK-03"; + type-text: "Trojan"; + latency: "38ms"; + } + } + } + } + } + + Card { + VerticalLayout { + spacing: 12px; + + Text { + text: "Japan"; + font-size: 16px; + font-weight: 600; + color: Theme.colors.foreground; + } + + GridLayout { + spacing: 12px; + Row { + ProxyNode { + name: "JP-Tokyo-01"; + type-text: "Vmess"; + latency: "122ms"; + } + + ProxyNode { + name: "JP-Tokyo-02"; + type-text: "SS"; + latency: "115ms"; + } + + ProxyNode { + name: "JP-Osaka-01"; + type-text: "Vmess"; + latency: "135ms"; + } + } + } + } + } + } + } + } +} + +component ProxyNode { + in property name: ""; + in property type-text: ""; + in property latency: ""; + + min-width: 200px; + height: 70px; + + Card { + HorizontalLayout { + spacing: 12px; + alignment: space-between; + + VerticalLayout { + spacing: 6px; + + Text { + text: root.name; + color: Theme.colors.foreground; + font-size: 14px; + font-weight: 600; + } + + Text { + text: root.type-text; + color: Theme.colors.muted-foreground; + font-size: 12px; + } + } + + Text { + text: root.latency; + color: Theme.colors.primary; + font-size: 14px; + font-weight: 600; + vertical-alignment: center; + } + } + } +} diff --git a/ui/pages-complete.slint b/ui/pages-complete.slint new file mode 100644 index 0000000..cb6e1ab --- /dev/null +++ b/ui/pages-complete.slint @@ -0,0 +1,799 @@ +import { Theme, SpacingSystem, Typography } from "./theme/theme.slint"; +import { Button } from "./components/button.slint"; +import { Card } from "./components/card.slint"; +import { Switch } from "./components/switch.slint"; +import { Badge } from "./components/badge.slint"; +import { Progress } from "./components/progress.slint"; +import { ScrollView } from "std-widgets.slint"; + +// Helper components - must be defined before they are used + +component SettingRow { + in property label: ""; + height: 52px; + HorizontalLayout { + spacing: 12px; + alignment: space-between; + Text { + text: root.label; + color: Theme.colors.foreground; + font-size: 14px; + font-weight: 500; + vertical-alignment: center; + } + @children + } +} + +component ProfileCard { + in property name: ""; + in property type-icon: ""; + in property description: ""; + in property is-remote: false; + in property usage: 0; + in property total: 100; + in property updated: ""; + min-width: 300px; + height: 140px; + Card { + VerticalLayout { + spacing: 10px; + HorizontalLayout { + spacing: 10px; + Text { + text: root.type-icon; + font-size: 20px; + vertical-alignment: center; + } + Text { + text: root.name; + color: Theme.colors.foreground; + font-size: 16px; + font-weight: 600; + vertical-alignment: center; + } + Rectangle { horizontal-stretch: 1; } + if root.is-remote: Button { + text: "↻"; + variant: "ghost"; + width: 28px; + height: 28px; + } + } + Text { + text: root.description; + color: Theme.colors.muted-foreground; + font-size: 13px; + overflow: elide; + } + if root.is-remote: VerticalLayout { + spacing: 6px; + Progress { + value: (root.usage / root.total) * 100; + } + HorizontalLayout { + alignment: space-between; + Text { + text: "Updated: " + root.updated; + color: Theme.colors.muted-foreground; + font-size: 11px; + } + Text { + text: root.usage + " / " + root.total + " GB"; + color: Theme.colors.foreground; + font-size: 11px; + font-weight: 600; + } + } + } + if !root.is-remote: Text { + text: "Local Profile"; + color: Theme.colors.muted-foreground; + font-size: 11px; + } + } + } +} + +component LogRow { + in property time: ""; + in property level: ""; + in property message: ""; + private property level-color: level == "error" ? #ef4444 : + level == "warn" ? #eab308 : + level == "info" ? #3b82f6 : #6b7280; + height: 28px; + HorizontalLayout { + padding-left: 12px; + padding-right: 12px; + spacing: 12px; + Text { + text: root.time; + color: Theme.colors.muted-foreground; + font-size: 12px; + font-family: "monospace"; + vertical-alignment: center; + width: 80px; + } + Text { + text: root.level; + color: root.level-color; + font-size: 12px; + font-family: "monospace"; + font-weight: 700; + vertical-alignment: center; + width: 70px; + } + Text { + text: root.message; + color: Theme.colors.foreground; + font-size: 12px; + font-family: "monospace"; + vertical-alignment: center; + } + } +} + +component ConnectionItem { + in property source: ""; + in property destination: ""; + in property protocol: ""; + in property upload: ""; + in property download: ""; + in property duration: ""; + height: 72px; + Card { + HorizontalLayout { + spacing: 16px; + Rectangle { + width: 48px; + height: 48px; + border-radius: 10px; + background: Theme.colors.primary.transparentize(0.9); + Text { + text: "🔗"; + font-size: 20px; + horizontal-alignment: center; + vertical-alignment: center; + } + } + VerticalLayout { + spacing: 6px; + Text { + text: root.source + " → " + root.destination; + color: Theme.colors.foreground; + font-size: 14px; + font-weight: 500; + overflow: elide; + } + HorizontalLayout { + spacing: 16px; + Text { + text: root.protocol; + color: Theme.colors.muted-foreground; + font-size: 12px; + } + Text { + text: "↑ " + root.upload; + color: Theme.colors.primary; + font-size: 12px; + font-weight: 600; + } + Text { + text: "↓ " + root.download; + color: #22c55e; + font-size: 12px; + font-weight: 600; + } + Text { + text: root.duration; + color: Theme.colors.muted-foreground; + font-size: 12px; + } + } + } + } + } +} + +component ProxyNode { + in property name: ""; + in property type-text: ""; + in property latency: ""; + min-width: 200px; + height: 70px; + Card { + HorizontalLayout { + spacing: 12px; + alignment: space-between; + VerticalLayout { + spacing: 6px; + Text { + text: root.name; + color: Theme.colors.foreground; + font-size: 14px; + font-weight: 600; + } + Text { + text: root.type-text; + color: Theme.colors.muted-foreground; + font-size: 12px; + } + } + Text { + text: root.latency; + color: Theme.colors.primary; + font-size: 14px; + font-weight: 600; + vertical-alignment: center; + } + } + } +} + +// Exported page components + +export component HomePage { + VerticalLayout { + padding: 24px; + spacing: 20px; + Text { + text: "Home"; + font-size: 28px; + font-weight: 700; + color: Theme.colors.foreground; + } + ScrollView { + VerticalLayout { + spacing: 20px; + Card { + VerticalLayout { + spacing: 16px; + HorizontalLayout { + spacing: 16px; + Rectangle { + width: 56px; + height: 56px; + border-radius: 12px; + background: Theme.colors.primary.transparentize(0.9); + Text { + text: "☁"; + color: Theme.colors.primary; + font-size: 28px; + horizontal-alignment: center; + vertical-alignment: center; + } + } + VerticalLayout { + spacing: 6px; + HorizontalLayout { + spacing: 10px; + Text { + text: "NanoCloud"; + color: Theme.colors.foreground; + font-size: 20px; + font-weight: 700; + vertical-alignment: center; + } + } + HorizontalLayout { + spacing: 8px; + Text { + text: "🌐 Free-Japan1-Ver.7"; + color: Theme.colors.muted-foreground; + font-size: 13px; + vertical-alignment: center; + } + Badge { + text: "Vmess"; + variant: "outline"; + } + Badge { + text: "UDP"; + variant: "secondary"; + } + } + } + Rectangle { horizontal-stretch: 1; } + Button { + text: "↻"; + variant: "ghost"; + width: 40px; + height: 40px; + } + } + Rectangle { + height: 1px; + background: Theme.colors.border; + } + GridLayout { + spacing: 20px; + Row { + VerticalLayout { + spacing: 6px; + Text { + text: "Usage / Total"; + color: Theme.colors.muted-foreground; + font-size: 12px; + font-weight: 600; + } + Text { + text: "1.26GB / 100GB"; + color: Theme.colors.foreground; + font-size: 16px; + font-weight: 600; + } + Progress { value: 1.26; } + } + VerticalLayout { + spacing: 6px; + Text { + text: "Expiry Date"; + color: Theme.colors.muted-foreground; + font-size: 12px; + font-weight: 600; + } + Text { + text: "2025-11-11"; + color: Theme.colors.foreground; + font-size: 16px; + font-weight: 600; + } + } + VerticalLayout { + spacing: 6px; + Text { + text: "Last Update"; + color: Theme.colors.muted-foreground; + font-size: 12px; + font-weight: 600; + } + Text { + text: "2025-10-12"; + color: Theme.colors.foreground; + font-size: 16px; + font-weight: 600; + } + } + } + } + } + } + Card { + VerticalLayout { + spacing: 16px; + Text { + text: "Location Info"; + font-size: 16px; + font-weight: 600; + color: Theme.colors.foreground; + } + HorizontalLayout { + spacing: 32px; + VerticalLayout { + spacing: 6px; + Text { + text: "IP Address"; + color: Theme.colors.muted-foreground; + font-size: 12px; + font-weight: 600; + } + Text { + text: "47.238.198.100"; + color: Theme.colors.foreground; + font-size: 14px; + font-weight: 500; + } + } + VerticalLayout { + spacing: 6px; + Text { + text: "Location"; + color: Theme.colors.muted-foreground; + font-size: 12px; + font-weight: 600; + } + Text { + text: "Japan · Tokyo"; + color: Theme.colors.foreground; + font-size: 14px; + font-weight: 500; + } + } + } + } + } + Card { + VerticalLayout { + spacing: 0; + Text { + text: "Quick Settings"; + font-size: 16px; + font-weight: 600; + color: Theme.colors.foreground; + padding-bottom: 12px; + } + SettingRow { + label: "TUN Mode"; + Switch { checked: false; } + } + Rectangle { + height: 1px; + background: Theme.colors.border; + } + SettingRow { + label: "System Proxy"; + Switch { checked: false; } + } + Rectangle { + height: 1px; + background: Theme.colors.border; + } + SettingRow { + label: "Proxy Port"; + Text { + text: "7890"; + color: Theme.colors.primary; + font-size: 14px; + font-weight: 600; + vertical-alignment: center; + } + } + } + } + } + } + } +} + +export component ProfilesPage { + VerticalLayout { + padding: 24px; + spacing: 20px; + HorizontalLayout { + spacing: 12px; + alignment: space-between; + Text { + text: "Profiles"; + font-size: 28px; + font-weight: 700; + color: Theme.colors.foreground; + vertical-alignment: center; + } + HorizontalLayout { + spacing: 8px; + Button { + text: "+ Add"; + variant: "default"; + } + Button { + text: "↻"; + variant: "ghost"; + } + } + } + ScrollView { + GridLayout { + spacing: 16px; + Row { + ProfileCard { + name: "NanoCloud"; + type-icon: "☁"; + description: "Free tier subscription - Japan servers"; + is-remote: true; + usage: 1.26; + total: 100.0; + updated: "Just now"; + } + ProfileCard { + name: "Local Config"; + type-icon: "📄"; + description: "Custom local configuration file"; + is-remote: false; + updated: "2025-01-15"; + } + ProfileCard { + name: "Premium VPN"; + type-icon: "☁"; + description: "Premium subscription with global servers"; + is-remote: true; + usage: 45.8; + total: 500.0; + updated: "2 hours ago"; + } + } + } + } + } +} + +export component LogsPage { + VerticalLayout { + padding: 24px; + spacing: 20px; + Text { + text: "Logs"; + font-size: 28px; + font-weight: 700; + color: Theme.colors.foreground; + } + Card { + height: 500px; + ScrollView { + VerticalLayout { + spacing: 0; + LogRow { + time: "10:23:45"; + level: "info"; + message: "Mihomo core started successfully"; + } + LogRow { + time: "10:23:46"; + level: "info"; + message: "HTTP proxy listening on 127.0.0.1:7890"; + } + LogRow { + time: "10:23:47"; + level: "debug"; + message: "Loading configuration from config.yaml"; + } + LogRow { + time: "10:23:48"; + level: "info"; + message: "Profile loaded: NanoCloud"; + } + LogRow { + time: "10:23:50"; + level: "warn"; + message: "DNS resolution timeout for example.com"; + } + LogRow { + time: "10:23:51"; + level: "error"; + message: "Failed to connect to proxy server"; + } + LogRow { + time: "10:23:52"; + level: "info"; + message: "Retrying connection..."; + } + LogRow { + time: "10:23:53"; + level: "info"; + message: "Connection established successfully"; + } + } + } + } + } +} + +export component ConnectionsPage { + VerticalLayout { + padding: 24px; + spacing: 20px; + Text { + text: "Connections"; + font-size: 28px; + font-weight: 700; + color: Theme.colors.foreground; + } + ScrollView { + VerticalLayout { + spacing: 12px; + ConnectionItem { + source: "192.168.1.100:54321"; + destination: "github.com:443"; + protocol: "HTTPS"; + upload: "1.2 KB/s"; + download: "45.6 KB/s"; + duration: "00:02:15"; + } + ConnectionItem { + source: "192.168.1.100:54322"; + destination: "google.com:443"; + protocol: "HTTPS"; + upload: "0.5 KB/s"; + download: "12.3 KB/s"; + duration: "00:01:30"; + } + ConnectionItem { + source: "192.168.1.100:54323"; + destination: "youtube.com:443"; + protocol: "HTTPS"; + upload: "2.1 KB/s"; + download: "256.7 KB/s"; + duration: "00:05:42"; + } + } + } + } +} + +export component SettingsPage { + VerticalLayout { + padding: 24px; + spacing: 20px; + Text { + text: "Settings"; + font-size: 28px; + font-weight: 700; + color: Theme.colors.foreground; + } + ScrollView { + VerticalLayout { + spacing: 16px; + Card { + VerticalLayout { + spacing: 0; + Text { + text: "App Settings"; + font-size: 16px; + font-weight: 600; + color: Theme.colors.foreground; + padding-bottom: 12px; + } + SettingRow { + label: "Auto Start"; + Switch { checked: false; } + } + Rectangle { + height: 1px; + background: Theme.colors.border; + } + SettingRow { + label: "Silent Start"; + Switch { checked: false; } + } + Rectangle { + height: 1px; + background: Theme.colors.border; + } + SettingRow { + label: "Clash Core"; + Switch { checked: true; } + } + } + } + Card { + VerticalLayout { + spacing: 0; + Text { + text: "Clash Settings"; + font-size: 16px; + font-weight: 600; + color: Theme.colors.foreground; + padding-bottom: 12px; + } + SettingRow { + label: "Allow LAN"; + Switch { checked: false; } + } + Rectangle { + height: 1px; + background: Theme.colors.border; + } + SettingRow { + label: "IPv6"; + Switch { checked: false; } + } + Rectangle { + height: 1px; + background: Theme.colors.border; + } + SettingRow { + label: "Unified Delay"; + Switch { checked: true; } + } + } + } + Card { + VerticalLayout { + spacing: 12px; + Text { + text: "About"; + font-size: 16px; + font-weight: 600; + color: Theme.colors.foreground; + } + HorizontalLayout { + spacing: 12px; + alignment: space-between; + Text { + text: "App Version"; + color: Theme.colors.muted-foreground; + font-size: 14px; + } + Text { + text: "1.0.0"; + color: Theme.colors.foreground; + font-size: 14px; + font-weight: 600; + } + } + } + } + } + } + } +} + +export component ProxiesPage { + VerticalLayout { + padding: 24px; + spacing: 20px; + Text { + text: "Proxies"; + font-size: 28px; + font-weight: 700; + color: Theme.colors.foreground; + } + ScrollView { + VerticalLayout { + spacing: 16px; + Card { + VerticalLayout { + spacing: 12px; + Text { + text: "Hong Kong"; + font-size: 16px; + font-weight: 600; + color: Theme.colors.foreground; + } + GridLayout { + spacing: 12px; + Row { + ProxyNode { + name: "HK-01"; + type-text: "Vmess"; + latency: "45ms"; + } + ProxyNode { + name: "HK-02"; + type-text: "Vmess"; + latency: "52ms"; + } + ProxyNode { + name: "HK-03"; + type-text: "Trojan"; + latency: "38ms"; + } + } + } + } + } + Card { + VerticalLayout { + spacing: 12px; + Text { + text: "Japan"; + font-size: 16px; + font-weight: 600; + color: Theme.colors.foreground; + } + GridLayout { + spacing: 12px; + Row { + ProxyNode { + name: "JP-Tokyo-01"; + type-text: "Vmess"; + latency: "122ms"; + } + ProxyNode { + name: "JP-Tokyo-02"; + type-text: "SS"; + latency: "115ms"; + } + ProxyNode { + name: "JP-Osaka-01"; + type-text: "Vmess"; + latency: "135ms"; + } + } + } + } + } + } + } + } +} diff --git a/ui/pages/connections.slint b/ui/pages/connections.slint new file mode 100644 index 0000000..804b959 --- /dev/null +++ b/ui/pages/connections.slint @@ -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-data; + + private property 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; + } + } + } +} diff --git a/ui/pages/home.slint b/ui/pages/home.slint new file mode 100644 index 0000000..2811bfe --- /dev/null +++ b/ui/pages/home.slint @@ -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 profile-name: "NanoCloud"; + in property profile-node: "免费-日本1-Ver.7"; + in property profile-usage: 1.26; + in property profile-total: 100; + in property profile-expiry: "2025-11-11"; + in property profile-updated: "2025-10-12 10:05"; + + in property location-ip: "47.238.198.100"; + in property location-region: "日本 · 东京"; + + in property tun-mode-enabled: false; + in property system-proxy-enabled: false; + in property 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 profile-name: ""; + in property profile-node: ""; + in property usage: 0; + in property total: 100; + in property expiry: ""; + in property 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 ip: ""; + in property 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 service: ""; + in property 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 tun-enabled: false; + in property proxy-enabled: false; + in property 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 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 + } +} diff --git a/ui/pages/logs.slint b/ui/pages/logs.slint new file mode 100644 index 0000000..0858ddf --- /dev/null +++ b/ui/pages/logs.slint @@ -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 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 log-entry; + in property ool> odd: false; + + private property hovered: false; + + private property 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; + } + } + } +} diff --git a/ui/pages/profiles.slint b/ui/pages/profiles.slint new file mode 100644 index 0000000..a02d1aa --- /dev/null +++ b/ui/pages/profiles.slint @@ -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 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 profile-data; + in property selected: false; + + callback clicked(); + callback refresh-clicked(); + + private property 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; + } + } + } +} diff --git a/ui/pages/proxies.slint b/ui/pages/proxies.slint new file mode 100644 index 0000000..322b0e1 --- /dev/null +++ b/ui/pages/proxies.slint @@ -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 current-mode: 0; // 0=Rule, 1=Global, 2=Direct + in property 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 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 node-data; + in property selected: false; + + callback clicked(); + + private property 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; + } + } + } +} diff --git a/ui/pages/settings.slint b/ui/pages/settings.slint new file mode 100644 index 0000000..de0dcb7 --- /dev/null +++ b/ui/pages/settings.slint @@ -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 auto-start: false; + in property silent-start: false; + in property clash-core: false; + + // Clash Settings + in property tun-mode: false; + in property allow-lan: false; + in property ipv6: false; + in property unified-delay: false; + in property log-level-index: 1; + in property proxy-port: 7890; + + // Misc + in property app-dir: "C:/Program Files/Clash"; + in property config-dir: "C:/Users/User/.config/clash"; + in property core-dir: "C:/Program Files/Clash/core"; + in property 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 icon: ""; + in property 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 + } +} diff --git a/ui/theme/theme.slint b/ui/theme/theme.slint index cefe5e4..f32d2a6 100644 --- a/ui/theme/theme.slint +++ b/ui/theme/theme.slint @@ -4,6 +4,7 @@ import { ColorPalette, LightColors, DarkColors } from "colors.slint"; import { Typography } from "typography.slint"; import { SpacingSystem } from "spacing.slint"; +import { Animations } from "../utils/animations.slint"; // Global theme manager (singleton) export global Theme { @@ -23,4 +24,4 @@ export global Theme { } // Re-export for convenience -export { Typography, SpacingSystem } +export { Typography, SpacingSystem, Animations } diff --git a/web/src/App.tsx b/web/src/App.tsx new file mode 100644 index 0000000..6e909f0 --- /dev/null +++ b/web/src/App.tsx @@ -0,0 +1,88 @@ +import {SidebarInset, SidebarMenuButton, SidebarMenuItem, SidebarProvider} from "@/components/ui/sidebar.tsx"; +import {EarthIcon, Grid2X2Icon, HomeIcon, Link2Icon, LogsIcon, SettingsIcon} from "lucide-react"; +import {AppSidebar} from "@/components/app-sidebar.tsx"; +import {Link, Route, Switch, useLocation} from "wouter"; +import {Home} from "@/pages/Home"; +import Proxies from "@/pages/Proxies.tsx"; +import {Profiles} from "@/pages/Profiles"; +import {Connections} from "@/pages/Connections.tsx"; +import {Logs} from "@/pages/Logs.tsx"; +import {Settings} from "@/pages/Settings.tsx"; +import {Toaster} from "@/components/ui/sonner.tsx"; +import {QueryClient, QueryClientProvider} from "@tanstack/react-query"; + +// Menu items. +const items = [ + { + title: "Home", + url: "/", + icon: HomeIcon, + }, + { + title: "Proxies", + url: "/proxies", + icon: EarthIcon, + }, + { + title: "Profiles", + url: "/profiles", + icon: Grid2X2Icon, + }, + { + title: "Connections", + url: "/connections", + icon: Link2Icon, + }, + { + title: "Logs", + url: "/logs", + icon: LogsIcon, + }, + { + title: "Settings", + url: "/settings", + icon: SettingsIcon, + }, +] + +const queryClient = new QueryClient() + +function App() { + const [location] = useLocation(); + return ( +
+ + + + + {items.map((item) => ( + + + + + {item.title} + + + + ))} + + +
+ + + + + + + + +
+
+
+
+ +
+ ) +} + +export default App diff --git a/web/src/assets/react.svg b/web/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/web/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/components/app-content.tsx b/web/src/components/app-content.tsx new file mode 100644 index 0000000..eed0b70 --- /dev/null +++ b/web/src/components/app-content.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import {cn} from "@/lib/utils.ts"; +import {ScrollArea} from "@/components/ui/scroll-area.tsx"; +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; +import {type} from "@tauri-apps/plugin-os"; + +type DivProps = React.DetailedHTMLProps, HTMLDivElement>; + +export function ContentHeader({ + className, + children, + ...props +}: DivProps) { + return ( +
+ {children} +
+ ) + +} + +export function ScrollableBody({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + {children} + + ) +} + +export const Container = ({ + className, + children, + ...props +}: DivProps) => { + return
{children}
+} \ No newline at end of file diff --git a/web/src/components/app-sidebar.tsx b/web/src/components/app-sidebar.tsx new file mode 100644 index 0000000..1f76397 --- /dev/null +++ b/web/src/components/app-sidebar.tsx @@ -0,0 +1,163 @@ +import {Command, Loader2, Play, Power} from "lucide-react" +import {type} from '@tauri-apps/plugin-os'; + +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "@/components/ui/sidebar" +import {Button} from "@/components/ui/button.tsx"; +import {ReactNode} from "react"; +import {cn} from "@/lib/utils.ts"; +import {useMutation, useQuery} from "@tanstack/react-query"; +import {invokeCommand} from "@/lib/invoke.ts"; + +type Status = + | { status: "NotStart" } + | { status: "Running" } + | { status: "Exited"; code: number } + +const delay = (ms: number) => new Promise((resolve) => { + const timer = setTimeout(resolve, ms) + // 返回一个清理函数 + return () => clearTimeout(timer) +}) + +function MihomoStateBar() { + const {data: status} = useQuery({ + queryKey: ['mihomo_status'], + queryFn: async () => invokeCommand('mihomo_status'), + refetchInterval: 1000 + }) + + const switchCore = useMutation({ + mutationFn: async () => { + const duration = delay(1000) + if (status?.status == "Running") { + await invokeCommand('mihomo_stop') + } else { + await invokeCommand('mihomo_start') + } + await duration + } + }) + + const isRunning = status?.status == "Running"; + const isStarting = switchCore.isPending; + + return
+
+
+ {/*呼吸灯效果*/} + {(isStarting || isRunning) && ( + + )} + +
+ +
+ + Clash + + + {isStarting ? "Starting..." : isRunning ? "Running" : "Stopped"} + +
+
+ + +
+} + +export function AppSidebar({children}: { children: ReactNode }) { + return ( + + + {/*macOS only*/} + {type() == "macos" &&
} + + + + +
+ +
+
+ Acme Inc + Enterprise +
+
+
+
+
+ + + + Application + + + {children} + + + + + + + + + ) +} \ No newline at end of file diff --git a/web/src/components/button.tsx b/web/src/components/button.tsx new file mode 100644 index 0000000..6034c48 --- /dev/null +++ b/web/src/components/button.tsx @@ -0,0 +1,32 @@ +import {Tooltip, TooltipContent} from "@/components/ui/tooltip.tsx"; +import {TooltipTrigger} from "@radix-ui/react-tooltip"; +import {ReactNode} from "react"; +import * as React from "react"; +import type {VariantProps} from "class-variance-authority"; +import {Button, buttonVariants} from "@/components/ui/button.tsx"; + + +export function TipButton({tipContent, ...props}: { + children: ReactNode, + tipContent?: ReactNode, +} & React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean +}) { + if (!tipContent) { + return + + +
+ + + {/* 输入配置 */} + + + updateConfig('mtu', parseInt(e.target.value) || undefined)} + placeholder="9000" + className="h-7 text-xs shadow-none" + /> + + + +
+ {/* DNS 列表 */} + {config.dnsHijack && config.dnsHijack.length > 0 && ( +
+ {config.dnsHijack.map((dns, index) => ( +
+ {dns} + +
+ ))} +
+ )} + + {/* 添加输入框 */} +
+ setDnsInput(e.target.value)} + onKeyDown={handleDnsInputKeyDown} + placeholder="0.0.0.0:53" + className="h-7 text-xs shadow-none font-mono flex-1" + /> + +
+
+
+ + + + + + + + + ) +} diff --git a/web/src/components/tun-settings-dialog.tsx b/web/src/components/tun-settings-dialog.tsx new file mode 100644 index 0000000..d7a5c11 --- /dev/null +++ b/web/src/components/tun-settings-dialog.tsx @@ -0,0 +1,133 @@ +// import {useState, useEffect} from "react"; +// import {Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogClose} from "@/components/ui/dialog"; +// import {Button} from "@/components/ui/button"; +// import {Input} from "@/components/ui/input"; +// import {Label} from "@/components/ui/label"; +// import {Switch} from "@/components/ui/switch"; +// import {TunConfig} from "@/lib/types"; +// import {toast} from "sonner"; +// import {Tabs, TabsList, TabsTrigger} from "@/components/ui/tabs.tsx"; +// +// interface TunSettingsDialogProps { +// open: boolean; +// onOpenChange: (open: boolean) => void; +// config: TunConfig; +// onSave: (config: TunConfig) => Promise; +// } +// +// export function TunSettingsDialog({open, onOpenChange, config, onSave}: TunSettingsDialogProps) { +// const [localConfig, setLocalConfig] = useState(config); +// +// useEffect(() => { +// setLocalConfig(config); +// }, [config, open]); +// +// const handleSave = async () => { +// try { +// await onSave(localConfig); +// onOpenChange(false); +// toast.success("TUN settings saved successfully"); +// } catch (error) { +// console.error(error); +// // toast.error is handled in fetch wrapper usually, but good to be safe +// } +// }; +// +// return ( +// +// +// +// TUN Mode Settings +// +//
+//
+// +// +//
+// +// +// System +// Gvisor +// Mixed +// +// +//
+//
+//
+// +//
+// setLocalConfig({...localConfig, auto_route: checked})} +// /> +//
+//
+//
+// +//
+// setLocalConfig({...localConfig, strict_route: checked})} +// /> +//
+//
+//
+// +// setLocalConfig({...localConfig, device: e.target.value})} +// className="col-span-3" +// /> +//
+//
+// +// setLocalConfig({...localConfig, mtu: parseInt(e.target.value) || 0})} +// className="col-span-3" +// /> +//
+//
+// +// +// setLocalConfig({ +// ...localConfig, +// dns_hijack: e.target.value.split(",").map((s) => s.trim()).filter(Boolean), +// }) +// } +// className="col-span-3" +// placeholder="any:53, tcp://any:53" +// /> +//
+// +//
+// +// +// +// +// +// +//
+//
+// ); +// } diff --git a/web/src/components/ui/accordion.tsx b/web/src/components/ui/accordion.tsx new file mode 100644 index 0000000..0bb6c22 --- /dev/null +++ b/web/src/components/ui/accordion.tsx @@ -0,0 +1,64 @@ +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDownIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Accordion({ + ...props +}: React.ComponentProps) { + return +} + +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + + ) +} + +function AccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + +
{children}
+
+ ) +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/web/src/components/ui/avatar.tsx b/web/src/components/ui/avatar.tsx new file mode 100644 index 0000000..b7224f0 --- /dev/null +++ b/web/src/components/ui/avatar.tsx @@ -0,0 +1,51 @@ +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/web/src/components/ui/badge.tsx b/web/src/components/ui/badge.tsx new file mode 100644 index 0000000..0205413 --- /dev/null +++ b/web/src/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/web/src/components/ui/button-group.tsx b/web/src/components/ui/button-group.tsx new file mode 100644 index 0000000..8600af0 --- /dev/null +++ b/web/src/components/ui/button-group.tsx @@ -0,0 +1,83 @@ +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" +import { Separator } from "@/components/ui/separator" + +const buttonGroupVariants = cva( + "flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2", + { + variants: { + orientation: { + horizontal: + "[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none", + vertical: + "flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none", + }, + }, + defaultVariants: { + orientation: "horizontal", + }, + } +) + +function ButtonGroup({ + className, + orientation, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function ButtonGroupText({ + className, + asChild = false, + ...props +}: React.ComponentProps<"div"> & { + asChild?: boolean +}) { + const Comp = asChild ? Slot : "div" + + return ( + + ) +} + +function ButtonGroupSeparator({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + ButtonGroup, + ButtonGroupSeparator, + ButtonGroupText, + buttonGroupVariants, +} diff --git a/web/src/components/ui/button.tsx b/web/src/components/ui/button.tsx new file mode 100644 index 0000000..21409a0 --- /dev/null +++ b/web/src/components/ui/button.tsx @@ -0,0 +1,60 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + "icon-sm": "size-8", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/web/src/components/ui/card.tsx b/web/src/components/ui/card.tsx new file mode 100644 index 0000000..681ad98 --- /dev/null +++ b/web/src/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/web/src/components/ui/context-menu.tsx b/web/src/components/ui/context-menu.tsx new file mode 100644 index 0000000..624c588 --- /dev/null +++ b/web/src/components/ui/context-menu.tsx @@ -0,0 +1,250 @@ +import * as React from "react" +import * as ContextMenuPrimitive from "@radix-ui/react-context-menu" +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function ContextMenu({ + ...props +}: React.ComponentProps) { + return +} + +function ContextMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function ContextMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function ContextMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function ContextMenuSub({ + ...props +}: React.ComponentProps) { + return +} + +function ContextMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function ContextMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + {children} + + + ) +} + +function ContextMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function ContextMenuContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function ContextMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean + variant?: "default" | "destructive" +}) { + return ( + + ) +} + +function ContextMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function ContextMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function ContextMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + ) +} + +function ContextMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function ContextMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +export { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, + ContextMenuCheckboxItem, + ContextMenuRadioItem, + ContextMenuLabel, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuGroup, + ContextMenuPortal, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuRadioGroup, +} diff --git a/web/src/components/ui/dialog.tsx b/web/src/components/ui/dialog.tsx new file mode 100644 index 0000000..6cb123b --- /dev/null +++ b/web/src/components/ui/dialog.tsx @@ -0,0 +1,141 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Dialog({ + ...props +}: React.ComponentProps) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/web/src/components/ui/empty.tsx b/web/src/components/ui/empty.tsx new file mode 100644 index 0000000..df91e9d --- /dev/null +++ b/web/src/components/ui/empty.tsx @@ -0,0 +1,104 @@ +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +function Empty({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +const emptyMediaVariants = cva( + "flex shrink-0 items-center justify-center mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0", + { + variants: { + variant: { + default: "bg-transparent", + icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function EmptyMedia({ + className, + variant = "default", + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) { + return ( +
a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4", + className + )} + {...props} + /> + ) +} + +function EmptyContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Empty, + EmptyHeader, + EmptyTitle, + EmptyDescription, + EmptyContent, + EmptyMedia, +} diff --git a/web/src/components/ui/field.tsx b/web/src/components/ui/field.tsx new file mode 100644 index 0000000..db0dc12 --- /dev/null +++ b/web/src/components/ui/field.tsx @@ -0,0 +1,246 @@ +import { useMemo } from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" +import { Separator } from "@/components/ui/separator" + +function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { + return ( +
[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3", + className + )} + {...props} + /> + ) +} + +function FieldLegend({ + className, + variant = "legend", + ...props +}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) { + return ( + + ) +} + +function FieldGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
[data-slot=field-group]]:gap-4", + className + )} + {...props} + /> + ) +} + +const fieldVariants = cva( + "group/field flex w-full gap-3 data-[invalid=true]:text-destructive", + { + variants: { + orientation: { + vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"], + horizontal: [ + "flex-row items-center", + "[&>[data-slot=field-label]]:flex-auto", + "has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + ], + responsive: [ + "flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto", + "@md/field-group:[&>[data-slot=field-label]]:flex-auto", + "@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + ], + }, + }, + defaultVariants: { + orientation: "vertical", + }, + } +) + +function Field({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function FieldContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function FieldLabel({ + className, + ...props +}: React.ComponentProps) { + return ( +