feat: better versions UI (#890)
This commit is contained in:
parent
b2a62f8e17
commit
c9c4c9536b
26 changed files with 1414 additions and 1285 deletions
|
|
@ -1,45 +0,0 @@
|
|||
use freya::prelude::*;
|
||||
use oneclient_common::VersionKey;
|
||||
use oneclient_common::domain::GameLoader;
|
||||
|
||||
use crate::components::{ART_PREVIEW_EDGE, DynamicArt};
|
||||
use crate::theme::colors;
|
||||
use crate::ui::border_all_color;
|
||||
|
||||
#[derive(PartialEq, Clone)]
|
||||
pub struct ClusterLandscapeArt {
|
||||
art: DynamicArt,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
impl ClusterLandscapeArt {
|
||||
pub fn for_version(
|
||||
major: u32,
|
||||
key: Option<VersionKey>,
|
||||
loader: Option<GameLoader>,
|
||||
selected: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
art: DynamicArt::for_version(major, key, loader).max_edge(ART_PREVIEW_EDGE),
|
||||
selected,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for ClusterLandscapeArt {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let border = if self.selected {
|
||||
border_all_color(2., colors::brand())
|
||||
} else {
|
||||
border_all_color(2., colors::component_border())
|
||||
};
|
||||
|
||||
rect()
|
||||
.width(Size::fill())
|
||||
.height(Size::fill())
|
||||
.corner_radius(CornerRadius::new_all(12.))
|
||||
.overflow(Overflow::Clip)
|
||||
.border(border.alignment(BorderAlignment::Inner))
|
||||
.child(self.art.clone())
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ pub struct Dropdown {
|
|||
width: Size,
|
||||
height: Size,
|
||||
outlined: bool,
|
||||
checked: Option<Vec<bool>>,
|
||||
key: DiffKey,
|
||||
}
|
||||
|
||||
|
|
@ -31,6 +32,7 @@ impl Dropdown {
|
|||
width: Size::px(72.),
|
||||
height: Size::px(24.),
|
||||
outlined: false,
|
||||
checked: None,
|
||||
key: DiffKey::None,
|
||||
}
|
||||
}
|
||||
|
|
@ -52,6 +54,11 @@ impl Dropdown {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn checked(mut self, checked: Vec<bool>) -> Self {
|
||||
self.checked = Some(checked);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn leading(mut self, leading: impl IntoElement) -> Self {
|
||||
self.leading = Some(leading.into_element());
|
||||
self
|
||||
|
|
@ -78,19 +85,21 @@ impl Component for Dropdown {
|
|||
let focus = use_focus(a11y_id);
|
||||
|
||||
let mut button_area = use_state(|| None::<Area>);
|
||||
let mut list_size = use_state(|| None::<Size2D>);
|
||||
let mut list_area = use_state(|| None::<Area>);
|
||||
|
||||
let selected = self.selected.clone();
|
||||
let leading = self.leading.clone();
|
||||
let options = self.options.clone();
|
||||
let on_select = self.on_select.clone();
|
||||
let outlined = self.outlined;
|
||||
let checked = self.checked.clone();
|
||||
let multi = checked.is_some();
|
||||
let is_open = open();
|
||||
|
||||
use_overlay_claim_when(is_open);
|
||||
|
||||
if !is_open && list_size().is_some() {
|
||||
let _ = list_size.take();
|
||||
if !is_open && list_area().is_some() {
|
||||
let _ = list_area.take();
|
||||
}
|
||||
|
||||
let trigger_bg = if hovering() {
|
||||
|
|
@ -103,13 +112,13 @@ impl Component for Dropdown {
|
|||
let list_h =
|
||||
visible as f32 * OPTION_HEIGHT + visible.saturating_sub(1) as f32 * OPTION_SPACING;
|
||||
|
||||
let offset_y = match (button_area(), list_size()) {
|
||||
let offset_y = match (button_area(), list_area()) {
|
||||
(Some(button), Some(list)) => {
|
||||
let root_height = Platform::get().root_size.peek().height;
|
||||
let space_below = root_height - button.max_y();
|
||||
let space_above = button.min_y();
|
||||
if list.height > space_below && list.height <= space_above {
|
||||
-(button.height() + list.height + 8.)
|
||||
if list.height() > space_below && list.height() <= space_above {
|
||||
-(button.height() + list.height() + 8.)
|
||||
} else {
|
||||
0.
|
||||
}
|
||||
|
|
@ -121,8 +130,15 @@ impl Component for Dropdown {
|
|||
.map(|b| Size::px(b.width()))
|
||||
.unwrap_or_else(|| self.width.clone());
|
||||
|
||||
let on_global_pointer_up = move |_: Event<PointerEventData>| {
|
||||
open.set_if_modified(false);
|
||||
let on_global_pointer_up = move |e: Event<PointerEventData>| {
|
||||
let loc = e.global_location();
|
||||
let inside = list_area().is_some_and(|area| {
|
||||
area.translate((0., offset_y).into())
|
||||
.contains((loc.x as f32, loc.y as f32).into())
|
||||
});
|
||||
if !(multi && inside) {
|
||||
open.set_if_modified(false);
|
||||
}
|
||||
};
|
||||
|
||||
rect()
|
||||
|
|
@ -185,7 +201,7 @@ impl Component for Dropdown {
|
|||
.offset_y(offset_y)
|
||||
.content(Content::Fit)
|
||||
.on_sized(move |e: Event<SizedEventData>| {
|
||||
list_size.set_if_modified(Some(e.area.size));
|
||||
list_area.set_if_modified(Some(e.area));
|
||||
})
|
||||
.child(
|
||||
rect()
|
||||
|
|
@ -202,11 +218,16 @@ impl Component for Dropdown {
|
|||
.children(options.into_iter().enumerate().map(
|
||||
|(idx, option)| {
|
||||
let on_select = on_select.clone();
|
||||
DropdownOption::new(option, move |_| {
|
||||
let checked = checked
|
||||
.as_ref()
|
||||
.map(|c| c.get(idx).copied().unwrap_or(false));
|
||||
DropdownOption::new(option, checked, move |_| {
|
||||
if let Some(handler) = &on_select {
|
||||
handler.call(idx);
|
||||
}
|
||||
open.set(false);
|
||||
if !multi {
|
||||
open.set(false);
|
||||
}
|
||||
})
|
||||
.into_element()
|
||||
},
|
||||
|
|
@ -224,16 +245,19 @@ impl Component for Dropdown {
|
|||
|
||||
struct DropdownOption {
|
||||
text: String,
|
||||
checked: Option<bool>,
|
||||
on_press: EventHandler<Event<PressEventData>>,
|
||||
}
|
||||
|
||||
impl DropdownOption {
|
||||
pub fn new(
|
||||
text: impl Into<String>,
|
||||
checked: Option<bool>,
|
||||
on_press: impl Into<EventHandler<Event<PressEventData>>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
text: text.into(),
|
||||
checked,
|
||||
on_press: on_press.into(),
|
||||
}
|
||||
}
|
||||
|
|
@ -241,7 +265,7 @@ impl DropdownOption {
|
|||
|
||||
impl PartialEq for DropdownOption {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.text == other.text
|
||||
self.text == other.text && self.checked == other.checked
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -254,7 +278,8 @@ impl Component for DropdownOption {
|
|||
|
||||
rect()
|
||||
.width(Size::fill())
|
||||
.padding(Gaps::new_symmetric(6., 8.))
|
||||
.height(Size::px(OPTION_HEIGHT))
|
||||
.padding(Gaps::new_symmetric(0., 8.))
|
||||
.corner_radius(CornerRadius::new_all(6.))
|
||||
.a11y_id(a11y_id)
|
||||
.a11y_focusable(true)
|
||||
|
|
@ -267,11 +292,27 @@ impl Component for DropdownOption {
|
|||
.on_pointer_enter(move |_| hovering.set(true))
|
||||
.on_pointer_leave(move |_| hovering.set(false))
|
||||
.on_press(self.on_press.clone())
|
||||
.horizontal()
|
||||
.content(Content::Flex)
|
||||
.cross_align(Alignment::Center)
|
||||
.child(
|
||||
label()
|
||||
.text(self.text.clone())
|
||||
.width(Size::flex(1.))
|
||||
.font_size(12.)
|
||||
.color(colors::fg_primary()),
|
||||
)
|
||||
.maybe_child(self.checked.map(|checked| {
|
||||
rect()
|
||||
.width(Size::px(14.))
|
||||
.height(Size::px(14.))
|
||||
.maybe_child(checked.then(|| {
|
||||
Icon::new(IconType::Check)
|
||||
.size(14.)
|
||||
.color(colors::brand())
|
||||
.into_element()
|
||||
}))
|
||||
.into_element()
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use std::path::PathBuf;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use bytes::Bytes;
|
||||
use freya::animation::{
|
||||
|
|
@ -115,23 +116,14 @@ impl DynamicArt {
|
|||
|
||||
pub fn use_art_bytes(art: &DynamicArt) -> (String, Bytes) {
|
||||
let max_edge = art.max_edge;
|
||||
let fallback = use_bundled_art(false);
|
||||
use_resolved_art(art).unwrap_or_else(|| {
|
||||
(
|
||||
format!("{max_edge}|{HOME_BACKGROUND_ASSET}"),
|
||||
fallback.read().clone(),
|
||||
)
|
||||
})
|
||||
use_resolved_art(art)
|
||||
.unwrap_or_else(|| (format!("{max_edge}|{HOME_BACKGROUND_ASSET}"), bundled_art()))
|
||||
}
|
||||
|
||||
fn use_bundled_art(skip: bool) -> Memo<Bytes> {
|
||||
use_memo(move || {
|
||||
if skip {
|
||||
Bytes::new()
|
||||
} else {
|
||||
AppAssets::get_bytes(HOME_BACKGROUND_ASSET).unwrap_or_default()
|
||||
}
|
||||
})
|
||||
fn bundled_art() -> Bytes {
|
||||
static ART: LazyLock<Bytes> =
|
||||
LazyLock::new(|| AppAssets::get_bytes(HOME_BACKGROUND_ASSET).unwrap_or_default());
|
||||
ART.clone()
|
||||
}
|
||||
|
||||
fn use_resolved_art(art: &DynamicArt) -> Option<(String, Bytes)> {
|
||||
|
|
@ -180,7 +172,6 @@ fn use_resolved_art(art: &DynamicArt) -> Option<(String, Bytes)> {
|
|||
|
||||
impl Component for DynamicArt {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let fallback = use_bundled_art(self.skeleton);
|
||||
let resolved = use_resolved_art(self);
|
||||
let show_skeleton = self.skeleton && resolved.is_none();
|
||||
|
||||
|
|
@ -209,12 +200,8 @@ impl Component for DynamicArt {
|
|||
}
|
||||
|
||||
let max_edge = self.max_edge;
|
||||
let (key, bytes) = resolved.unwrap_or_else(|| {
|
||||
(
|
||||
format!("{max_edge}|{HOME_BACKGROUND_ASSET}"),
|
||||
fallback.read().clone(),
|
||||
)
|
||||
});
|
||||
let (key, bytes) = resolved
|
||||
.unwrap_or_else(|| (format!("{max_edge}|{HOME_BACKGROUND_ASSET}"), bundled_art()));
|
||||
|
||||
ImageViewer::new((key, bytes))
|
||||
.width(Size::fill())
|
||||
|
|
|
|||
245
packages/oneclient_app/src/components/filter_menu.rs
Normal file
245
packages/oneclient_app/src/components/filter_menu.rs
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
use freya::animation::{AnimNum, Ease, OnCreation, use_animation};
|
||||
use freya::prelude::*;
|
||||
|
||||
use crate::components::{Button, Icon, IconType};
|
||||
use crate::hooks::use_overlay_claim;
|
||||
use crate::theme::colors;
|
||||
|
||||
const PANEL_W: f32 = 172.;
|
||||
const BUTTON_W: f32 = 34.;
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct FilterOption {
|
||||
text: String,
|
||||
selected: bool,
|
||||
on_press: EventHandler<()>,
|
||||
}
|
||||
|
||||
impl FilterOption {
|
||||
pub fn new(
|
||||
text: impl Into<String>,
|
||||
selected: bool,
|
||||
on_press: impl Into<EventHandler<()>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
text: text.into(),
|
||||
selected,
|
||||
on_press: on_press.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct FilterSection {
|
||||
title: &'static str,
|
||||
options: Vec<FilterOption>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub struct FilterMenu {
|
||||
active: bool,
|
||||
sections: Vec<FilterSection>,
|
||||
}
|
||||
|
||||
impl FilterMenu {
|
||||
pub fn new(active: bool) -> Self {
|
||||
Self {
|
||||
active,
|
||||
sections: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn section(
|
||||
mut self,
|
||||
title: &'static str,
|
||||
options: impl IntoIterator<Item = FilterOption>,
|
||||
) -> Self {
|
||||
self.sections.push(FilterSection {
|
||||
title,
|
||||
options: options.into_iter().collect(),
|
||||
});
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for FilterMenu {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let mut open = use_state(|| false);
|
||||
|
||||
let icon_color = if self.active {
|
||||
colors::brand()
|
||||
} else {
|
||||
colors::fg_secondary()
|
||||
};
|
||||
|
||||
rect()
|
||||
.width(Size::px(BUTTON_W))
|
||||
.child(
|
||||
Button::new()
|
||||
.secondary()
|
||||
.icon()
|
||||
.tooltip("Sort and filter")
|
||||
.width(Size::px(BUTTON_W))
|
||||
.height(Size::px(34.))
|
||||
.on_press(move |e: Event<PressEventData>| {
|
||||
e.stop_propagation();
|
||||
open.toggle();
|
||||
})
|
||||
.child(Icon::new(IconType::Sliders04).size(16.).color(icon_color)),
|
||||
)
|
||||
.maybe_child(open().then(|| {
|
||||
FilterPanel {
|
||||
sections: self.sections.clone(),
|
||||
on_close: (move |()| open.set(false)).into(),
|
||||
}
|
||||
.into_element()
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct FilterPanel {
|
||||
sections: Vec<FilterSection>,
|
||||
on_close: EventHandler<()>,
|
||||
}
|
||||
|
||||
impl Component for FilterPanel {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
use_overlay_claim();
|
||||
|
||||
let backdrop_close = self.on_close.clone();
|
||||
let key_close = self.on_close.clone();
|
||||
|
||||
let a11y_id = use_a11y();
|
||||
|
||||
let fade = use_animation(|conf| {
|
||||
conf.on_creation(OnCreation::Run);
|
||||
AnimNum::new(0., 1.).time(160).ease(Ease::Out)
|
||||
});
|
||||
let progress = fade.read().value();
|
||||
|
||||
let mut panel = rect()
|
||||
.vertical()
|
||||
.width(Size::fill())
|
||||
.spacing(4.)
|
||||
.padding(Gaps::new_all(8.))
|
||||
.opacity(progress)
|
||||
.offset_y((progress - 1.0) * 6.)
|
||||
.corner_radius(CornerRadius::new_all(10.))
|
||||
.background(colors::page_elevated().with_a(230))
|
||||
.backdrop_blur(12.)
|
||||
.border(crate::ui::border_all_color(1., colors::component_border()))
|
||||
.shadow(Shadow::from((
|
||||
0.,
|
||||
8.,
|
||||
32.,
|
||||
0.,
|
||||
Color::from_argb(120, 0, 0, 0),
|
||||
)))
|
||||
.a11y_id(a11y_id)
|
||||
.a11y_role(AccessibilityRole::Dialog)
|
||||
.on_global_key_down(move |e: Event<KeyboardEventData>| {
|
||||
if e.key == Key::Named(NamedKey::Escape) {
|
||||
key_close.call(());
|
||||
}
|
||||
});
|
||||
|
||||
for section in &self.sections {
|
||||
panel = panel.child(section_label(section.title));
|
||||
for option in §ion.options {
|
||||
panel = panel.child(ChoiceRow {
|
||||
option: option.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
rect()
|
||||
.height(Size::px(0.))
|
||||
.width(Size::px(PANEL_W))
|
||||
.layer(Layer::Overlay)
|
||||
.child(
|
||||
rect()
|
||||
.layer(Layer::OverlayLevel(10))
|
||||
.position(Position::new_global().top(0.).left(0.))
|
||||
.width(Size::window_percent(100.))
|
||||
.height(Size::window_percent(100.))
|
||||
.on_press(move |_| backdrop_close.call(())),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.width(Size::fill())
|
||||
.layer(Layer::OverlayLevel(12))
|
||||
.margin(Gaps::new(6., 0., 0., -(PANEL_W - BUTTON_W)))
|
||||
.child(panel),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn section_label(text: &'static str) -> impl IntoElement {
|
||||
label()
|
||||
.text(text)
|
||||
.font_size(10.)
|
||||
.font_weight(FontWeight::SEMI_BOLD)
|
||||
.color(colors::fg_secondary())
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct ChoiceRow {
|
||||
option: FilterOption,
|
||||
}
|
||||
|
||||
impl Component for ChoiceRow {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let selected = self.option.selected;
|
||||
let on_press = self.option.on_press.clone();
|
||||
|
||||
let a11y_id = use_a11y();
|
||||
let focus = use_focus(a11y_id);
|
||||
let focused = focus().is_focused();
|
||||
|
||||
let color = if selected {
|
||||
colors::fg_primary()
|
||||
} else {
|
||||
colors::fg_secondary()
|
||||
};
|
||||
|
||||
let bg = if selected {
|
||||
colors::component_bg()
|
||||
} else if focused {
|
||||
colors::ghost_overlay_hover()
|
||||
} else {
|
||||
Color::TRANSPARENT
|
||||
};
|
||||
|
||||
rect()
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(8.)
|
||||
.padding(Gaps::new_symmetric(5., 8.))
|
||||
.corner_radius(CornerRadius::new_all(6.))
|
||||
.background(bg)
|
||||
.content(Content::Flex)
|
||||
.a11y_id(a11y_id)
|
||||
.a11y_focusable(true)
|
||||
.a11y_role(AccessibilityRole::MenuItemRadio)
|
||||
.maybe(focused, |el| {
|
||||
el.border(crate::ui::border_all_color(1., colors::brand()))
|
||||
})
|
||||
.cursor(CursorIcon::Pointer)
|
||||
.on_press(move |_| on_press.call(()))
|
||||
.child(
|
||||
label()
|
||||
.text(self.option.text.clone())
|
||||
.font_size(12.)
|
||||
.width(Size::flex(1.0))
|
||||
.color(color),
|
||||
)
|
||||
.maybe_child(selected.then(|| {
|
||||
Icon::new(IconType::Check)
|
||||
.size(14.)
|
||||
.color(colors::brand())
|
||||
.into_element()
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,152 +0,0 @@
|
|||
use freya::prelude::*;
|
||||
use oneclient_core::clusters::Cluster;
|
||||
|
||||
use crate::components::{ART_PREVIEW_EDGE, DynamicArt};
|
||||
use crate::theme::colors;
|
||||
use crate::ui::border_all_color;
|
||||
|
||||
const CARD_HEIGHT_PX: f32 = 240.;
|
||||
|
||||
pub struct InstanceCard {
|
||||
id: i64,
|
||||
title: String,
|
||||
tags: Vec<String>,
|
||||
art: DynamicArt,
|
||||
selected: bool,
|
||||
on_press: EventHandler<Event<PressEventData>>,
|
||||
}
|
||||
|
||||
impl InstanceCard {
|
||||
pub fn new(
|
||||
cluster: &Cluster,
|
||||
selected: bool,
|
||||
on_press: impl Into<EventHandler<Event<PressEventData>>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: cluster.id,
|
||||
title: cluster.name.clone(),
|
||||
tags: cluster.tags.clone(),
|
||||
art: DynamicArt::for_cluster(cluster).max_edge(ART_PREVIEW_EDGE),
|
||||
selected,
|
||||
on_press: on_press.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for InstanceCard {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
&& self.title == other.title
|
||||
&& self.tags == other.tags
|
||||
&& self.art == other.art
|
||||
&& self.selected == other.selected
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for InstanceCard {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let mut hovering = use_state(|| false);
|
||||
|
||||
let a11y_id = use_a11y();
|
||||
let focus = use_focus(a11y_id);
|
||||
|
||||
let selected = self.selected;
|
||||
let hovered = *hovering.read();
|
||||
let focused = focus().is_focused();
|
||||
let on_press = self.on_press.clone();
|
||||
|
||||
let opacity = if selected {
|
||||
1.0
|
||||
} else if hovered || focused {
|
||||
0.85
|
||||
} else {
|
||||
0.6
|
||||
};
|
||||
|
||||
let border_color = if selected || focused {
|
||||
colors::brand()
|
||||
} else if hovered {
|
||||
colors::component_border_hover()
|
||||
} else {
|
||||
colors::component_border()
|
||||
};
|
||||
|
||||
rect()
|
||||
.key(self.id)
|
||||
.width(Size::flex(1.0))
|
||||
.height(Size::px(CARD_HEIGHT_PX))
|
||||
.a11y_id(a11y_id)
|
||||
.a11y_focusable(true)
|
||||
.a11y_role(AccessibilityRole::Button)
|
||||
.on_press(move |e| on_press.call(e))
|
||||
.on_pointer_enter(move |_| {
|
||||
*hovering.write() = true;
|
||||
})
|
||||
.on_pointer_leave(move |_| {
|
||||
*hovering.write() = false;
|
||||
})
|
||||
.child(
|
||||
rect()
|
||||
.width(Size::fill())
|
||||
.height(Size::fill())
|
||||
.overflow(Overflow::Clip)
|
||||
.corner_radius(CornerRadius::new_all(12.))
|
||||
.opacity(opacity)
|
||||
.child(
|
||||
rect()
|
||||
.width(Size::fill())
|
||||
.height(Size::fill())
|
||||
.position(Position::new_absolute())
|
||||
.child(self.art.clone()),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.width(Size::fill())
|
||||
.height(Size::fill())
|
||||
.padding(Gaps::new_symmetric(12., 16.))
|
||||
.main_align(Alignment::SpaceBetween)
|
||||
.corner_radius(CornerRadius::new_all(12.))
|
||||
.cross_align(Alignment::Start)
|
||||
.border(
|
||||
border_all_color(1., border_color)
|
||||
.alignment(BorderAlignment::Inner),
|
||||
)
|
||||
.layer(Layer::Relative(3))
|
||||
.background(
|
||||
LinearGradient::new()
|
||||
.angle(0.)
|
||||
.stop((Color::from_af32rgb(0.8, 0, 0, 0), 0.))
|
||||
.stop((Color::from_af32rgb(0.3, 0, 0, 0), 20.))
|
||||
.stop((Color::from_af32rgb(0.3, 0, 0, 0), 60.))
|
||||
.stop((Color::from_af32rgb(0.8, 0, 0, 0), 100.)),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.horizontal()
|
||||
.spacing(8.)
|
||||
.children(self.tags.iter().map(|tag| {
|
||||
rect()
|
||||
.padding(Gaps::new_symmetric(4., 8.))
|
||||
.corner_radius(CornerRadius::new_all(999.))
|
||||
.background(colors::fg_primary())
|
||||
.child(
|
||||
label()
|
||||
.text(tag.clone())
|
||||
.font_size(12.)
|
||||
.font_weight(FontWeight::MEDIUM)
|
||||
.color(colors::brand()),
|
||||
)
|
||||
.into_element()
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
label()
|
||||
.text(self.title.clone())
|
||||
.font_size(28.)
|
||||
.font_weight(FontWeight::SEMI_BOLD)
|
||||
.color(colors::fg_primary()),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
135
packages/oneclient_app/src/components/instance_row.rs
Normal file
135
packages/oneclient_app/src/components/instance_row.rs
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
use freya::prelude::*;
|
||||
use oneclient_core::clusters::Cluster;
|
||||
|
||||
use crate::components::{ART_PREVIEW_EDGE, DynamicArt};
|
||||
use crate::theme::colors;
|
||||
use crate::ui::{border_all_color, last_played_label};
|
||||
|
||||
const ROW_HEIGHT_PX: f32 = 56.;
|
||||
const THUMB_PX: f32 = 40.;
|
||||
|
||||
pub struct InstanceRow {
|
||||
id: i64,
|
||||
title: String,
|
||||
subtitle: String,
|
||||
played: String,
|
||||
art: DynamicArt,
|
||||
selected: bool,
|
||||
on_press: EventHandler<Event<PressEventData>>,
|
||||
}
|
||||
|
||||
impl InstanceRow {
|
||||
pub fn new(
|
||||
cluster: &Cluster,
|
||||
selected: bool,
|
||||
on_press: impl Into<EventHandler<Event<PressEventData>>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: cluster.id,
|
||||
title: cluster.name.clone(),
|
||||
subtitle: format!("{} \u{b7} {}", cluster.mc_version, cluster.mc_loader),
|
||||
played: last_played_label(cluster.last_played),
|
||||
art: DynamicArt::for_cluster(cluster).max_edge(ART_PREVIEW_EDGE),
|
||||
selected,
|
||||
on_press: on_press.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for InstanceRow {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
&& self.title == other.title
|
||||
&& self.subtitle == other.subtitle
|
||||
&& self.played == other.played
|
||||
&& self.art == other.art
|
||||
&& self.selected == other.selected
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for InstanceRow {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let mut hovering = use_state(|| false);
|
||||
|
||||
let a11y_id = use_a11y();
|
||||
let focus = use_focus(a11y_id);
|
||||
|
||||
let selected = self.selected;
|
||||
let hovered = *hovering.read();
|
||||
let focused = focus().is_focused();
|
||||
let on_press = self.on_press.clone();
|
||||
|
||||
let background = if selected {
|
||||
colors::brand().with_a(36)
|
||||
} else if hovered {
|
||||
colors::component_bg_hover()
|
||||
} else {
|
||||
colors::component_bg()
|
||||
};
|
||||
|
||||
let border_color = if selected || focused {
|
||||
colors::brand()
|
||||
} else if hovered {
|
||||
colors::component_border_hover()
|
||||
} else {
|
||||
colors::component_border()
|
||||
};
|
||||
|
||||
rect()
|
||||
.key(self.id)
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
.height(Size::px(ROW_HEIGHT_PX))
|
||||
.padding(Gaps::new(0., 14., 0., 8.))
|
||||
.spacing(14.)
|
||||
.cross_align(Alignment::Center)
|
||||
.content(Content::Flex)
|
||||
.corner_radius(CornerRadius::new_all(12.))
|
||||
.background(background)
|
||||
.border(border_all_color(1., border_color).alignment(BorderAlignment::Inner))
|
||||
.a11y_id(a11y_id)
|
||||
.a11y_focusable(true)
|
||||
.a11y_role(AccessibilityRole::Button)
|
||||
.on_press(move |e| on_press.call(e))
|
||||
.on_pointer_enter(move |_| hovering.set(true))
|
||||
.on_pointer_leave(move |_| hovering.set(false))
|
||||
.child(
|
||||
rect()
|
||||
.width(Size::px(THUMB_PX))
|
||||
.height(Size::px(THUMB_PX))
|
||||
.corner_radius(CornerRadius::new_all(8.))
|
||||
.overflow(Overflow::Clip)
|
||||
.child(self.art.clone()),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.vertical()
|
||||
.width(Size::flex(1.0))
|
||||
.spacing(3.)
|
||||
.child(
|
||||
label()
|
||||
.text(self.title.clone())
|
||||
.font_size(14.)
|
||||
.font_weight(FontWeight::MEDIUM)
|
||||
.max_lines(1)
|
||||
.text_overflow(TextOverflow::Ellipsis)
|
||||
.color(colors::fg_primary()),
|
||||
)
|
||||
.child(
|
||||
label()
|
||||
.text(self.subtitle.clone())
|
||||
.font_size(12.)
|
||||
.max_lines(1)
|
||||
.text_overflow(TextOverflow::Ellipsis)
|
||||
.color(colors::fg_secondary()),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
label()
|
||||
.text(self.played.clone())
|
||||
.font_size(12.)
|
||||
.max_lines(1)
|
||||
.color(colors::fg_secondary()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ mod avatar;
|
|||
mod button;
|
||||
mod charts;
|
||||
mod checkbox;
|
||||
mod cluster_landscape_art;
|
||||
mod cluster_update_popup;
|
||||
mod context_menu;
|
||||
mod control_center;
|
||||
|
|
@ -14,10 +13,11 @@ mod dropdown;
|
|||
mod dynamic_art;
|
||||
mod essential_confirm;
|
||||
mod file_drop;
|
||||
mod filter_menu;
|
||||
mod flagged_install_popup;
|
||||
mod generic_prompt;
|
||||
mod icons;
|
||||
mod instance_card;
|
||||
mod instance_row;
|
||||
mod java_install_manager;
|
||||
mod java_prompt;
|
||||
mod link_confirm;
|
||||
|
|
@ -62,7 +62,6 @@ pub use avatar::Avatar;
|
|||
pub use button::{Button, ButtonSize, ButtonVariant, link_button, open_folder_button};
|
||||
pub use charts::{BarChart, PieChart, ValueUnit, slice_color};
|
||||
pub use checkbox::checkbox_labeled;
|
||||
pub use cluster_landscape_art::ClusterLandscapeArt;
|
||||
pub use cluster_update_popup::ClusterUpdatePopup;
|
||||
pub use context_menu::ContextMenu;
|
||||
pub use control_center::ControlCenter;
|
||||
|
|
@ -71,10 +70,11 @@ pub use dropdown::Dropdown;
|
|||
pub use dynamic_art::{ART_PREVIEW_EDGE, DynamicArt};
|
||||
pub use essential_confirm::EssentialConfirmOverlay;
|
||||
pub use file_drop::FileDropOverlay;
|
||||
pub use filter_menu::{FilterMenu, FilterOption};
|
||||
pub use flagged_install_popup::FlaggedInstallPopup;
|
||||
pub use generic_prompt::GenericPromptOverlay;
|
||||
pub use icons::{Icon, IconTint, IconType};
|
||||
pub use instance_card::InstanceCard;
|
||||
pub use instance_row::InstanceRow;
|
||||
pub use java_install_manager::JavaInstallManager;
|
||||
pub use java_prompt::JavaPromptOverlay;
|
||||
pub use link_confirm::ConfirmLinkOverlay;
|
||||
|
|
|
|||
|
|
@ -2,18 +2,19 @@ use freya::prelude::*;
|
|||
use oneclient_common::VersionKey;
|
||||
use oneclient_core::clusters::Cluster;
|
||||
|
||||
use crate::components::ClusterLandscapeArt;
|
||||
use crate::components::{ART_PREVIEW_EDGE, DynamicArt};
|
||||
use crate::theme::colors;
|
||||
use crate::ui::border_all_color;
|
||||
use crate::utils::{ReleaseLine, line_art_key, line_title, loader_tags};
|
||||
use crate::utils::{ReleaseLine, line_art_key, line_title};
|
||||
|
||||
const CARD_HEIGHT_PX: f32 = 240.;
|
||||
const CARD_HEIGHT_PX: f32 = 150.;
|
||||
|
||||
pub struct VersionCard {
|
||||
pub line: ReleaseLine,
|
||||
pub art_key: Option<VersionKey>,
|
||||
pub tags: Vec<String>,
|
||||
pub title: String,
|
||||
pub caption: String,
|
||||
pub count: usize,
|
||||
pub selected: bool,
|
||||
pub on_press: EventHandler<Event<PressEventData>>,
|
||||
}
|
||||
|
|
@ -22,14 +23,16 @@ impl VersionCard {
|
|||
pub fn new(
|
||||
line: ReleaseLine,
|
||||
clusters: &[Cluster],
|
||||
caption: String,
|
||||
selected: bool,
|
||||
on_press: impl Into<EventHandler<Event<PressEventData>>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
line,
|
||||
art_key: line_art_key(line, clusters),
|
||||
tags: loader_tags(clusters),
|
||||
title: line_title(line, clusters),
|
||||
caption,
|
||||
count: clusters.len(),
|
||||
selected,
|
||||
on_press: on_press.into(),
|
||||
}
|
||||
|
|
@ -40,8 +43,9 @@ impl PartialEq for VersionCard {
|
|||
fn eq(&self, other: &Self) -> bool {
|
||||
self.line == other.line
|
||||
&& self.art_key == other.art_key
|
||||
&& self.tags == other.tags
|
||||
&& self.title == other.title
|
||||
&& self.caption == other.caption
|
||||
&& self.count == other.count
|
||||
&& self.selected == other.selected
|
||||
}
|
||||
}
|
||||
|
|
@ -58,101 +62,94 @@ impl Component for VersionCard {
|
|||
let focused = focus().is_focused();
|
||||
let on_press = self.on_press.clone();
|
||||
|
||||
let opacity = if selected || hovered || focused {
|
||||
if selected { 1.0 } else { 0.85 }
|
||||
let art_opacity = if selected {
|
||||
1.0
|
||||
} else if hovered || focused {
|
||||
0.85
|
||||
} else {
|
||||
0.6
|
||||
};
|
||||
|
||||
let border_color = if selected || focused {
|
||||
colors::brand()
|
||||
let border = if selected || focused {
|
||||
border_all_color(2., colors::brand())
|
||||
} else if hovered {
|
||||
colors::component_border_hover()
|
||||
border_all_color(1., colors::component_border_hover())
|
||||
} else {
|
||||
colors::component_border()
|
||||
border_all_color(1., colors::component_border())
|
||||
};
|
||||
|
||||
rect()
|
||||
.key(self.line)
|
||||
.width(Size::flex(1.0))
|
||||
.width(Size::fill())
|
||||
.height(Size::px(CARD_HEIGHT_PX))
|
||||
.corner_radius(CornerRadius::new_all(16.))
|
||||
.overflow(Overflow::Clip)
|
||||
.background(colors::page_elevated())
|
||||
.a11y_id(a11y_id)
|
||||
.a11y_focusable(true)
|
||||
.a11y_role(AccessibilityRole::Button)
|
||||
.on_press(move |e| on_press.call(e))
|
||||
.on_pointer_enter(move |_| {
|
||||
*hovering.write() = true;
|
||||
})
|
||||
.on_pointer_leave(move |_| {
|
||||
*hovering.write() = false;
|
||||
})
|
||||
.on_pointer_enter(move |_| hovering.set(true))
|
||||
.on_pointer_leave(move |_| hovering.set(false))
|
||||
.child(
|
||||
rect()
|
||||
.width(Size::fill())
|
||||
.height(Size::fill())
|
||||
.overflow(Overflow::Clip)
|
||||
.corner_radius(CornerRadius::new_all(12.))
|
||||
.opacity(opacity)
|
||||
.position(Position::new_absolute())
|
||||
.opacity(art_opacity)
|
||||
.child(
|
||||
rect()
|
||||
.width(Size::fill())
|
||||
.height(Size::fill())
|
||||
.position(Position::new_absolute())
|
||||
.child(ClusterLandscapeArt::for_version(
|
||||
self.line.major,
|
||||
self.art_key,
|
||||
None,
|
||||
false,
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.width(Size::fill())
|
||||
.height(Size::fill())
|
||||
.padding(Gaps::new_symmetric(12., 16.))
|
||||
.main_align(Alignment::SpaceBetween)
|
||||
.corner_radius(CornerRadius::new_all(12.))
|
||||
.cross_align(Alignment::Start)
|
||||
.border(
|
||||
border_all_color(1., border_color)
|
||||
.alignment(BorderAlignment::Inner),
|
||||
)
|
||||
.layer(Layer::Relative(3))
|
||||
.background(
|
||||
LinearGradient::new()
|
||||
.angle(0.)
|
||||
.stop((Color::from_af32rgb(0.8, 0, 0, 0), 0.))
|
||||
.stop((Color::from_af32rgb(0.3, 0, 0, 0), 20.))
|
||||
.stop((Color::from_af32rgb(0.3, 0, 0, 0), 60.))
|
||||
.stop((Color::from_af32rgb(0.8, 0, 0, 0), 100.)),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.horizontal()
|
||||
.spacing(8.)
|
||||
.children(self.tags.iter().map(|tag| {
|
||||
rect()
|
||||
.padding(Gaps::new_symmetric(4., 8.))
|
||||
.corner_radius(CornerRadius::new_all(999.))
|
||||
.background(colors::fg_primary())
|
||||
.child(
|
||||
label()
|
||||
.text(tag.clone())
|
||||
.font_size(12.)
|
||||
.font_weight(FontWeight::MEDIUM)
|
||||
.color(colors::brand()),
|
||||
)
|
||||
.into_element()
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
label()
|
||||
.text(format!("Version {}", self.title))
|
||||
.font_size(32.)
|
||||
.font_weight(FontWeight::SEMI_BOLD)
|
||||
.color(colors::fg_primary()),
|
||||
),
|
||||
DynamicArt::for_version(self.line.major, self.art_key, None)
|
||||
.max_edge(ART_PREVIEW_EDGE),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.width(Size::fill())
|
||||
.height(Size::fill())
|
||||
.padding(12.)
|
||||
.spacing(5.)
|
||||
.main_align(Alignment::End)
|
||||
.corner_radius(CornerRadius::new_all(16.))
|
||||
.border(border.alignment(BorderAlignment::Inner))
|
||||
.layer(Layer::Relative(3))
|
||||
.background(
|
||||
LinearGradient::new()
|
||||
.angle(0.)
|
||||
.stop((Color::from_af32rgb(0.1, 11, 16, 19), 0.))
|
||||
.stop((Color::from_af32rgb(0.6, 11, 16, 19), 55.))
|
||||
.stop((Color::from_af32rgb(0.95, 11, 16, 19), 100.)),
|
||||
)
|
||||
.child(
|
||||
label()
|
||||
.text(self.title.clone())
|
||||
.font_size(22.)
|
||||
.font_weight(FontWeight::SEMI_BOLD)
|
||||
.color(Color::WHITE),
|
||||
)
|
||||
.child(
|
||||
label()
|
||||
.text(self.caption.clone())
|
||||
.font_size(11.)
|
||||
.max_lines(1)
|
||||
.text_overflow(TextOverflow::Ellipsis)
|
||||
.color(colors::fg_secondary()),
|
||||
),
|
||||
)
|
||||
.maybe_child((self.count > 1).then(|| {
|
||||
rect()
|
||||
.position(Position::new_absolute().top(8.).right(8.))
|
||||
.layer(Layer::Relative(4))
|
||||
.padding(Gaps::new_symmetric(4., 8.))
|
||||
.corner_radius(CornerRadius::new_all(6.))
|
||||
.background(Color::from_af32rgb(0.66, 11, 16, 19))
|
||||
.child(
|
||||
label()
|
||||
.text(format!("{} instances", self.count))
|
||||
.font_size(11.)
|
||||
.font_weight(FontWeight::MEDIUM)
|
||||
.color(colors::fg_primary()),
|
||||
)
|
||||
.into_element()
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ pub use queries::{
|
|||
CachedImageQuery, CancelMicrosoftLoginKeys, CancelMicrosoftLoginMutation, ClusterAction,
|
||||
ClusterBundles, ClusterLogsQuery, ClusterMutation, DisableWarningsQuery, DiscardLeftoversKeys,
|
||||
DiscardLeftoversMutation, FinishMicrosoftLoginMutation, GameVersion, JavaMajorsQuery,
|
||||
LeftoversQuery, LoaderVersionSet, LogAction, LogContentQuery, MigrationQuery,
|
||||
LeftoversQuery, LogAction, LogContentQuery, MigrationQuery,
|
||||
OnboardingBundlesQuery, RefreshAccountKeys, RemoveAccountKeys, ScreenshotAction,
|
||||
SetDefaultAccountKeys, StorageAction, StorageActionMutation, StorageReportQuery, TermsQuery,
|
||||
UploadLogKeys, UploadLogMutation, UseDiscardLeftovers, UseLogAction, UseRefreshAccount,
|
||||
|
|
@ -43,7 +43,7 @@ pub use queries::{
|
|||
invalidate_cluster_queries, invalidate_java_queries, invalidate_leftovers_queries,
|
||||
invalidate_logs_queries, invalidate_profile_queries, invalidate_screenshots_queries,
|
||||
invalidate_storage_queries, java_majors, java_runtimes, latest_changelog_version, loaded_image,
|
||||
loader_game_versions, loader_versions, login_code_already_handled, migration_detection,
|
||||
loader_versions, login_code_already_handled, migration_detection,
|
||||
mutation_error, mutation_is_pending, mutation_is_running, mutation_ok,
|
||||
onboarding_bundles_items, package_meta_batch, package_updates, pick_version_metadata,
|
||||
project_detail, provider_versions, query_error, query_is_busy, query_is_loading,
|
||||
|
|
@ -59,7 +59,7 @@ pub use queries::{
|
|||
use_cluster_profile, use_cluster_screenshots, use_cluster_settings, use_clusters,
|
||||
use_current_account, use_default_account, use_disable_warnings, use_discard_leftovers,
|
||||
use_finish_microsoft_login, use_game_profile, use_game_versions, use_global_analytics,
|
||||
use_java_majors, use_java_runtimes, use_leftovers, use_loader_game_versions,
|
||||
use_java_majors, use_java_runtimes, use_leftovers,
|
||||
use_loader_versions, use_local_image, use_log_action, use_log_content, use_migration,
|
||||
use_named_profiles, use_onboarding_bundles, use_package_categories, use_package_meta_batch,
|
||||
use_package_project, use_package_search, use_package_updates, use_package_versions,
|
||||
|
|
@ -67,7 +67,7 @@ pub use queries::{
|
|||
use_refresh_account, use_refresh_all_accounts, use_remove_account, use_screenshot_action,
|
||||
use_screenshot_folder_watch, use_set_default_account, use_storage_action, use_storage_report,
|
||||
use_terms, use_upload_log, use_version_art, use_version_art_gallery, use_version_loaders,
|
||||
use_version_metadata, use_versions, version_list, version_loaders, versions_metadata,
|
||||
use_version_metadata, use_versions, version_list, versions_metadata,
|
||||
versions_total,
|
||||
};
|
||||
pub use queries::{
|
||||
|
|
|
|||
|
|
@ -107,10 +107,10 @@ pub use version_metadata::{
|
|||
use_version_art_gallery, use_version_metadata,
|
||||
};
|
||||
pub use versions::{
|
||||
GameVersion, JavaMajorsQuery, LoaderVersionSet, game_versions, java_majors,
|
||||
loader_game_versions, loader_versions, use_game_versions, use_java_majors,
|
||||
use_loader_game_versions, use_loader_versions, use_version_loaders, use_versions,
|
||||
version_loaders, versions_metadata,
|
||||
GameVersion, JavaMajorsQuery, game_versions, java_majors,
|
||||
loader_versions, use_game_versions, use_java_majors,
|
||||
use_loader_versions, use_version_loaders, use_versions,
|
||||
versions_metadata,
|
||||
};
|
||||
pub use worlds::{
|
||||
add_world_datapacks, delete_world, delete_world_datapack, invalidate_world_contents,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use freya::query::{Query, QueryCapability, QueryStateData, UseQuery, use_query};
|
||||
|
|
@ -216,72 +216,6 @@ pub fn use_version_loaders(mc_version: String) -> UseQuery<VersionLoadersQuery>
|
|||
))
|
||||
}
|
||||
|
||||
pub fn version_loaders(query: &UseQuery<VersionLoadersQuery>) -> Vec<GameLoader> {
|
||||
super::state::settled_or_loading(query).unwrap_or_default()
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct LoaderGameVersionsQuery;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct LoaderGameVersionsKeys {
|
||||
pub loader: GameLoader,
|
||||
pub with_legacy: bool,
|
||||
}
|
||||
|
||||
pub type LoaderVersionSet = Option<Arc<HashSet<String>>>;
|
||||
|
||||
impl QueryCapability for LoaderGameVersionsQuery {
|
||||
type Ok = LoaderVersionSet;
|
||||
type Err = LauncherError;
|
||||
type Keys = LoaderGameVersionsKeys;
|
||||
|
||||
async fn run(&self, keys: &Self::Keys) -> Result<Self::Ok, Self::Err> {
|
||||
let state = crate::launcher::state()?;
|
||||
let keys = *keys;
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mc = state.services.mc();
|
||||
let mut metadata = state.metadata.lock().await;
|
||||
|
||||
let Some(mut ids) =
|
||||
oneclient_core::get_versions_for_loader(&mut metadata, &mc, keys.loader).await?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if keys.with_legacy
|
||||
&& let Some(legacy) =
|
||||
oneclient_core::get_versions_for_loader(&mut metadata, &mc, GameLoader::Ornithe)
|
||||
.await?
|
||||
{
|
||||
ids.extend(legacy);
|
||||
}
|
||||
|
||||
Ok(Some(Arc::new(ids.into_iter().collect::<HashSet<String>>())))
|
||||
})
|
||||
.await
|
||||
.map_err(|err| LauncherError::Minecraft(err.to_string()))?
|
||||
}
|
||||
}
|
||||
|
||||
pub fn use_loader_game_versions(
|
||||
loader: GameLoader,
|
||||
with_legacy: bool,
|
||||
) -> UseQuery<LoaderGameVersionsQuery> {
|
||||
use_query(Query::new(
|
||||
LoaderGameVersionsKeys {
|
||||
loader,
|
||||
with_legacy,
|
||||
},
|
||||
LoaderGameVersionsQuery,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn loader_game_versions(query: &UseQuery<LoaderGameVersionsQuery>) -> Option<LoaderVersionSet> {
|
||||
super::state::settled_or_loading(query)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct JavaMajorsQuery;
|
||||
|
||||
|
|
|
|||
|
|
@ -92,6 +92,21 @@ pub fn relative_time(created_at: Instant) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn last_played_label(ts: Option<chrono::DateTime<chrono::Utc>>) -> String {
|
||||
let Some(ts) = ts else {
|
||||
return "Never".to_string();
|
||||
};
|
||||
match (chrono::Utc::now() - ts).num_days() {
|
||||
..=0 => "Today".to_string(),
|
||||
1 => "Yesterday".to_string(),
|
||||
d @ 2..=6 => format!("{d} days ago"),
|
||||
d @ 7..=13 => format!("{} week ago", d / 7),
|
||||
d @ 14..=29 => format!("{} weeks ago", d / 7),
|
||||
d @ 30..=59 => format!("{} month ago", d / 30),
|
||||
d => format!("{} months ago", d / 30),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gap the clamped menu keeps from the window edges
|
||||
pub const EDGE_MARGIN: f32 = 8.;
|
||||
|
||||
|
|
|
|||
|
|
@ -113,20 +113,6 @@ pub fn group_clusters_by_release<'a>(
|
|||
groups
|
||||
}
|
||||
|
||||
pub fn loader_tags(clusters: &[Cluster]) -> Vec<String> {
|
||||
let mut tags = Vec::new();
|
||||
for cluster in clusters {
|
||||
if cluster.mc_loader.is_modded() {
|
||||
let label = cluster.mc_loader.to_string();
|
||||
if !tags.iter().any(|t| t == &label) {
|
||||
tags.push(label);
|
||||
}
|
||||
}
|
||||
}
|
||||
tags.sort();
|
||||
tags
|
||||
}
|
||||
|
||||
pub fn version_keys(clusters: &[Cluster]) -> Vec<VersionKey> {
|
||||
let mut keys = Vec::new();
|
||||
for cluster in clusters {
|
||||
|
|
|
|||
|
|
@ -67,26 +67,21 @@ impl Component for ClusterSettings {
|
|||
let versions = loader_versions(&versions_query);
|
||||
let runtimes = java_runtimes(&runtimes_query);
|
||||
|
||||
let instance_section: Vec<Element> = if cluster.user_created {
|
||||
vec![
|
||||
section_header("INSTANCE").into_element(),
|
||||
InstanceRow {
|
||||
facts: InstanceFacts {
|
||||
cluster_id,
|
||||
name: cluster.name.clone(),
|
||||
description: cluster.description.clone(),
|
||||
tags: cluster.tags.clone(),
|
||||
cover: cluster.cover_file(),
|
||||
mc_version: cluster.mc_version.clone(),
|
||||
mc_loader: cluster.mc_loader,
|
||||
kind: cluster.kind,
|
||||
},
|
||||
}
|
||||
.into_element(),
|
||||
]
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let instance_row = cluster.user_created.then(|| {
|
||||
InstanceRow {
|
||||
facts: InstanceFacts {
|
||||
cluster_id,
|
||||
name: cluster.name.clone(),
|
||||
description: cluster.description.clone(),
|
||||
tags: cluster.tags.clone(),
|
||||
cover: cluster.cover_file(),
|
||||
mc_version: cluster.mc_version.clone(),
|
||||
mc_loader: cluster.mc_loader,
|
||||
kind: cluster.kind,
|
||||
},
|
||||
}
|
||||
.into_element()
|
||||
});
|
||||
|
||||
let mod_loader = !cluster.lacks_mod_loader();
|
||||
let loader_section: Vec<Element> = if mod_loader {
|
||||
|
|
@ -97,7 +92,6 @@ impl Component for ClusterSettings {
|
|||
loader,
|
||||
selected: cluster.mc_loader_version.clone(),
|
||||
versions,
|
||||
locked: cluster.user_created,
|
||||
}
|
||||
.into_element(),
|
||||
]
|
||||
|
|
@ -117,7 +111,16 @@ impl Component for ClusterSettings {
|
|||
.height(Size::fill())
|
||||
.scrollbar_gutter(true)
|
||||
.spacing(4.)
|
||||
.children(instance_section)
|
||||
.child(section_header("INSTANCE"))
|
||||
.append_children(instance_row)
|
||||
.child(
|
||||
DedicatedDirRow {
|
||||
cluster_id,
|
||||
dedicated: cluster.uses_dedicated_dir(),
|
||||
locked: cluster.is_isolated(),
|
||||
}
|
||||
.into_element(),
|
||||
)
|
||||
.child(section_header("GAME"))
|
||||
.child(
|
||||
ToggleRow {
|
||||
|
|
@ -146,15 +149,6 @@ impl Component for ClusterSettings {
|
|||
.into_element(),
|
||||
)
|
||||
.append_children(loader_section)
|
||||
.child(section_header("DIRECTORY"))
|
||||
.child(
|
||||
DedicatedDirRow {
|
||||
cluster_id,
|
||||
dedicated: cluster.uses_dedicated_dir(),
|
||||
locked: cluster.is_isolated(),
|
||||
}
|
||||
.into_element(),
|
||||
)
|
||||
.child(section_header("SHORTCUT"))
|
||||
.child(ShortcutRow { cluster_id }.into_element())
|
||||
.child(section_header("JAVA"))
|
||||
|
|
@ -955,7 +949,6 @@ struct LoaderRow {
|
|||
loader: GameLoader,
|
||||
selected: Option<String>,
|
||||
versions: Arc<[String]>,
|
||||
locked: bool,
|
||||
}
|
||||
|
||||
impl Component for LoaderRow {
|
||||
|
|
@ -968,19 +961,6 @@ impl Component for LoaderRow {
|
|||
.clone()
|
||||
.unwrap_or_else(|| versions.first().cloned().unwrap_or_else(|| "Latest".into()));
|
||||
|
||||
if self.locked {
|
||||
return settings_row_disabled(
|
||||
IconType::Rocket02,
|
||||
"Loader Version",
|
||||
"Set when the instance was created.",
|
||||
label()
|
||||
.text(format!("{} {selected}", self.loader))
|
||||
.font_size(12.)
|
||||
.color(colors::fg_secondary()),
|
||||
)
|
||||
.into_element();
|
||||
}
|
||||
|
||||
let control: Element = if versions.is_empty() {
|
||||
label()
|
||||
.text("No versions available")
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
use super::*;
|
||||
|
||||
use freya::animation::{AnimNum, Ease, OnCreation, use_animation};
|
||||
use freya::router::RouterContext;
|
||||
use oneclient_content::packages::ContentType;
|
||||
use oneclient_core::settings::ViewLayout;
|
||||
|
||||
use crate::components::{
|
||||
Button, CardLayout, ChevronToggle, Icon, IconType, LazySection, PackageEntry, PackageRow,
|
||||
ScrollArea, Segment, SegmentedControl, TextInput, package_context_menu, use_shared_delete,
|
||||
Button, CardLayout, ChevronToggle, FilterMenu, FilterOption, Icon, IconType, LazySection,
|
||||
PackageEntry, PackageRow, ScrollArea, Segment, SegmentedControl, TextInput,
|
||||
package_context_menu, use_shared_delete,
|
||||
};
|
||||
use crate::hooks::{ClusterAction, use_cluster_mutation, use_dispatch, use_overlay_claim};
|
||||
use crate::hooks::{ClusterAction, use_cluster_mutation, use_dispatch};
|
||||
use crate::routes::Route;
|
||||
use crate::theme::colors;
|
||||
use crate::{Actions, utils};
|
||||
|
|
@ -120,8 +120,6 @@ impl HiddenFilter {
|
|||
}
|
||||
}
|
||||
|
||||
const FILTER_PANEL_W: f32 = 172.;
|
||||
const FILTER_BTN_W: f32 = 34.;
|
||||
const TOOLBAR_STACK_W: f32 = 640.;
|
||||
const TABS_ROW_H: f32 = 34.;
|
||||
|
||||
|
|
@ -199,13 +197,13 @@ pub(super) fn toolbar_bar(
|
|||
.into_element(),
|
||||
)
|
||||
.into_element(),
|
||||
FilterButton {
|
||||
filter_button(
|
||||
sort,
|
||||
current_sort,
|
||||
enabled_filter,
|
||||
hidden_filter,
|
||||
uses_bundles,
|
||||
}
|
||||
)
|
||||
.into_element(),
|
||||
];
|
||||
|
||||
|
|
@ -405,260 +403,50 @@ pub(crate) fn notice_bar(text: String) -> Element {
|
|||
.into_element()
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct FilterButton {
|
||||
sort: State<Option<String>>,
|
||||
fn filter_button(
|
||||
mut sort: State<Option<String>>,
|
||||
current_sort: SortMode,
|
||||
enabled_filter: State<EnabledFilter>,
|
||||
hidden_filter: State<HiddenFilter>,
|
||||
mut enabled_filter: State<EnabledFilter>,
|
||||
mut hidden_filter: State<HiddenFilter>,
|
||||
uses_bundles: bool,
|
||||
}
|
||||
) -> FilterMenu {
|
||||
let show = *enabled_filter.read();
|
||||
let hidden = *hidden_filter.read();
|
||||
|
||||
impl Component for FilterButton {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let mut open = use_state(|| false);
|
||||
// Hiding hidden packages is the default so it does not count as the filters being touched
|
||||
let active = current_sort != SortMode::NameAsc
|
||||
|| show != EnabledFilter::All
|
||||
|| hidden != HiddenFilter::Hide;
|
||||
|
||||
let sort = self.sort;
|
||||
let current_sort = self.current_sort;
|
||||
let enabled_filter = self.enabled_filter;
|
||||
let hidden_filter = self.hidden_filter;
|
||||
let uses_bundles = self.uses_bundles;
|
||||
|
||||
// Hiding hidden packages is the default so it does not count as the filters being touched
|
||||
let is_open = open();
|
||||
let active = current_sort != SortMode::NameAsc
|
||||
|| *enabled_filter.read() != EnabledFilter::All
|
||||
|| *hidden_filter.read() != HiddenFilter::Hide;
|
||||
|
||||
let icon_color = if active {
|
||||
colors::brand()
|
||||
} else {
|
||||
colors::fg_secondary()
|
||||
};
|
||||
|
||||
rect()
|
||||
.width(Size::px(FILTER_BTN_W))
|
||||
.child(
|
||||
Button::new()
|
||||
.secondary()
|
||||
.icon()
|
||||
.tooltip("Sort and filter")
|
||||
.width(Size::px(FILTER_BTN_W))
|
||||
.height(Size::px(34.))
|
||||
.on_press(move |e: Event<PressEventData>| {
|
||||
e.stop_propagation();
|
||||
open.toggle();
|
||||
})
|
||||
.child(Icon::new(IconType::Sliders04).size(16.).color(icon_color)),
|
||||
)
|
||||
.maybe_child(is_open.then(|| {
|
||||
let on_close: EventHandler<()> = (move |()| open.set(false)).into();
|
||||
FilterPopover {
|
||||
sort,
|
||||
current_sort,
|
||||
enabled_filter,
|
||||
hidden_filter,
|
||||
uses_bundles,
|
||||
on_close,
|
||||
}
|
||||
.into_element()
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct FilterPopover {
|
||||
sort: State<Option<String>>,
|
||||
current_sort: SortMode,
|
||||
enabled_filter: State<EnabledFilter>,
|
||||
hidden_filter: State<HiddenFilter>,
|
||||
uses_bundles: bool,
|
||||
on_close: EventHandler<()>,
|
||||
}
|
||||
|
||||
impl Component for FilterPopover {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
use_overlay_claim();
|
||||
|
||||
let mut sort = self.sort;
|
||||
let current_sort = self.current_sort;
|
||||
let mut enabled_filter = self.enabled_filter;
|
||||
let mut hidden_filter = self.hidden_filter;
|
||||
let backdrop_close = self.on_close.clone();
|
||||
let key_close = self.on_close.clone();
|
||||
|
||||
let a11y_id = use_a11y();
|
||||
|
||||
let fade = use_animation(|conf| {
|
||||
conf.on_creation(OnCreation::Run);
|
||||
AnimNum::new(0., 1.).time(160).ease(Ease::Out)
|
||||
});
|
||||
let progress = fade.read().value();
|
||||
|
||||
let show = *enabled_filter.read();
|
||||
let hidden = *hidden_filter.read();
|
||||
|
||||
let mut panel = rect()
|
||||
.vertical()
|
||||
.width(Size::fill())
|
||||
.spacing(4.)
|
||||
.padding(Gaps::new_all(8.))
|
||||
.opacity(progress)
|
||||
.offset_y((progress - 1.0) * 6.)
|
||||
.corner_radius(CornerRadius::new_all(10.))
|
||||
.background(colors::page_elevated().with_a(230))
|
||||
.backdrop_blur(12.)
|
||||
.border(crate::ui::border_all_color(1., colors::component_border()))
|
||||
.shadow(Shadow::from((
|
||||
0.,
|
||||
8.,
|
||||
32.,
|
||||
0.,
|
||||
Color::from_argb(120, 0, 0, 0),
|
||||
)))
|
||||
.a11y_id(a11y_id)
|
||||
.a11y_role(AccessibilityRole::Dialog)
|
||||
.on_global_key_down(move |e: Event<KeyboardEventData>| {
|
||||
if e.key == Key::Named(NamedKey::Escape) {
|
||||
key_close.call(());
|
||||
}
|
||||
})
|
||||
.child(section_label("Sort by"));
|
||||
|
||||
for mode in SortMode::ALL {
|
||||
let selected = mode == current_sort;
|
||||
let on_press: EventHandler<Event<PressEventData>> = (move |_| {
|
||||
sort.set(Some(mode.key().to_string()));
|
||||
})
|
||||
.into();
|
||||
panel = panel.child(ChoiceRow {
|
||||
text: mode.label(),
|
||||
selected,
|
||||
on_press,
|
||||
});
|
||||
}
|
||||
|
||||
panel = panel.child(section_label("Show"));
|
||||
for filter in EnabledFilter::ALL {
|
||||
let selected = filter == show;
|
||||
let on_press: EventHandler<Event<PressEventData>> = (move |_| {
|
||||
enabled_filter.set(filter);
|
||||
})
|
||||
.into();
|
||||
panel = panel.child(ChoiceRow {
|
||||
text: filter.label(),
|
||||
selected,
|
||||
on_press,
|
||||
});
|
||||
}
|
||||
|
||||
if self.uses_bundles {
|
||||
panel = panel.child(section_label("Hidden packages"));
|
||||
for filter in HiddenFilter::ALL {
|
||||
let selected = filter == hidden;
|
||||
let on_press: EventHandler<Event<PressEventData>> = (move |_| {
|
||||
hidden_filter.set(filter);
|
||||
let menu = FilterMenu::new(active)
|
||||
.section(
|
||||
"Sort by",
|
||||
SortMode::ALL.map(|mode| {
|
||||
FilterOption::new(mode.label(), mode == current_sort, move |()| {
|
||||
sort.set(Some(mode.key().to_string()));
|
||||
})
|
||||
.into();
|
||||
panel = panel.child(ChoiceRow {
|
||||
text: filter.label(),
|
||||
selected,
|
||||
on_press,
|
||||
});
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.section(
|
||||
"Show",
|
||||
EnabledFilter::ALL.map(|filter| {
|
||||
FilterOption::new(filter.label(), filter == show, move |()| {
|
||||
enabled_filter.set(filter);
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
rect()
|
||||
.height(Size::px(0.))
|
||||
.width(Size::px(FILTER_PANEL_W))
|
||||
.layer(Layer::Overlay)
|
||||
.child(
|
||||
rect()
|
||||
.layer(Layer::OverlayLevel(10))
|
||||
.position(Position::new_global().top(0.).left(0.))
|
||||
.width(Size::window_percent(100.))
|
||||
.height(Size::window_percent(100.))
|
||||
.on_press(move |_| backdrop_close.call(())),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.width(Size::fill())
|
||||
.layer(Layer::OverlayLevel(12))
|
||||
.margin(Gaps::new(6., 0., 0., -(FILTER_PANEL_W - FILTER_BTN_W)))
|
||||
.child(panel),
|
||||
)
|
||||
if !uses_bundles {
|
||||
return menu;
|
||||
}
|
||||
}
|
||||
|
||||
fn section_label(text: &'static str) -> impl IntoElement {
|
||||
label()
|
||||
.text(text)
|
||||
.font_size(10.)
|
||||
.font_weight(FontWeight::SEMI_BOLD)
|
||||
.color(colors::fg_secondary())
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct ChoiceRow {
|
||||
text: &'static str,
|
||||
selected: bool,
|
||||
on_press: EventHandler<Event<PressEventData>>,
|
||||
}
|
||||
|
||||
impl Component for ChoiceRow {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let text = self.text;
|
||||
let selected = self.selected;
|
||||
|
||||
let a11y_id = use_a11y();
|
||||
let focus = use_focus(a11y_id);
|
||||
let focused = focus().is_focused();
|
||||
|
||||
let color = if selected {
|
||||
colors::fg_primary()
|
||||
} else {
|
||||
colors::fg_secondary()
|
||||
};
|
||||
|
||||
let bg = if selected {
|
||||
colors::component_bg()
|
||||
} else if focused {
|
||||
colors::ghost_overlay_hover()
|
||||
} else {
|
||||
Color::TRANSPARENT
|
||||
};
|
||||
|
||||
rect()
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(8.)
|
||||
.padding(Gaps::new_symmetric(5., 8.))
|
||||
.corner_radius(CornerRadius::new_all(6.))
|
||||
.background(bg)
|
||||
.content(Content::Flex)
|
||||
.a11y_id(a11y_id)
|
||||
.a11y_focusable(true)
|
||||
.a11y_role(AccessibilityRole::MenuItemRadio)
|
||||
.maybe(focused, |el| {
|
||||
el.border(crate::ui::border_all_color(1., colors::brand()))
|
||||
menu.section(
|
||||
"Hidden packages",
|
||||
HiddenFilter::ALL.map(|filter| {
|
||||
FilterOption::new(filter.label(), filter == hidden, move |()| {
|
||||
hidden_filter.set(filter);
|
||||
})
|
||||
.cursor(CursorIcon::Pointer)
|
||||
.on_press(self.on_press.clone())
|
||||
.child(
|
||||
label()
|
||||
.text(text)
|
||||
.font_size(12.)
|
||||
.width(Size::flex(1.0))
|
||||
.color(color),
|
||||
)
|
||||
.maybe_child(selected.then(|| {
|
||||
Icon::new(IconType::Check)
|
||||
.size(14.)
|
||||
.color(colors::brand())
|
||||
.into_element()
|
||||
}))
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn add_from_file_button(
|
||||
|
|
|
|||
|
|
@ -11,8 +11,12 @@ pub fn heading(picks: &Picks) -> (&'static str, String) {
|
|||
),
|
||||
Step::Loader => (
|
||||
"Choose a mod loader",
|
||||
"Loaders are what packages install into. Pick one, then the Minecraft version it runs on."
|
||||
.to_string(),
|
||||
match &picks.versions.chosen {
|
||||
Some(version) => format!(
|
||||
"Loaders are what packages install into. Only the ones with a build for {version} can be picked."
|
||||
),
|
||||
None => "Loaders are what packages install into.".to_string(),
|
||||
},
|
||||
),
|
||||
Step::Version => (
|
||||
"Choose a version",
|
||||
|
|
@ -20,10 +24,10 @@ pub fn heading(picks: &Picks) -> (&'static str, String) {
|
|||
TypeChoice::OneClient => {
|
||||
"Only versions OneClient ships a build for are listed.".to_string()
|
||||
}
|
||||
TypeChoice::Scratch => format!(
|
||||
"Versions {} can run. Switch the release type to reach snapshots, betas and alphas.",
|
||||
picks.loader_label()
|
||||
),
|
||||
TypeChoice::Scratch => {
|
||||
"Every Minecraft version. Switch the release type to reach snapshots, betas and alphas."
|
||||
.to_string()
|
||||
}
|
||||
},
|
||||
),
|
||||
Step::Bundles => (
|
||||
|
|
@ -47,15 +51,13 @@ pub fn footer_note(picks: &Picks) -> String {
|
|||
}
|
||||
TypeChoice::Scratch => "Keeps its own game folder, worlds and packs.".to_string(),
|
||||
},
|
||||
Step::Loader => picks.loader_label(),
|
||||
Step::Version => match (&picks.versions.chosen, picks.loader.chosen) {
|
||||
(Some(version), Some(_)) => {
|
||||
format!("{version} downloads the first time you launch it.")
|
||||
}
|
||||
(Some(version), None) => {
|
||||
format!("{} has no build for {version}.", picks.loader_label())
|
||||
}
|
||||
(None, _) => "Pick a version to continue.".to_string(),
|
||||
Step::Loader => match (&picks.versions.chosen, picks.loader.chosen) {
|
||||
(Some(version), None) => format!("Pick a loader with a build for {version}."),
|
||||
_ => picks.loader_label(),
|
||||
},
|
||||
Step::Version => match &picks.versions.chosen {
|
||||
Some(version) => format!("{version} downloads the first time you launch it."),
|
||||
None => "Pick a version to continue.".to_string(),
|
||||
},
|
||||
Step::Bundles => {
|
||||
let taken = picks.bundles.taken_count();
|
||||
|
|
@ -100,8 +102,12 @@ pub fn step_value(wizard: Wizard, picks: &Picks, step: Step) -> String {
|
|||
|
||||
pub fn is_ready(picks: &Picks) -> bool {
|
||||
match picks.step {
|
||||
Step::Type | Step::Loader | Step::Bundles => true,
|
||||
Step::Version => picks.versions.chosen.is_some() && picks.loader.chosen.is_some(),
|
||||
Step::Type | Step::Bundles => true,
|
||||
Step::Loader => picks.loader.chosen.is_some(),
|
||||
Step::Version => {
|
||||
picks.versions.chosen.is_some()
|
||||
&& (picks.choice == TypeChoice::Scratch || picks.loader.chosen.is_some())
|
||||
}
|
||||
Step::Customize => validate_instance_name(&picks.name).is_ok(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use std::collections::HashSet;
|
||||
|
||||
use freya::prelude::*;
|
||||
use oneclient_core::GameVersionKind;
|
||||
use oneclient_core::clusters::ClusterKind;
|
||||
|
||||
use super::copy::{footer_note, heading, is_ready, step_value};
|
||||
|
|
@ -112,7 +113,7 @@ impl Component for CreateInstanceModal {
|
|||
step: use_state(|| 0usize),
|
||||
choice: use_state(|| TypeChoice::OneClient),
|
||||
version: use_state(|| None::<String>),
|
||||
filter: use_state(|| 0usize),
|
||||
filter: use_state(|| kind_bit(GameVersionKind::Release)),
|
||||
query: use_state(String::new),
|
||||
loader: use_state(|| LoaderChoice::Fabric),
|
||||
loader_version: use_state(|| None::<String>),
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@ use oneclient_core::{BundleArchive, GameVersionKind};
|
|||
use super::model::*;
|
||||
use super::package_names;
|
||||
use crate::hooks::{
|
||||
GameVersion, LoaderVersionSet, available_bundles, game_versions, java_majors,
|
||||
loader_game_versions, loader_versions, package_meta_batch, query_error, use_available_bundles,
|
||||
use_game_versions, use_java_majors, use_loader_game_versions, use_loader_versions,
|
||||
use_package_meta_batch, use_version_loaders, use_versions, version_loaders, versions_metadata,
|
||||
GameVersion, available_bundles, game_versions, java_majors, loader_versions,
|
||||
package_meta_batch, query_error, settled_or_loading, use_available_bundles, use_game_versions,
|
||||
use_java_majors, use_loader_versions, use_package_meta_batch, use_version_loaders,
|
||||
use_versions, versions_metadata,
|
||||
};
|
||||
use crate::utils::version_sort_key;
|
||||
|
||||
|
|
@ -22,11 +22,12 @@ pub struct VersionPicks {
|
|||
pub list: VersionList,
|
||||
pub loaded: bool,
|
||||
pub error: Option<String>,
|
||||
pub filter: usize,
|
||||
pub filter: u8,
|
||||
}
|
||||
|
||||
pub struct LoaderPicks {
|
||||
pub chosen: Option<GameLoader>,
|
||||
pub available: Option<Vec<GameLoader>>,
|
||||
pub version: Option<String>,
|
||||
pub versions: Arc<[String]>,
|
||||
}
|
||||
|
|
@ -82,14 +83,8 @@ impl Picks {
|
|||
}
|
||||
}
|
||||
|
||||
fn matches_filter(kind: GameVersionKind, filter: usize) -> bool {
|
||||
match filter {
|
||||
0 => kind == GameVersionKind::Release,
|
||||
1 => kind == GameVersionKind::Snapshot,
|
||||
2 => kind == GameVersionKind::Beta,
|
||||
3 => kind == GameVersionKind::Alpha,
|
||||
_ => true,
|
||||
}
|
||||
fn matches_filter(kind: GameVersionKind, filter: u8) -> bool {
|
||||
filter & kind_bit(kind) != 0
|
||||
}
|
||||
|
||||
struct OneClientVersion {
|
||||
|
|
@ -161,35 +156,6 @@ fn use_catalogue(choice: TypeChoice) -> Catalogue {
|
|||
}
|
||||
}
|
||||
|
||||
struct LoaderScope {
|
||||
choice: LoaderChoice,
|
||||
allowed: Option<LoaderVersionSet>,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
impl LoaderScope {
|
||||
fn settled(&self) -> bool {
|
||||
self.allowed.is_some()
|
||||
}
|
||||
|
||||
fn permits(&self, id: &str) -> bool {
|
||||
match &self.allowed {
|
||||
Some(Some(ids)) => ids.contains(id),
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn use_loader_scope(choice: LoaderChoice) -> LoaderScope {
|
||||
let query = use_loader_game_versions(choice.primary(), choice == LoaderChoice::Fabric);
|
||||
|
||||
LoaderScope {
|
||||
choice,
|
||||
allowed: loader_game_versions(&query),
|
||||
error: query_error(&query),
|
||||
}
|
||||
}
|
||||
|
||||
struct BundleContext {
|
||||
archives: Arc<[BundleArchive]>,
|
||||
settled: bool,
|
||||
|
|
@ -242,8 +208,7 @@ fn use_bundle_context(
|
|||
fn build_version_list(
|
||||
choice: TypeChoice,
|
||||
catalogue: &Catalogue,
|
||||
scope: &LoaderScope,
|
||||
filter: usize,
|
||||
filter: u8,
|
||||
needle: &str,
|
||||
) -> VersionList {
|
||||
let matches_needle = |id: &str| needle.is_empty() || id.to_lowercase().contains(needle);
|
||||
|
|
@ -271,7 +236,7 @@ fn build_version_list(
|
|||
let visible = all
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, entry)| in_scope(entry, scope, filter))
|
||||
.filter(|(_, entry)| matches_filter(entry.kind, filter))
|
||||
.filter(|(_, entry)| matches_needle(&entry.id))
|
||||
.map(|(index, _)| index as u32)
|
||||
.collect();
|
||||
|
|
@ -279,22 +244,15 @@ fn build_version_list(
|
|||
VersionList::Catalogue { all, visible }
|
||||
}
|
||||
|
||||
fn in_scope(entry: &GameVersion, scope: &LoaderScope, filter: usize) -> bool {
|
||||
matches_filter(entry.kind, filter) && scope.permits(&entry.id)
|
||||
}
|
||||
|
||||
fn default_version(
|
||||
choice: TypeChoice,
|
||||
catalogue: &Catalogue,
|
||||
scope: &LoaderScope,
|
||||
filter: usize,
|
||||
) -> Option<String> {
|
||||
fn default_version(choice: TypeChoice, catalogue: &Catalogue, filter: u8) -> Option<String> {
|
||||
if choice == TypeChoice::OneClient {
|
||||
return catalogue.oneclient.first().map(|entry| entry.id.clone());
|
||||
}
|
||||
|
||||
let all = catalogue.vanilla.as_deref().unwrap_or(&[]);
|
||||
let mut scoped = all.iter().filter(|entry| in_scope(entry, scope, filter));
|
||||
let mut scoped = all
|
||||
.iter()
|
||||
.filter(|entry| matches_filter(entry.kind, filter));
|
||||
let first = scoped.clone().next();
|
||||
scoped
|
||||
.find(|entry| entry.kind.is_release())
|
||||
|
|
@ -345,19 +303,18 @@ pub fn resolve(w: Wizard) -> Picks {
|
|||
let step = steps[index];
|
||||
|
||||
let catalogue = use_catalogue(choice);
|
||||
let scope = use_loader_scope(*w.loader.read());
|
||||
|
||||
let filter = *w.filter.read();
|
||||
let needle = w.query.read().trim().to_lowercase();
|
||||
let versions = build_version_list(choice, &catalogue, &scope, filter, &needle);
|
||||
let versions = build_version_list(choice, &catalogue, filter, &needle);
|
||||
|
||||
let oneclient = choice == TypeChoice::OneClient;
|
||||
let versions_settled = if oneclient {
|
||||
catalogue.oneclient_settled
|
||||
} else {
|
||||
catalogue.vanilla.is_some() && scope.settled()
|
||||
catalogue.vanilla.is_some()
|
||||
};
|
||||
let versions_error = catalogue.error.clone().or_else(|| scope.error.clone());
|
||||
let versions_error = catalogue.error.clone();
|
||||
|
||||
let still_offered = |chosen: &String| {
|
||||
if oneclient {
|
||||
|
|
@ -367,17 +324,20 @@ pub fn resolve(w: Wizard) -> Picks {
|
|||
.vanilla
|
||||
.iter()
|
||||
.flat_map(|all| all.iter())
|
||||
.any(|entry: &GameVersion| &entry.id == chosen && in_scope(entry, &scope, filter))
|
||||
.any(|entry: &GameVersion| {
|
||||
&entry.id == chosen && matches_filter(entry.kind, filter)
|
||||
})
|
||||
}
|
||||
};
|
||||
let version = (w.version.read().clone())
|
||||
.filter(still_offered)
|
||||
.or_else(|| default_version(choice, &catalogue, &scope, filter));
|
||||
.or_else(|| default_version(choice, &catalogue, filter));
|
||||
|
||||
let available = version_loaders(&use_version_loaders(version.clone().unwrap_or_default()));
|
||||
let available_query = use_version_loaders(version.clone().unwrap_or_default());
|
||||
let available = settled_or_loading(&available_query);
|
||||
let loader = match choice {
|
||||
TypeChoice::OneClient => version.as_ref().and_then(|id| catalogue.loader_for(id)),
|
||||
TypeChoice::Scratch => scope.choice.resolve(&available),
|
||||
TypeChoice::Scratch => w.loader.read().resolve(available.as_deref().unwrap_or(&[])),
|
||||
};
|
||||
|
||||
let loader_versions = loader_versions(&use_loader_versions(
|
||||
|
|
@ -423,6 +383,7 @@ pub fn resolve(w: Wizard) -> Picks {
|
|||
},
|
||||
loader: LoaderPicks {
|
||||
chosen: loader,
|
||||
available,
|
||||
version: loader_version,
|
||||
versions: loader_versions,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ use oneclient_cluster::naming::{
|
|||
};
|
||||
|
||||
use crate::components::{
|
||||
ART_PREVIEW_EDGE, Button, GALLERY_COVER_EDGE, Icon, IconType, LocalImage, TextInput,
|
||||
VersionArtGallery,
|
||||
ART_PREVIEW_EDGE, Button, DynamicArt, GALLERY_COVER_EDGE, Icon, IconType, LocalImage,
|
||||
TextInput, VersionArtGallery,
|
||||
};
|
||||
use crate::theme::colors;
|
||||
use crate::ui::border_all_color;
|
||||
|
|
@ -107,6 +107,7 @@ pub fn details_body(
|
|||
state: DetailsState,
|
||||
placeholder: String,
|
||||
existing_cover: Option<PathBuf>,
|
||||
version_art: DynamicArt,
|
||||
name_problem: Option<NameProblem>,
|
||||
) -> Element {
|
||||
let mut touched = state.name_touched;
|
||||
|
|
@ -158,7 +159,10 @@ pub fn details_body(
|
|||
.into_element(),
|
||||
))
|
||||
.child(field("Tags", tag_field(state, &tags, tag_open)))
|
||||
.child(field("Background image", cover_field(state, preview)))
|
||||
.child(field(
|
||||
"Background image",
|
||||
cover_field(state, preview, version_art),
|
||||
))
|
||||
.into_element()
|
||||
}
|
||||
|
||||
|
|
@ -405,7 +409,11 @@ fn chosen_tags(state: DetailsState, tags: &[String], open: bool) -> Element {
|
|||
.into_element()
|
||||
}
|
||||
|
||||
fn cover_field(state: DetailsState, preview: Option<(PathBuf, bool)>) -> Element {
|
||||
fn cover_field(
|
||||
state: DetailsState,
|
||||
preview: Option<(PathBuf, bool)>,
|
||||
version_art: DynamicArt,
|
||||
) -> Element {
|
||||
let mut cover = state.cover;
|
||||
let mut cleared = state.cover_cleared;
|
||||
|
||||
|
|
@ -504,11 +512,7 @@ fn cover_field(state: DetailsState, preview: Option<(PathBuf, bool)>) -> Element
|
|||
.picked(picked)
|
||||
.skeleton(true)
|
||||
.into_element(),
|
||||
None => label()
|
||||
.text("Version art")
|
||||
.font_size(11.)
|
||||
.color(colors::fg_secondary())
|
||||
.into_element(),
|
||||
None => version_art.max_edge(ART_PREVIEW_EDGE).into_element(),
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
|
|
|
|||
|
|
@ -116,7 +116,13 @@ impl Component for EditInstanceModal {
|
|||
title: "Edit the instance".to_string(),
|
||||
subtitle: "Only how it is presented changes here. The version, loader and installed content stay as they are."
|
||||
.to_string(),
|
||||
body: details_body(state, self.facts.name.clone(), existing, name_problem),
|
||||
body: details_body(
|
||||
state,
|
||||
self.facts.name.clone(),
|
||||
existing,
|
||||
version_art(Some(&self.facts.mc_version), Some(self.facts.mc_loader)),
|
||||
name_problem,
|
||||
),
|
||||
scrolls_itself: false,
|
||||
note: "Changes apply straight away.".to_string(),
|
||||
secondary_label: "Cancel".to_string(),
|
||||
|
|
|
|||
|
|
@ -3,11 +3,25 @@ use std::sync::Arc;
|
|||
|
||||
use freya::prelude::*;
|
||||
use oneclient_common::domain::GameLoader;
|
||||
use oneclient_core::GameVersionKind;
|
||||
|
||||
use super::details::DetailsState;
|
||||
use crate::hooks::GameVersion;
|
||||
|
||||
pub const FILTERS: [&str; 5] = ["Releases", "Snapshots", "Beta", "Alpha", "All versions"];
|
||||
pub const VERSION_KINDS: [(&str, GameVersionKind); 4] = [
|
||||
("Releases", GameVersionKind::Release),
|
||||
("Snapshots", GameVersionKind::Snapshot),
|
||||
("Beta", GameVersionKind::Beta),
|
||||
("Alpha", GameVersionKind::Alpha),
|
||||
];
|
||||
|
||||
pub fn kind_bit(kind: GameVersionKind) -> u8 {
|
||||
let index = VERSION_KINDS
|
||||
.iter()
|
||||
.position(|(_, k)| *k == kind)
|
||||
.unwrap_or(0);
|
||||
1 << index
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TypeChoice {
|
||||
|
|
@ -111,7 +125,7 @@ impl Step {
|
|||
}
|
||||
|
||||
const ONECLIENT_STEPS: [Step; 4] = [Step::Type, Step::Version, Step::Bundles, Step::Customize];
|
||||
const SCRATCH_STEPS: [Step; 4] = [Step::Type, Step::Loader, Step::Version, Step::Customize];
|
||||
const SCRATCH_STEPS: [Step; 4] = [Step::Type, Step::Version, Step::Loader, Step::Customize];
|
||||
|
||||
pub fn step_order(choice: TypeChoice) -> &'static [Step] {
|
||||
match choice {
|
||||
|
|
@ -170,7 +184,7 @@ pub struct Wizard {
|
|||
pub step: State<usize>,
|
||||
pub choice: State<TypeChoice>,
|
||||
pub version: State<Option<String>>,
|
||||
pub filter: State<usize>,
|
||||
pub filter: State<u8>,
|
||||
pub query: State<String>,
|
||||
pub loader: State<LoaderChoice>,
|
||||
pub loader_version: State<Option<String>>,
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ pub fn rail(parts: Rail) -> Element {
|
|||
.spacing(18.)
|
||||
.background(
|
||||
LinearGradient::new()
|
||||
.angle(180.)
|
||||
.angle(0.)
|
||||
.stop((Color::from_argb(77, 11, 16, 19), 0.))
|
||||
.stop((Color::from_argb(26, 11, 16, 19), 20.))
|
||||
.stop((Color::from_argb(204, 11, 16, 19), 56.))
|
||||
|
|
|
|||
|
|
@ -11,8 +11,9 @@ use super::cards::{
|
|||
};
|
||||
use super::data::Picks;
|
||||
use super::details::details_body;
|
||||
use super::model::FILTERS;
|
||||
use super::model::{LoaderChoice, Step, TypeChoice, Wizard};
|
||||
use super::model::{VERSION_KINDS, kind_bit};
|
||||
use super::rail::version_art;
|
||||
use crate::components::{
|
||||
Dropdown, GRID_GAP, Icon, IconType, ScrollArea, TextInput, centered_spinner,
|
||||
};
|
||||
|
|
@ -32,6 +33,7 @@ pub fn body(wizard: Wizard, picks: &Picks) -> Element {
|
|||
wizard.details,
|
||||
picks.suggested.clone(),
|
||||
None,
|
||||
version_art(picks.versions.chosen.as_deref(), picks.loader.chosen),
|
||||
validate_instance_name(&picks.name).err(),
|
||||
),
|
||||
),
|
||||
|
|
@ -99,7 +101,6 @@ fn type_step(mut wizard: Wizard, picks: &Picks) -> Element {
|
|||
fn select_loader(mut wizard: Wizard, choice: LoaderChoice) {
|
||||
wizard.loader.set(choice);
|
||||
wizard.loader_version.set(None);
|
||||
wizard.version.set(None);
|
||||
}
|
||||
|
||||
fn loader_step(wizard: Wizard, picks: &Picks) -> Element {
|
||||
|
|
@ -108,15 +109,28 @@ fn loader_step(wizard: Wizard, picks: &Picks) -> Element {
|
|||
let cards: Vec<Element> = LoaderChoice::ALL
|
||||
.into_iter()
|
||||
.map(|choice| {
|
||||
let offered = picks
|
||||
.loader
|
||||
.available
|
||||
.as_ref()
|
||||
.is_none_or(|available| choice.resolve(available).is_some());
|
||||
cell_card(CellCard {
|
||||
id: choice.name(),
|
||||
title: choice.name(),
|
||||
blurb: choice.blurb().to_string(),
|
||||
meta: None,
|
||||
meta: (!offered).then(|| {
|
||||
let version = picks.versions.chosen.as_deref();
|
||||
format!("No build for {}", version.unwrap_or("this version"))
|
||||
}),
|
||||
corner: loader_mark(choice, chosen == choice, LOADER_MARK_SIZE),
|
||||
selected: chosen == choice,
|
||||
selected: offered && chosen == choice,
|
||||
checkbox: false,
|
||||
on_press: (move |()| select_loader(wizard, choice)).into(),
|
||||
on_press: (move |()| {
|
||||
if offered {
|
||||
select_loader(wizard, choice);
|
||||
}
|
||||
})
|
||||
.into(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
|
@ -243,14 +257,26 @@ fn version_controls(mut wizard: Wizard, picks: &Picks) -> Element {
|
|||
)
|
||||
.child(
|
||||
Dropdown::new(
|
||||
FILTERS[filter.min(FILTERS.len() - 1)],
|
||||
FILTERS.iter().map(|f| (*f).to_string()).collect(),
|
||||
filter_summary(filter),
|
||||
VERSION_KINDS
|
||||
.iter()
|
||||
.map(|(name, _)| (*name).to_string())
|
||||
.collect(),
|
||||
)
|
||||
.checked(
|
||||
VERSION_KINDS
|
||||
.iter()
|
||||
.map(|(_, kind)| filter & kind_bit(*kind) != 0)
|
||||
.collect(),
|
||||
)
|
||||
.width(Size::px(168.))
|
||||
.height(Size::px(32.))
|
||||
.on_select(move |index: usize| {
|
||||
wizard.filter.set(index);
|
||||
wizard.version.set(None);
|
||||
if let Some((_, kind)) = VERSION_KINDS.get(index) {
|
||||
let mask = *wizard.filter.peek();
|
||||
wizard.filter.set(mask ^ kind_bit(*kind));
|
||||
wizard.version.set(None);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.into_element()
|
||||
|
|
@ -258,6 +284,20 @@ fn version_controls(mut wizard: Wizard, picks: &Picks) -> Element {
|
|||
.into_element()
|
||||
}
|
||||
|
||||
fn filter_summary(filter: u8) -> String {
|
||||
let picked: Vec<&str> = VERSION_KINDS
|
||||
.iter()
|
||||
.filter(|(_, kind)| filter & kind_bit(*kind) != 0)
|
||||
.map(|(name, _)| *name)
|
||||
.collect();
|
||||
match picked.as_slice() {
|
||||
[] => "None".to_string(),
|
||||
[only] => (*only).to_string(),
|
||||
all if all.len() == VERSION_KINDS.len() => "All versions".to_string(),
|
||||
[first, rest @ ..] => format!("{first} +{}", rest.len()),
|
||||
}
|
||||
}
|
||||
|
||||
fn version_list(mut wizard: Wizard, picks: &Picks) -> Element {
|
||||
if let Some(error) = &picks.versions.error {
|
||||
return centered_failure(error);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -73,9 +73,6 @@ pub enum ClusterError {
|
|||
#[error("invalid name: {0}")]
|
||||
InvalidName(crate::naming::NameProblem),
|
||||
|
||||
#[error("the loader of a custom instance cannot be changed after it is created")]
|
||||
LoaderLocked,
|
||||
|
||||
#[error("unknown loader id {0} in database")]
|
||||
InvalidLoader(i64),
|
||||
|
||||
|
|
|
|||
|
|
@ -185,9 +185,6 @@ impl ClusterManager {
|
|||
}
|
||||
|
||||
if existing.user_created {
|
||||
if matches!(update.mc_loader_version, Patch::Set(_)) {
|
||||
return Err(ClusterError::LoaderLocked);
|
||||
}
|
||||
if let Some(raw) = update.name.as_deref()
|
||||
&& raw.trim() != existing.name
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue