diff --git a/gui/assets/arrow-right-svgrepo-com.svg b/gui/assets/arrow-right-svgrepo-com.svg
new file mode 100644
index 0000000..4ae7291
--- /dev/null
+++ b/gui/assets/arrow-right-svgrepo-com.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/gui/assets/delete-2-svgrepo-com.svg b/gui/assets/delete-2-svgrepo-com.svg
new file mode 100644
index 0000000..27cdd28
--- /dev/null
+++ b/gui/assets/delete-2-svgrepo-com.svg
@@ -0,0 +1,8 @@
+
+
\ No newline at end of file
diff --git a/gui/assets/earth-14-svgrepo-com.svg b/gui/assets/earth-14-svgrepo-com.svg
new file mode 100644
index 0000000..0512760
--- /dev/null
+++ b/gui/assets/earth-14-svgrepo-com.svg
@@ -0,0 +1,135 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/gui/assets/edit-3-svgrepo-com.svg b/gui/assets/edit-3-svgrepo-com.svg
new file mode 100644
index 0000000..7d955aa
--- /dev/null
+++ b/gui/assets/edit-3-svgrepo-com.svg
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/gui/assets/main.scss b/gui/assets/main.scss
index 4584e68..439f7b7 100644
--- a/gui/assets/main.scss
+++ b/gui/assets/main.scss
@@ -432,3 +432,348 @@ a:visited {
}
}
}
+
+.server-list-page {
+ display: flex;
+ flex-direction: column;
+ padding: 1.5rem;
+ gap: 1rem;
+}
+
+.server-list-page h1 {
+ text-align: center;
+}
+
+.login_version {
+ font-size: 0.55em;
+ font-weight: 400;
+ color: rgba(255, 255, 255, 0.4);
+ vertical-align: middle;
+}
+
+.server-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+ width: 500px;
+ margin: 0 auto;
+}
+
+/* Rounded card */
+.server-card {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ padding: 1rem 1.25rem;
+ border-radius: 12px;
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.server-card__icon {
+ width: 32px;
+ height: 32px;
+ opacity: 0.65;
+ filter: brightness(0) invert(0.8); /* light gray */
+ flex-shrink: 0;
+}
+
+.server-card__info {
+ display: flex;
+ flex-direction: column;
+ gap: 0.15rem;
+ flex: 1; /* pushes the connect button to the far right */
+ min-width: 0; /* prevents text overflow from breaking flex layout */
+}
+
+.server-card__name {
+ font-weight: 600;
+ font-size: 0.95rem;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.server-card__address {
+ font-size: 0.78rem;
+ opacity: 0.55;
+}
+
+
+.server-card__action {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 38px;
+ height: 38px;
+ padding: 0;
+ line-height: 0;
+ border-radius: 8px;
+ border: 1px solid rgba(255, 255, 255, 0.15);
+ background: rgba(255, 255, 255, 0.07);
+ cursor: pointer;
+ transition: background 0.15s, border-color 0.15s, transform 0.1s;
+}
+
+.server-card__action img {
+ width: 20px;
+ height: 20px;
+ filter: brightness(0) invert(0.8); /* light gray */
+ opacity: 0.75;
+ transition: opacity 0.15s;
+}
+
+.server-card__action:hover {
+ background: rgba(255, 255, 255, 0.15);
+ border-color: rgba(255, 255, 255, 0.35);
+ transform: scale(1.08);
+}
+
+.server-card__action:hover img {
+ opacity: 1.0;
+}
+
+.server-card__action:active {
+ transform: scale(0.95);
+}
+
+/* Add server — dashed outline style to distinguish from real cards */
+.add-server-btn {
+ width: 100%;
+ padding: 0.85rem;
+ border-radius: 12px;
+ border: 2px dashed rgba(255, 255, 255, 0.2);
+ background: transparent;
+ color: rgba(255, 255, 255, 0.45);
+ font-size: 0.9rem;
+ cursor: pointer;
+ transition: border-color 0.15s, color 0.15s;
+ width: 500px;
+ margin: 0 auto;
+}
+
+.add-server-btn:hover {
+ border-color: rgba(255, 255, 255, 0.4);
+ color: rgba(255, 255, 255, 0.7);
+}
+
+
+.modal-backdrop {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.0);
+ z-index: 999;
+ animation: backdrop-fade-in 150ms ease-out forwards;
+}
+
+.modal-container {
+ position: fixed;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+ pointer-events: none;
+}
+
+
+.modal {
+ pointer-events: auto;
+
+ /* Make this solid or nearly solid instead of see-through */
+ background: #141414;
+
+ border-radius: 12px;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.45);
+
+ padding: 1.25rem 1.5rem 1.4rem;
+ width: 500px;
+ max-width: 90vw;
+
+ color: #fff;
+ display: flex;
+ flex-direction: column;
+ gap: 0.9rem;
+
+ opacity: 0;
+ transform: scale(0.9);
+ animation: modal-pop-in 160ms ease-out forwards;
+}
+
+.modal h2 {
+ font-size: 1.05rem;
+ font-weight: 600;
+ text-align: left;
+ margin: 0;
+}
+
+/* Form layout */
+
+.modal-field {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+}
+
+.modal-field label {
+ font-size: 0.8rem;
+ opacity: 0.7;
+}
+
+.modal-field input {
+ padding: 0.55rem 0.6rem;
+ border-radius: 8px;
+ border: 1px solid rgba(255, 255, 255, 0.18);
+ background: rgba(0, 0, 0, 0.35);
+ color: #fff;
+ font-size: 0.85rem;
+ outline: none;
+ transition: border-color 0.15s, background 0.15s, box-shadow 0.15s;
+}
+
+.modal-field input::placeholder {
+ color: rgba(255, 255, 255, 0.45);
+}
+
+.modal-field input:focus {
+ border-color: rgba(255, 255, 255, 0.55);
+ background: rgba(0, 0, 0, 0.55);
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.25);
+}
+
+/* Actions row */
+
+.modal-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 0.5rem;
+ margin-top: 0.5rem;
+}
+
+/* Secondary button (Cancel) */
+
+.modal-btn {
+ padding: 0.5rem 0.9rem;
+ border-radius: 8px;
+ border: 1px solid rgba(255, 255, 255, 0.18);
+ background: rgba(255, 255, 255, 0.07);
+ color: rgba(255, 255, 255, 0.85);
+ font-size: 0.85rem;
+ cursor: pointer;
+ transition: background 0.15s, border-color 0.15s, transform 0.1s;
+}
+
+.modal-btn:hover {
+ background: rgba(255, 255, 255, 0.15);
+ border-color: rgba(255, 255, 255, 0.35);
+ transform: translateY(-1px);
+}
+
+.modal-btn:active {
+ transform: translateY(0) scale(0.97);
+}
+
+/* Primary button (Save) */
+
+.modal-btn--primary {
+ background: rgba(67, 156, 255, 0.85);
+ border-color: rgba(67, 156, 255, 1);
+ color: #ffffff;
+}
+
+.modal-btn--primary:hover {
+ background: rgba(92, 174, 255, 0.95);
+ border-color: rgba(135, 196, 255, 1);
+}
+
+/* Delete button (danger) */
+
+.modal-btn--danger {
+ background: rgba(220, 60, 60, 0.85);
+ border-color: rgba(220, 60, 60, 1);
+ color: #ffffff;
+}
+
+.modal-btn--danger:hover {
+ background: rgba(240, 80, 80, 0.95);
+ border-color: rgba(255, 120, 120, 1);
+}
+
+.modal-actions__spacer {
+ flex: 1;
+}
+
+/* Override mode username row */
+
+.override-username-row {
+ display: flex;
+ gap: 0.75rem;
+ align-items: center;
+ padding: 0.75rem 1.25rem;
+ border-radius: 12px;
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.override-username-input {
+ flex: 1;
+ padding: 0.55rem 0.6rem;
+ border-radius: 8px;
+ border: 1px solid rgba(255, 255, 255, 0.18);
+ background: rgba(0, 0, 0, 0.35);
+ color: #fff;
+ font-size: 0.85rem;
+ outline: none;
+ transition: border-color 0.15s, background 0.15s, box-shadow 0.15s;
+}
+
+.override-username-input:focus {
+ border-color: rgba(255, 255, 255, 0.55);
+ background: rgba(0, 0, 0, 0.55);
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.25);
+}
+
+.override-username-input::placeholder {
+ color: rgba(255, 255, 255, 0.45);
+}
+
+/* Connect action button highlight */
+
+.server-card__action--connect:hover {
+ background: rgba(67, 156, 255, 0.3);
+ border-color: rgba(67, 156, 255, 0.6);
+}
+
+/* Ping info on server card */
+
+.server-card__ping {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ gap: 0.1rem;
+ font-size: 0.75rem;
+ opacity: 0.6;
+ flex-shrink: 0;
+ min-width: 60px;
+ text-align: right;
+}
+
+/* Keyframes */
+
+@keyframes backdrop-fade-in {
+ from { background: rgba(0, 0, 0, 0.0); }
+ to { background: rgba(0, 0, 0, 0.4); }
+}
+
+@keyframes modal-pop-in {
+ from {
+ opacity: 0;
+ transform: scale(0.9);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1.0);
+ }
+}
diff --git a/gui/src/main.rs b/gui/src/main.rs
index f246ad2..0625106 100644
--- a/gui/src/main.rs
+++ b/gui/src/main.rs
@@ -3,12 +3,10 @@
use dioxus::prelude::*;
use mumble_web2_client::{
network_entrypoint, reqwest, AudioSettings, ChannelId, Command, ConfigSystem,
- ConfigSystemInterface as _, ConnectionState, Platform, PlatformInterface as _, ServerState,
+ ConfigSystemInterface as _, ConnectTarget, ConnectionState, Platform, PlatformInterface as _,
SharedState, State, UserId, UserState, VERSION,
};
-use mumble_web2_common::{ProxyOverrides, ServerStatus};
-use std::collections::{HashMap, HashSet};
-use std::{fmt, sync::Arc};
+use mumble_web2_common::{ProxyOverrides, ServerEntry};
use Command::*;
use ConnectionState::*;
@@ -498,183 +496,528 @@ pub fn ServerView(overrides: Resource) -> Element {
}
#[component]
-pub fn LoginView(overrides: Resource) -> Element {
+fn ServerCard(
+ idx: usize,
+ server: ServerEntry,
+ editing_index: Signal