fix(skins): isolate UrlModal and PreviewPanel as Component structs to prevent conditional hooks
This commit is contained in:
parent
5a7ecf9293
commit
005e7ce77b
1 changed files with 430 additions and 360 deletions
|
|
@ -26,6 +26,10 @@ impl Component for AccountSkins {
|
|||
.and_then(|id| accounts.iter().find(|a| a.id == id).cloned())
|
||||
.or_else(|| default_account.clone());
|
||||
|
||||
let account_uuid = active_account.as_ref().map(|a| a.id.to_string());
|
||||
let account_id = active_account.as_ref().map(|a| a.id);
|
||||
let username = active_account.as_ref().map(|a| a.username.clone());
|
||||
|
||||
let status_msg = use_state(|| None::<(String, bool)>); // (message, is_error)
|
||||
let show_url_modal = use_state(|| false);
|
||||
let is_loading = use_state(|| false);
|
||||
|
|
@ -37,7 +41,12 @@ impl Component for AccountSkins {
|
|||
.overflow(Overflow::Clip)
|
||||
.padding(40.)
|
||||
.spacing(28.)
|
||||
.child(preview_panel(active_account.clone(), status_msg))
|
||||
.child(PreviewPanel {
|
||||
account_uuid,
|
||||
account_id,
|
||||
username,
|
||||
status_msg,
|
||||
})
|
||||
.child(side_panel(
|
||||
active_account,
|
||||
accounts,
|
||||
|
|
@ -46,168 +55,184 @@ impl Component for AccountSkins {
|
|||
show_url_modal,
|
||||
is_loading,
|
||||
))
|
||||
.maybe_child((*show_url_modal.read()).then(|| {
|
||||
url_modal(selected_id, status_msg, show_url_modal, is_loading)
|
||||
.maybe_child((*show_url_modal.read()).then(|| UrlModal {
|
||||
selected_id,
|
||||
status_msg,
|
||||
show_url_modal,
|
||||
is_loading,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn preview_panel(
|
||||
account: Option<MinecraftAccount>,
|
||||
#[derive(PartialEq, Clone)]
|
||||
struct PreviewPanel {
|
||||
account_uuid: Option<String>,
|
||||
account_id: Option<uuid::Uuid>,
|
||||
username: Option<String>,
|
||||
status_msg: State<Option<(String, bool)>>,
|
||||
) -> impl IntoElement {
|
||||
let account_uuid = account.as_ref().map(|a| a.id.to_string());
|
||||
let custom_query = use_custom_skin(account_uuid.clone().unwrap_or_default());
|
||||
let custom_data = crate::hooks::settled_or_loading(&custom_query);
|
||||
let is_slim = custom_data.as_ref().map_or(false, |d| d.is_slim);
|
||||
let has_custom = custom_data.as_ref().map_or(false, |d| d.has_custom);
|
||||
}
|
||||
|
||||
let acc_id_copy = account.as_ref().map(|a| a.id);
|
||||
let username_copy = account.as_ref().map(|a| a.username.clone());
|
||||
impl Component for PreviewPanel {
|
||||
fn render_key(&self) -> DiffKey {
|
||||
DiffKey::from(&self.account_id)
|
||||
}
|
||||
|
||||
let set_classic = {
|
||||
let acc_id = acc_id_copy;
|
||||
let uname = username_copy.clone();
|
||||
let mut status = status_msg;
|
||||
move |_| {
|
||||
let Some(id) = acc_id else { return };
|
||||
let uname = uname.clone();
|
||||
let uuid_str = id.to_string();
|
||||
spawn(async move {
|
||||
if let Ok(skin_path) = oneclient_common::paths::skin_file_path(&uuid_str) {
|
||||
if let Ok(bytes) = polyio::read(&skin_path).await {
|
||||
let _ = save_account_skin(&uuid_str, uname.as_deref(), &bytes, false).await;
|
||||
status.set(Some((
|
||||
"Model set to Classic (4px arms)".to_string(),
|
||||
false,
|
||||
)));
|
||||
} else if let Some(steve_bytes) = crate::AppAssets::get_bytes("steve.png") {
|
||||
let _ = save_account_skin(&uuid_str, uname.as_deref(), &steve_bytes, false)
|
||||
.await;
|
||||
status.set(Some(("Model set to Classic".to_string(), false)));
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let account_uuid = self.account_uuid.clone();
|
||||
let custom_query = use_custom_skin(account_uuid.clone().unwrap_or_default());
|
||||
let custom_data = crate::hooks::settled_or_loading(&custom_query);
|
||||
let is_slim = custom_data.as_ref().map_or(false, |d| d.is_slim);
|
||||
let has_custom = custom_data.as_ref().map_or(false, |d| d.has_custom);
|
||||
|
||||
let acc_id_copy = self.account_id;
|
||||
let username_copy = self.username.clone();
|
||||
let status_msg = self.status_msg;
|
||||
|
||||
let set_classic = {
|
||||
let acc_id = acc_id_copy;
|
||||
let uname = username_copy.clone();
|
||||
let mut status = status_msg;
|
||||
move |_| {
|
||||
let Some(id) = acc_id else { return };
|
||||
let uname = uname.clone();
|
||||
let uuid_str = id.to_string();
|
||||
spawn(async move {
|
||||
if let Ok(skin_path) = oneclient_common::paths::skin_file_path(&uuid_str) {
|
||||
if let Ok(bytes) = polyio::read(&skin_path).await {
|
||||
let _ = save_account_skin(&uuid_str, uname.as_deref(), &bytes, false)
|
||||
.await;
|
||||
status.set(Some((
|
||||
"Model set to Classic (4px arms)".to_string(),
|
||||
false,
|
||||
)));
|
||||
} else if let Some(steve_bytes) = crate::AppAssets::get_bytes("steve.png") {
|
||||
let _ =
|
||||
save_account_skin(&uuid_str, uname.as_deref(), &steve_bytes, false)
|
||||
.await;
|
||||
status.set(Some(("Model set to Classic".to_string(), false)));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let set_slim = {
|
||||
let acc_id = acc_id_copy;
|
||||
let uname = username_copy.clone();
|
||||
let mut status = status_msg;
|
||||
move |_| {
|
||||
let Some(id) = acc_id else { return };
|
||||
let uname = uname.clone();
|
||||
let uuid_str = id.to_string();
|
||||
spawn(async move {
|
||||
if let Ok(skin_path) = oneclient_common::paths::skin_file_path(&uuid_str) {
|
||||
if let Ok(bytes) = polyio::read(&skin_path).await {
|
||||
let _ = save_account_skin(&uuid_str, uname.as_deref(), &bytes, true).await;
|
||||
status.set(Some((
|
||||
"Model set to Slim (3px arms)".to_string(),
|
||||
false,
|
||||
)));
|
||||
} else if let Some(alex_bytes) = crate::AppAssets::get_bytes("alex.png") {
|
||||
let _ = save_account_skin(&uuid_str, uname.as_deref(), &alex_bytes, true)
|
||||
.await;
|
||||
status.set(Some(("Model set to Slim".to_string(), false)));
|
||||
let set_slim = {
|
||||
let acc_id = acc_id_copy;
|
||||
let uname = username_copy.clone();
|
||||
let mut status = status_msg;
|
||||
move |_| {
|
||||
let Some(id) = acc_id else { return };
|
||||
let uname = uname.clone();
|
||||
let uuid_str = id.to_string();
|
||||
spawn(async move {
|
||||
if let Ok(skin_path) = oneclient_common::paths::skin_file_path(&uuid_str) {
|
||||
if let Ok(bytes) = polyio::read(&skin_path).await {
|
||||
let _ =
|
||||
save_account_skin(&uuid_str, uname.as_deref(), &bytes, true).await;
|
||||
status.set(Some(("Model set to Slim (3px arms)".to_string(), false)));
|
||||
} else if let Some(alex_bytes) = crate::AppAssets::get_bytes("alex.png") {
|
||||
let _ =
|
||||
save_account_skin(&uuid_str, uname.as_deref(), &alex_bytes, true)
|
||||
.await;
|
||||
status.set(Some(("Model set to Slim".to_string(), false)));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
rect()
|
||||
.vertical()
|
||||
.width(Size::px(340.))
|
||||
.height(Size::fill())
|
||||
.spacing(16.)
|
||||
.child(
|
||||
rect()
|
||||
.center()
|
||||
.width(Size::fill())
|
||||
.height(Size::flex(1.0))
|
||||
.corner_radius(CornerRadius::new_all(16.))
|
||||
.background(colors::page_elevated())
|
||||
.border(border_all_color(1., colors::component_border()))
|
||||
.child(match account_uuid {
|
||||
Some(uuid) => PlayerModel::new(uuid)
|
||||
.width(Size::fill())
|
||||
.height(Size::fill())
|
||||
.into_element(),
|
||||
None => Icon::new(IconType::Users01)
|
||||
.size(64.)
|
||||
.color(colors::fg_secondary())
|
||||
.into_element(),
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
.main_align(Alignment::Center)
|
||||
.spacing(8.)
|
||||
.child(
|
||||
rect()
|
||||
.center()
|
||||
.padding(Gaps::new_symmetric(6., 14.))
|
||||
.corner_radius(CornerRadius::new_all(8.))
|
||||
.background(if !is_slim {
|
||||
colors::brand()
|
||||
} else {
|
||||
colors::component_bg()
|
||||
})
|
||||
.cursor(CursorIcon::Pointer)
|
||||
.on_press(set_classic)
|
||||
.child(
|
||||
label()
|
||||
.text("Classic (4px)")
|
||||
.font_size(12.)
|
||||
.font_weight(if !is_slim {
|
||||
FontWeight::SEMI_BOLD
|
||||
} else {
|
||||
FontWeight::NORMAL
|
||||
})
|
||||
.color(colors::fg_primary()),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.center()
|
||||
.padding(Gaps::new_symmetric(6., 14.))
|
||||
.corner_radius(CornerRadius::new_all(8.))
|
||||
.background(if is_slim {
|
||||
colors::brand()
|
||||
} else {
|
||||
colors::component_bg()
|
||||
})
|
||||
.cursor(CursorIcon::Pointer)
|
||||
.on_press(set_slim)
|
||||
.child(
|
||||
label()
|
||||
.text("Slim (3px)")
|
||||
.font_size(12.)
|
||||
.font_weight(if is_slim {
|
||||
FontWeight::SEMI_BOLD
|
||||
} else {
|
||||
FontWeight::NORMAL
|
||||
})
|
||||
.color(colors::fg_primary()),
|
||||
),
|
||||
),
|
||||
)
|
||||
.maybe_child(has_custom.then(|| {
|
||||
rect()
|
||||
.center()
|
||||
.child(
|
||||
label()
|
||||
.text("Custom Skin Active")
|
||||
.font_size(11.)
|
||||
.color(colors::success()),
|
||||
)
|
||||
.into_element()
|
||||
}))
|
||||
.into_element()
|
||||
rect()
|
||||
.vertical()
|
||||
.width(Size::px(340.))
|
||||
.height(Size::fill())
|
||||
.spacing(16.)
|
||||
.child(
|
||||
rect()
|
||||
.center()
|
||||
.width(Size::fill())
|
||||
.height(Size::flex(1.0))
|
||||
.corner_radius(CornerRadius::new_all(16.))
|
||||
.background(colors::page_elevated())
|
||||
.border(border_all_color(1., colors::component_border()))
|
||||
.child(match account_uuid {
|
||||
Some(uuid) => PlayerModel::new(uuid)
|
||||
.width(Size::fill())
|
||||
.height(Size::fill())
|
||||
.into_element(),
|
||||
None => Icon::new(IconType::Users01)
|
||||
.size(64.)
|
||||
.color(colors::fg_secondary())
|
||||
.into_element(),
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
.main_align(Alignment::Center)
|
||||
.spacing(8.)
|
||||
.child(
|
||||
rect()
|
||||
.center()
|
||||
.padding(Gaps::new_symmetric(6., 14.))
|
||||
.corner_radius(CornerRadius::new_all(8.))
|
||||
.background(if !is_slim {
|
||||
colors::brand()
|
||||
} else {
|
||||
colors::component_bg()
|
||||
})
|
||||
.cursor(CursorIcon::Pointer)
|
||||
.on_press(set_classic)
|
||||
.child(
|
||||
label()
|
||||
.text("Classic (4px)")
|
||||
.font_size(12.)
|
||||
.font_weight(if !is_slim {
|
||||
FontWeight::SEMI_BOLD
|
||||
} else {
|
||||
FontWeight::NORMAL
|
||||
})
|
||||
.color(colors::fg_primary()),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.center()
|
||||
.padding(Gaps::new_symmetric(6., 14.))
|
||||
.corner_radius(CornerRadius::new_all(8.))
|
||||
.background(if is_slim {
|
||||
colors::brand()
|
||||
} else {
|
||||
colors::component_bg()
|
||||
})
|
||||
.cursor(CursorIcon::Pointer)
|
||||
.on_press(set_slim)
|
||||
.child(
|
||||
label()
|
||||
.text("Slim (3px)")
|
||||
.font_size(12.)
|
||||
.font_weight(if is_slim {
|
||||
FontWeight::SEMI_BOLD
|
||||
} else {
|
||||
FontWeight::NORMAL
|
||||
})
|
||||
.color(colors::fg_primary()),
|
||||
),
|
||||
),
|
||||
)
|
||||
.maybe_child(has_custom.then(|| {
|
||||
rect()
|
||||
.center()
|
||||
.child(
|
||||
label()
|
||||
.text("Custom Skin Active")
|
||||
.font_size(11.)
|
||||
.color(colors::success()),
|
||||
)
|
||||
.into_element()
|
||||
}))
|
||||
.into_element()
|
||||
}
|
||||
}
|
||||
|
||||
fn side_panel(
|
||||
|
|
@ -302,7 +327,7 @@ fn side_panel(
|
|||
});
|
||||
};
|
||||
|
||||
// Remove / Reset skin
|
||||
// Reset skin to default
|
||||
let id_for_reset = account_id;
|
||||
let uname_for_reset = username_opt.clone();
|
||||
let mut status_reset = status_msg;
|
||||
|
|
@ -313,190 +338,176 @@ fn side_panel(
|
|||
let uuid_str = id.to_string();
|
||||
|
||||
spawn(async move {
|
||||
let _ = delete_account_skin(&uuid_str, uname.as_deref()).await;
|
||||
status_reset.set(Some(("Skin reset to default Steve/Alex".to_string(), false)));
|
||||
match delete_account_skin(&uuid_str, uname.as_deref()).await {
|
||||
Ok(()) => {
|
||||
status_reset
|
||||
.set(Some(("Skin reset to default skin.".to_string(), false)));
|
||||
}
|
||||
Err(err) => {
|
||||
status_reset.set(Some((err, true)));
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
let mut open_url_modal = show_url_modal;
|
||||
let open_from_url = move |_| {
|
||||
open_url_modal.set(true);
|
||||
};
|
||||
let mut open_url_dialog = show_url_modal;
|
||||
|
||||
rect()
|
||||
.vertical()
|
||||
.width(Size::flex(1.0))
|
||||
.height(Size::fill())
|
||||
.overflow(Overflow::Clip)
|
||||
.spacing(18.)
|
||||
.spacing(24.)
|
||||
.child(
|
||||
rect()
|
||||
.vertical()
|
||||
.spacing(4.)
|
||||
.child(
|
||||
label()
|
||||
.text("Skin Manager")
|
||||
.font_size(32.)
|
||||
.text("Skin Customizer")
|
||||
.font_size(24.)
|
||||
.font_weight(FontWeight::BOLD)
|
||||
.color(colors::fg_primary()),
|
||||
)
|
||||
.child(
|
||||
label()
|
||||
.text(format!("Managing skin for: {username}"))
|
||||
.font_size(14.)
|
||||
.text(format!("Managing skin for {username}"))
|
||||
.font_size(13.)
|
||||
.color(colors::fg_secondary()),
|
||||
),
|
||||
)
|
||||
.maybe_child((*status_msg.read()).as_ref().map(|(msg, is_err)| {
|
||||
// Status message notification banner
|
||||
.maybe_child(status_msg.read().as_ref().map(|(msg, is_err)| {
|
||||
let is_err = *is_err;
|
||||
rect()
|
||||
.horizontal()
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(8.)
|
||||
.padding(Gaps::new_symmetric(8., 12.))
|
||||
.width(Size::fill())
|
||||
.padding(Gaps::new_symmetric(10., 14.))
|
||||
.corner_radius(CornerRadius::new_all(8.))
|
||||
.background(if *is_err {
|
||||
.background(if is_err {
|
||||
colors::danger().with_a(30)
|
||||
} else {
|
||||
colors::success().with_a(30)
|
||||
})
|
||||
.border(border_all_color(
|
||||
1.,
|
||||
if *is_err {
|
||||
if is_err {
|
||||
colors::danger()
|
||||
} else {
|
||||
colors::success()
|
||||
},
|
||||
))
|
||||
.child(
|
||||
Icon::new(if *is_err {
|
||||
IconType::AlertTriangle
|
||||
} else {
|
||||
IconType::CheckCircle
|
||||
})
|
||||
.size(16.)
|
||||
.color(if *is_err {
|
||||
colors::danger()
|
||||
} else {
|
||||
colors::success()
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
label()
|
||||
.text(msg.clone())
|
||||
.font_size(13.)
|
||||
.color(colors::fg_primary()),
|
||||
.color(if is_err {
|
||||
colors::danger()
|
||||
} else {
|
||||
colors::success()
|
||||
}),
|
||||
)
|
||||
.into_element()
|
||||
}))
|
||||
// Account selector dropdown/buttons
|
||||
.child(account_selector(accounts, selected_id))
|
||||
// Action buttons
|
||||
.child(
|
||||
rect()
|
||||
.horizontal()
|
||||
.content(Content::wrap_spacing(8.))
|
||||
.spacing(8.)
|
||||
.vertical()
|
||||
.spacing(12.)
|
||||
.child(
|
||||
Button::new()
|
||||
.primary()
|
||||
.enabled(!*is_loading.read() && account_id.is_some())
|
||||
.on_press(import_from_file)
|
||||
.child(Icon::new(IconType::FilePlus02).size(16.))
|
||||
.text("Upload File (.png)"),
|
||||
)
|
||||
.child(
|
||||
Button::new()
|
||||
.secondary()
|
||||
.on_press(open_from_url)
|
||||
.child(Icon::new(IconType::Globe01).size(16.))
|
||||
.text("From Username / URL"),
|
||||
)
|
||||
.child(
|
||||
Button::new()
|
||||
.secondary()
|
||||
.on_press(download_skin)
|
||||
.child(Icon::new(IconType::Download01).size(16.))
|
||||
.text("Export Skin"),
|
||||
.child(
|
||||
rect()
|
||||
.horizontal()
|
||||
.spacing(8.)
|
||||
.cross_align(Alignment::Center)
|
||||
.child(Icon::new(IconType::FilePlus02).size(18.))
|
||||
.child(label().text("Upload PNG from Computer").font_size(14.)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new()
|
||||
.ghost()
|
||||
.on_press(reset_skin)
|
||||
.child(Icon::new(IconType::Trash01).size(16.))
|
||||
.text("Reset Skin"),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.vertical()
|
||||
.spacing(8.)
|
||||
.child(section_label("SWITCH ACCOUNT"))
|
||||
.enabled(!*is_loading.read() && account_id.is_some())
|
||||
.on_press(move |_| open_url_dialog.set(true))
|
||||
.child(
|
||||
rect()
|
||||
.horizontal()
|
||||
.spacing(8.)
|
||||
.cross_align(Alignment::Center)
|
||||
.child(Icon::new(IconType::Globe01).size(18.))
|
||||
.child(
|
||||
label()
|
||||
.text("Import by Username / URL")
|
||||
.font_size(14.),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.horizontal()
|
||||
.content(Content::wrap_spacing(8.))
|
||||
.spacing(8.)
|
||||
.children(accounts.into_iter().map(|acc| {
|
||||
let id = acc.id;
|
||||
let is_sel = selected_id.read().map_or(false, |sel| sel == id);
|
||||
let mut sel_state = selected_id;
|
||||
rect()
|
||||
.horizontal()
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(6.)
|
||||
.padding(Gaps::new_symmetric(6., 12.))
|
||||
.corner_radius(CornerRadius::new_all(8.))
|
||||
.background(if is_sel {
|
||||
colors::brand().with_a(50)
|
||||
} else {
|
||||
colors::page_elevated()
|
||||
})
|
||||
.border(border_all_color(
|
||||
1.,
|
||||
if is_sel {
|
||||
colors::brand()
|
||||
} else {
|
||||
colors::component_border()
|
||||
},
|
||||
))
|
||||
.cursor(CursorIcon::Pointer)
|
||||
.on_press(move |_| sel_state.set(Some(id)))
|
||||
.spacing(12.)
|
||||
.child(
|
||||
Button::new()
|
||||
.ghost()
|
||||
.enabled(!*is_loading.read() && account_id.is_some())
|
||||
.on_press(download_skin)
|
||||
.child(
|
||||
label()
|
||||
.text(acc.username)
|
||||
.font_size(13.)
|
||||
.color(colors::fg_primary()),
|
||||
)
|
||||
.into_element()
|
||||
})),
|
||||
rect()
|
||||
.horizontal()
|
||||
.spacing(8.)
|
||||
.cross_align(Alignment::Center)
|
||||
.child(Icon::new(IconType::Download01).size(16.))
|
||||
.child(label().text("Save PNG").font_size(13.)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new()
|
||||
.ghost()
|
||||
.enabled(!*is_loading.read() && account_id.is_some())
|
||||
.on_press(reset_skin)
|
||||
.child(
|
||||
rect()
|
||||
.horizontal()
|
||||
.spacing(8.)
|
||||
.cross_align(Alignment::Center)
|
||||
.child(
|
||||
Icon::new(IconType::Trash01)
|
||||
.size(16.)
|
||||
.color(colors::danger()),
|
||||
)
|
||||
.child(
|
||||
label()
|
||||
.text("Reset to Default")
|
||||
.font_size(13.)
|
||||
.color(colors::danger()),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
// Information note
|
||||
.child(
|
||||
rect()
|
||||
.vertical()
|
||||
.spacing(10.)
|
||||
.padding(Gaps::new_all(14.))
|
||||
.corner_radius(CornerRadius::new_all(10.))
|
||||
.background(colors::page_elevated())
|
||||
.border(border_all_color(1., colors::component_border()))
|
||||
.corner_radius(CornerRadius::new_all(8.))
|
||||
.background(colors::component_bg())
|
||||
.spacing(6.)
|
||||
.child(
|
||||
label()
|
||||
.text("💡 Offline Skin Tips")
|
||||
.font_size(14.)
|
||||
.text("Offline Skin Loader Integration")
|
||||
.font_size(13.)
|
||||
.font_weight(FontWeight::SEMI_BOLD)
|
||||
.color(colors::fg_primary()),
|
||||
)
|
||||
.child(
|
||||
label()
|
||||
.text("• Supports all standard Minecraft skins (64x64 or legacy 64x32 PNGs).")
|
||||
.font_size(12.)
|
||||
.color(colors::fg_secondary()),
|
||||
)
|
||||
.child(
|
||||
label()
|
||||
.text("• Interactive 3D preview: Click & drag on the character to rotate!")
|
||||
.font_size(12.)
|
||||
.color(colors::fg_secondary()),
|
||||
)
|
||||
.child(
|
||||
label()
|
||||
.text("• Skins automatically sync with CustomSkinLoader for in-game rendering on offline servers.")
|
||||
.text("Custom skins are saved locally to your profile and automatically synchronized to CustomSkinLoader so your skin shows up in game seamlessly on offline/cracked clusters.")
|
||||
.font_size(12.)
|
||||
.color(colors::fg_secondary()),
|
||||
),
|
||||
|
|
@ -504,107 +515,166 @@ fn side_panel(
|
|||
.into_element()
|
||||
}
|
||||
|
||||
fn url_modal(
|
||||
fn account_selector(
|
||||
accounts: Vec<MinecraftAccount>,
|
||||
selected_id: State<Option<uuid::Uuid>>,
|
||||
) -> impl IntoElement {
|
||||
let mut selected_state = selected_id;
|
||||
|
||||
rect()
|
||||
.vertical()
|
||||
.spacing(8.)
|
||||
.child(
|
||||
label()
|
||||
.text("SELECT ACCOUNT")
|
||||
.font_size(11.)
|
||||
.font_weight(FontWeight::BOLD)
|
||||
.color(colors::fg_secondary()),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.horizontal()
|
||||
.spacing(8.)
|
||||
.content(Content::Flex)
|
||||
.children(accounts.into_iter().map(|acc| {
|
||||
let id = acc.id;
|
||||
let uname = acc.username.clone();
|
||||
let is_active = *selected_state.read() == Some(id);
|
||||
|
||||
rect()
|
||||
.center()
|
||||
.padding(Gaps::new_symmetric(8., 16.))
|
||||
.corner_radius(CornerRadius::new_all(8.))
|
||||
.background(if is_active {
|
||||
colors::brand()
|
||||
} else {
|
||||
colors::component_bg()
|
||||
})
|
||||
.border(border_all_color(
|
||||
1.,
|
||||
if is_active {
|
||||
colors::brand()
|
||||
} else {
|
||||
colors::component_border()
|
||||
},
|
||||
))
|
||||
.cursor(CursorIcon::Pointer)
|
||||
.on_press(move |_| selected_state.set(Some(id)))
|
||||
.child(
|
||||
label()
|
||||
.text(uname)
|
||||
.font_size(13.)
|
||||
.font_weight(if is_active {
|
||||
FontWeight::SEMI_BOLD
|
||||
} else {
|
||||
FontWeight::NORMAL
|
||||
})
|
||||
.color(colors::fg_primary()),
|
||||
)
|
||||
.into_element()
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone)]
|
||||
struct UrlModal {
|
||||
selected_id: State<Option<uuid::Uuid>>,
|
||||
status_msg: State<Option<(String, bool)>>,
|
||||
show_url_modal: State<bool>,
|
||||
is_loading: State<bool>,
|
||||
) -> impl IntoElement {
|
||||
let input_text = use_state(String::new);
|
||||
let mut modal_open = show_url_modal;
|
||||
let mut status = status_msg;
|
||||
let mut loading = is_loading;
|
||||
}
|
||||
|
||||
let target_uuid = (*selected_id.peek()).map(|id| id.to_string());
|
||||
impl Component for UrlModal {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let input_text = use_state(String::new);
|
||||
let mut modal_open = self.show_url_modal;
|
||||
let mut status = self.status_msg;
|
||||
let mut loading = self.is_loading;
|
||||
|
||||
let apply_url = move |_| {
|
||||
let Some(ref uuid_str) = target_uuid else {
|
||||
return;
|
||||
};
|
||||
let uuid_str = uuid_str.clone();
|
||||
let query = input_text.peek().trim().to_string();
|
||||
if query.is_empty() {
|
||||
return;
|
||||
}
|
||||
let target_uuid = (*self.selected_id.peek()).map(|id| id.to_string());
|
||||
|
||||
loading.set(true);
|
||||
modal_open.set(false);
|
||||
|
||||
spawn(async move {
|
||||
match fetch_skin_online(&query).await {
|
||||
Ok((bytes, is_slim)) => {
|
||||
let _ = save_account_skin(&uuid_str, None, &bytes, is_slim).await;
|
||||
status.set(Some((
|
||||
format!("Skin for \"{query}\" applied successfully!"),
|
||||
false,
|
||||
)));
|
||||
}
|
||||
Err(err) => {
|
||||
status.set(Some((err, true)));
|
||||
}
|
||||
let apply_url = move |_| {
|
||||
let Some(ref uuid_str) = target_uuid else {
|
||||
return;
|
||||
};
|
||||
let uuid_str = uuid_str.clone();
|
||||
let query = input_text.peek().trim().to_string();
|
||||
if query.is_empty() {
|
||||
return;
|
||||
}
|
||||
loading.set(false);
|
||||
});
|
||||
};
|
||||
|
||||
OverlayPopup::new()
|
||||
.position(Position::new_global().top(140.).left(280.))
|
||||
.on_close(move |()| modal_open.set(false))
|
||||
.child(
|
||||
rect()
|
||||
.vertical()
|
||||
.width(Size::px(380.))
|
||||
.padding(Gaps::new_all(20.))
|
||||
.spacing(16.)
|
||||
.corner_radius(CornerRadius::new_all(12.))
|
||||
.background(colors::page_elevated())
|
||||
.border(border_all_color(1., colors::component_border()))
|
||||
.shadow(Shadow::from((0., 8., 32., 0., Color::from_argb(140, 0, 0, 0))))
|
||||
.child(
|
||||
label()
|
||||
.text("Import Skin by Username or URL")
|
||||
.font_size(16.)
|
||||
.font_weight(FontWeight::SEMI_BOLD)
|
||||
.color(colors::fg_primary()),
|
||||
)
|
||||
.child(
|
||||
label()
|
||||
.text("Enter any Minecraft player name (e.g. Technoblade, Notch) or a direct skin image URL:")
|
||||
.font_size(12.)
|
||||
.color(colors::fg_secondary()),
|
||||
)
|
||||
.child(
|
||||
TextInput::new(input_text)
|
||||
.placeholder("Username (e.g. Dream) or https://...")
|
||||
.width(Size::fill()),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.horizontal()
|
||||
.main_align(Alignment::End)
|
||||
.spacing(8.)
|
||||
.child(
|
||||
Button::new()
|
||||
.ghost()
|
||||
.on_press(move |_| modal_open.set(false))
|
||||
.text("Cancel"),
|
||||
)
|
||||
.child(
|
||||
Button::new()
|
||||
.primary()
|
||||
.on_press(apply_url)
|
||||
.text("Apply Skin"),
|
||||
),
|
||||
),
|
||||
)
|
||||
.into_element()
|
||||
}
|
||||
loading.set(true);
|
||||
modal_open.set(false);
|
||||
|
||||
fn section_label(text: &'static str) -> impl IntoElement {
|
||||
label()
|
||||
.text(text)
|
||||
.font_size(11.)
|
||||
.font_weight(FontWeight::SEMI_BOLD)
|
||||
.color(colors::fg_secondary())
|
||||
.into_element()
|
||||
spawn(async move {
|
||||
match fetch_skin_online(&query).await {
|
||||
Ok((bytes, is_slim)) => {
|
||||
let _ = save_account_skin(&uuid_str, None, &bytes, is_slim).await;
|
||||
status.set(Some((
|
||||
format!("Skin for \"{query}\" applied successfully!"),
|
||||
false,
|
||||
)));
|
||||
}
|
||||
Err(err) => {
|
||||
status.set(Some((err, true)));
|
||||
}
|
||||
}
|
||||
loading.set(false);
|
||||
});
|
||||
};
|
||||
|
||||
OverlayPopup::new()
|
||||
.position(Position::new_global().top(140.).left(280.))
|
||||
.on_close(move |()| modal_open.set(false))
|
||||
.child(
|
||||
rect()
|
||||
.vertical()
|
||||
.width(Size::px(380.))
|
||||
.padding(Gaps::new_all(20.))
|
||||
.spacing(16.)
|
||||
.corner_radius(CornerRadius::new_all(12.))
|
||||
.background(colors::page_elevated())
|
||||
.border(border_all_color(1., colors::component_border()))
|
||||
.shadow(Shadow::from((0., 8., 32., 0., Color::from_argb(140, 0, 0, 0))))
|
||||
.child(
|
||||
label()
|
||||
.text("Import Skin by Username or URL")
|
||||
.font_size(16.)
|
||||
.font_weight(FontWeight::SEMI_BOLD)
|
||||
.color(colors::fg_primary()),
|
||||
)
|
||||
.child(
|
||||
label()
|
||||
.text(
|
||||
"Enter any Minecraft player name (e.g. Technoblade, Notch) or a direct skin image URL:",
|
||||
)
|
||||
.font_size(12.)
|
||||
.color(colors::fg_secondary()),
|
||||
)
|
||||
.child(
|
||||
TextInput::new(input_text)
|
||||
.placeholder("Username (e.g. Dream) or https://...")
|
||||
.width(Size::fill()),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.horizontal()
|
||||
.main_align(Alignment::End)
|
||||
.spacing(8.)
|
||||
.child(
|
||||
Button::new()
|
||||
.ghost()
|
||||
.on_press(move |_| modal_open.set(false))
|
||||
.text("Cancel"),
|
||||
)
|
||||
.child(
|
||||
Button::new()
|
||||
.primary()
|
||||
.on_press(apply_url)
|
||||
.text("Apply Skin"),
|
||||
),
|
||||
),
|
||||
)
|
||||
.into_element()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue