Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions crates/remux-dashboard/assets/theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,90 @@ html, body {
padding: 20px 0;
}

/* ── Public User Selection Grid ─────────────────────────────────── */
.public-users-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 16px;
margin: 4px 0 16px 0;
padding: 8px;
background: var(--panel-elevated);
border-radius: var(--radius-md);
border: 1px solid var(--border);
}

.public-user-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
cursor: pointer;
width: 72px;
transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1);
background: transparent;
border: none;
padding: 0;
color: inherit;
font: inherit;
border-radius: var(--radius-sm);
}

.public-user-card:focus-visible {
outline: 2px solid var(--primary);
outline-offset: 4px;
}

.public-user-card:hover {
transform: translateY(-2px);
}

.public-user-avatar-wrapper {
width: 52px;
height: 52px;
border-radius: 50%;
overflow: hidden;
background: var(--border);
display: flex;
align-items: center;
justify-content: center;
border: 2px solid transparent;
transition: all 0.2s ease;
box-shadow: var(--shadow-sm);
}

.public-user-card:hover .public-user-avatar-wrapper,
.public-user-card:focus-visible .public-user-avatar-wrapper {
border-color: var(--primary);
box-shadow: 0 0 12px rgba(var(--primary-rgb), 0.3);
}

.public-user-avatar-img {
width: 100%;
height: 100%;
object-fit: cover;
}

.public-user-avatar-placeholder {
font-size: 1.5rem;
font-weight: 800;
color: var(--text-muted);
text-transform: uppercase;
user-select: none;
}

.public-user-name {
font-size: 0.72rem;
font-weight: 600;
color: var(--text);
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}

/* ── Task groups ─────────────────────────────────────────────────── */
.task-group + .task-group {
margin-top: 16px;
Expand Down
73 changes: 72 additions & 1 deletion crates/remux-dashboard/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use dioxus::prelude::*;
use web_sys::wasm_bindgen::JsCast;
use remux_sdks::{
remux::{
AuthenticateUserByName, CountryInfo, GetCountries, GetStartupConfiguration,
JellyfinAuth, PostStartupComplete, PostStartupConfiguration, PostStartupUser,
PublicSystemInfo, StartupConfiguration, StartupUser, Username,
PublicSystemInfo, StartupConfiguration, StartupUser, Username, GetPublicUsers, UserDto,
},
ClientError,
};
Expand Down Expand Up @@ -87,6 +88,23 @@ fn Login(on_login: EventHandler) -> Element {
let mut password = use_signal(String::new);
let mut error = use_signal(|| Option::<String>::None);
let mut loading = use_signal(|| false);
let mut public_users = use_signal(Vec::<UserDto>::new);

let server_url_effect = server_url.clone();
use_effect(move || {
let url_opt = server_url_effect.read().clone();
if let Some(url) = url_opt {
if !url.is_empty() {
spawn(async move {
if let Ok(client) = remux_sdks::remux::client(&url) {
if let Ok(users) = client.execute(GetPublicUsers).await {
public_users.set(users);
}
}
});
}
}
});

use_effect(move || {
spawn(async move {
Expand Down Expand Up @@ -202,6 +220,59 @@ fn Login(on_login: EventHandler) -> Element {
onsubmit: on_submit,
style: "display:flex;flex-direction:column;gap:14px;",

if !public_users.read().is_empty() {
div {
style: "text-align:center;margin-bottom:6px;",
span {
style: "font-size:0.75rem;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.05em",
"Select User"
}
}
div { class: "public-users-container",
for user in public_users.read().clone() {
{
let username_val = user.name.clone();
let has_img = user.primary_image_tag.is_some();
let user_id = user.id;
let url = server_url.read().clone().unwrap_or_default();
rsx! {
button {
r#type: "button",
class: "public-user-card",
key: "{user_id}",
onclick: move |_| {
username.set(username_val.clone());
if let Some(window) = web_sys::window() {
if let Some(document) = window.document() {
if let Some(element) = document.get_element_by_id("password") {
if let Ok(html_element) = element.dyn_into::<web_sys::HtmlElement>() {
let _ = html_element.focus();
}
}
}
}
},
div { class: "public-user-avatar-wrapper",
if has_img {
img {
class: "public-user-avatar-img",
src: "{url}/users/{user_id}/images/primary",
alt: "{username_val}",
}
} else {
span { class: "public-user-avatar-placeholder",
"{username_val.chars().next().unwrap_or('?')}"
}
}
}
span { class: "public-user-name", "{username_val}" }
}
}
}
}
}
}

if server_url.read().as_deref() == Some("") {
div { class: "field",
label { class: "field-label", r#for: "host", "Server URL" }
Expand Down
55 changes: 31 additions & 24 deletions crates/remux-dashboard/src/pages/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,12 @@ pub fn UserForm(
})
.unwrap_or(true)
});
let mut hide_from_login = use_signal(|| {
existing
.as_ref()
.map(|u| u.policy.is_hidden)
.unwrap_or(false)
});

let on_submit = move |e: Event<FormData>| {
e.prevent_default();
Expand Down Expand Up @@ -309,6 +315,7 @@ pub fn UserForm(
let remote_search_snapshot = *enable_remote_search.peek();
let max_sessions_snapshot = *max_active_sessions.peek();
let video_transcoding_snapshot = *enable_video_transcoding.peek();
let hide_from_login_snapshot = *hide_from_login.peek();

saving.set(true);
err.set(None);
Expand Down Expand Up @@ -354,6 +361,7 @@ pub fn UserForm(
.policy
.clone();
policy.is_administrator = admin;
policy.is_hidden = hide_from_login_snapshot;
policy.filter_rules = filter_rules.clone();
policy.stream_filter = stream_filter.clone();
policy.enable_remote_search = remote_search_snapshot;
Expand All @@ -380,30 +388,23 @@ pub fn UserForm(
let new_user = client
.execute(CreateUser { name, password: pw })
.await?;
if admin
|| filter_rules.is_some()
|| stream_filter.is_some()
|| !remote_search_snapshot
|| max_sessions_snapshot > 0
|| !video_transcoding_snapshot
{
let mut policy = new_user
.policy
.clone();
policy.is_administrator = admin;
policy.filter_rules = filter_rules.clone();
policy.stream_filter = stream_filter.clone();
policy.enable_remote_search = remote_search_snapshot;
policy.max_active_sessions = max_sessions_snapshot;
policy.enable_video_playback_transcoding =
video_transcoding_snapshot;
client
.execute(UpdateUserPolicy {
user_id: new_user.id,
policy,
})
.await?;
}
let mut policy = new_user
.policy
.clone();
policy.is_administrator = admin;
policy.is_hidden = hide_from_login_snapshot;
policy.filter_rules = filter_rules.clone();
policy.stream_filter = stream_filter.clone();
policy.enable_remote_search = remote_search_snapshot;
policy.max_active_sessions = max_sessions_snapshot;
policy.enable_video_playback_transcoding =
video_transcoding_snapshot;
client
.execute(UpdateUserPolicy {
user_id: new_user.id,
policy,
})
.await?;
}
Ok(())
}
Expand Down Expand Up @@ -487,6 +488,12 @@ pub fn UserForm(
on_change: move |v| enable_video_transcoding.set(v),
}

ToggleRow {
label: "Hide user from login screens",
checked: *hide_from_login.read(),
on_change: move |v| hide_from_login.set(v),
}

div { class: "field",
label { class: "field-label", r#for: "u-max-streams", "Max Concurrent Streams" }
input {
Expand Down
10 changes: 10 additions & 0 deletions crates/remux-sdks/src/remux/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4889,6 +4889,16 @@ impl Endpoint for GetUsers {
}
}

#[derive(Debug, Clone)]
pub struct GetPublicUsers;

impl Endpoint for GetPublicUsers {
type Output = Vec<UserDto>;
fn path(&self) -> String {
"/users/public".into()
}
}

#[derive(Debug, Clone)]
pub struct CreateUser {
pub name: String,
Expand Down
3 changes: 3 additions & 0 deletions crates/remux-server/src/api/startup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ pub async fn post_startup_user(
None,
)?;
user.is_admin = true;
let mut policy = crate::api::UserPolicy::default();
policy.is_hidden = true;
user.policy = Some(sqlx::types::Json(policy));
user.save_by_username(
&state
.ctx
Expand Down
Loading
Loading