Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
72 changes: 72 additions & 0 deletions crates/remux-dashboard/assets/theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,78 @@ 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);
}

.public-user-card:hover {
transform: translateY(-2px);
}
Comment thread
BrandonItaly marked this conversation as resolved.

.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 {
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
72 changes: 71 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,58 @@ 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! {
div {
class: "public-user-card",
key: "{user_id}",
Comment thread
BrandonItaly marked this conversation as resolved.
Outdated
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;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is hidden is already true by default.

think we should make policy non optional for a user (struct) so its already there.

user.policy = Some(sqlx::types::Json(policy));
user.save_by_username(
&state
.ctx
Expand Down
20 changes: 18 additions & 2 deletions crates/remux-server/src/api/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -761,8 +761,24 @@ pub async fn update_user(
// ===== Route aliases (same handler, different path) =====

#[get("/users/public")]
pub async fn users_public() -> Result<impl IntoResponse> {
Ok(Json::<Vec<api::UserDto>>(vec![]).into_response())
pub async fn users_public(
State(state): State<AppState>,
) -> Result<impl IntoResponse> {
let all_users = sqlx::query_as::<_, User>("SELECT * FROM users")
.fetch_all(&state.ctx.db)
.await?;
Comment thread
BrandonItaly marked this conversation as resolved.

let dtos: Vec<api::UserDto> = all_users
.into_iter()
Comment thread
BrandonItaly marked this conversation as resolved.
.filter(|u| {
let policy = u.policy.as_ref().map(|p| &p.0);
let is_hidden = policy.map(|p| p.is_hidden).unwrap_or(false);
!is_hidden
})
Comment thread
BrandonItaly marked this conversation as resolved.
.map(|u| api::db_user_to_dto(&state.ctx.config.data_dir, u))
.collect();

Ok(Json(dtos).into_response())
}

#[get("/users/{user_id}")]
Expand Down