feat: added a picker for the amount of columns in a row (#784)
This commit is contained in:
parent
12783fe59a
commit
f55a86a024
13 changed files with 195 additions and 53 deletions
27
packages/oneclient_app/src/components/grid_columns.rs
Normal file
27
packages/oneclient_app/src/components/grid_columns.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
use freya::prelude::*;
|
||||
|
||||
use super::Dropdown;
|
||||
|
||||
pub const GRID_COLUMNS_MIN: u8 = 1;
|
||||
pub const GRID_COLUMNS_MAX: u8 = 5;
|
||||
|
||||
pub fn resolved_columns(columns: u8) -> usize {
|
||||
columns.clamp(GRID_COLUMNS_MIN, GRID_COLUMNS_MAX) as usize
|
||||
}
|
||||
|
||||
pub fn grid_columns_picker(mut columns: State<u8>, height: f32) -> impl IntoElement {
|
||||
let options: Vec<String> = (GRID_COLUMNS_MIN..=GRID_COLUMNS_MAX)
|
||||
.map(|n| n.to_string())
|
||||
.collect();
|
||||
let selected = resolved_columns(*columns.read()).to_string();
|
||||
|
||||
Dropdown::new(selected, options)
|
||||
.width(Size::px(60.))
|
||||
.height(Size::px(height))
|
||||
.on_select(move |idx: usize| columns.set(GRID_COLUMNS_MIN + idx as u8))
|
||||
}
|
||||
|
||||
pub fn cell_width(total: f32, cols: usize, gap: f32) -> f32 {
|
||||
let cols = cols.max(1);
|
||||
((total - (cols - 1) as f32 * gap) / cols as f32).max(0.)
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ mod context_menu;
|
|||
mod dropdown;
|
||||
mod dynamic_art;
|
||||
mod file_drop;
|
||||
mod grid_columns;
|
||||
mod icons;
|
||||
mod java_install_manager;
|
||||
mod generic_prompt;
|
||||
|
|
@ -51,6 +52,7 @@ pub use context_menu::ContextMenu;
|
|||
pub use dropdown::Dropdown;
|
||||
pub use dynamic_art::{ART_PREVIEW_EDGE, DynamicArt};
|
||||
pub use file_drop::FileDropOverlay;
|
||||
pub use grid_columns::{cell_width, grid_columns_picker, resolved_columns};
|
||||
pub use icons::{Icon, IconTint, IconType};
|
||||
pub use java_install_manager::JavaInstallManager;
|
||||
pub use generic_prompt::GenericPromptOverlay;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ use super::{use_dispatch, use_settings_snapshot};
|
|||
pub struct PersistedView {
|
||||
pub layout: State<ViewLayout>,
|
||||
pub sort: State<Option<String>>,
|
||||
pub columns: State<u8>,
|
||||
}
|
||||
|
||||
pub fn use_view_state(key: &str) -> PersistedView {
|
||||
|
|
@ -17,14 +18,17 @@ pub fn use_view_state(key: &str) -> PersistedView {
|
|||
let initial = settings.view_state(&key);
|
||||
let init_layout = initial.layout;
|
||||
let init_sort = initial.sort;
|
||||
let init_columns = initial.columns;
|
||||
let layout = use_state(move || init_layout);
|
||||
let sort = use_state(move || init_sort.clone());
|
||||
let columns = use_state(move || init_columns);
|
||||
|
||||
let mut first = use_state(|| true);
|
||||
use_side_effect(move || {
|
||||
let next = ViewState {
|
||||
layout: *layout.read(),
|
||||
sort: sort.read().clone(),
|
||||
columns: *columns.read(),
|
||||
};
|
||||
if *first.peek() {
|
||||
first.set(false);
|
||||
|
|
@ -38,5 +42,9 @@ pub fn use_view_state(key: &str) -> PersistedView {
|
|||
dispatch.set_settings(updated);
|
||||
});
|
||||
|
||||
PersistedView { layout, sort }
|
||||
PersistedView {
|
||||
layout,
|
||||
sort,
|
||||
columns,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,15 +114,6 @@ pub fn centered_note(text: &str) -> Element {
|
|||
.into_element()
|
||||
}
|
||||
|
||||
/// Returns 1 until the grid has measured itself so the first frame still renders
|
||||
pub fn grid_columns_for_width(width: f32, max_col: f32, gap: f32) -> usize {
|
||||
if width <= 0. {
|
||||
return 1;
|
||||
}
|
||||
(((width + gap) / (max_col + gap)).ceil() as usize).max(1)
|
||||
}
|
||||
|
||||
/// Reports its measured size back through `width` for [`grid_columns_for_width`]
|
||||
/// Short final rows are padded with empty flex cells so tiles keep the column width
|
||||
pub fn flow_grid(items: Vec<Element>, cols: usize, mut width: State<f32>, gap: f32) -> Element {
|
||||
let cols = cols.max(1);
|
||||
|
|
|
|||
|
|
@ -31,9 +31,10 @@ pub(super) fn grid_row(
|
|||
cluster_id: i64,
|
||||
package_type: &str,
|
||||
installed: &InstalledMap,
|
||||
metrics: GridMetrics,
|
||||
) -> impl IntoElement {
|
||||
let package_type = package_type.to_string();
|
||||
let fill = GRID_COLUMNS - row.len();
|
||||
let fill = metrics.cols.saturating_sub(row.len());
|
||||
let installed: Vec<Option<InstallSource>> = row
|
||||
.iter()
|
||||
.map(|item| installed_for(installed, item))
|
||||
|
|
@ -42,14 +43,14 @@ pub(super) fn grid_row(
|
|||
rect()
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
.height(Size::px(CARD_H))
|
||||
.height(Size::px(metrics.card_h))
|
||||
.spacing(GRID_SPACING)
|
||||
.content(Content::Flex)
|
||||
.children(
|
||||
row.into_iter()
|
||||
.zip(installed)
|
||||
.map(move |(item, installed)| {
|
||||
PackageCard::new(item, cluster_id, package_type.clone(), installed)
|
||||
PackageCard::new(item, cluster_id, package_type.clone(), installed, metrics)
|
||||
.into_element()
|
||||
}),
|
||||
)
|
||||
|
|
@ -76,6 +77,7 @@ struct PackageCard {
|
|||
cluster_id: i64,
|
||||
package_type: String,
|
||||
installed: Option<InstallSource>,
|
||||
metrics: GridMetrics,
|
||||
}
|
||||
|
||||
impl PackageCard {
|
||||
|
|
@ -84,12 +86,14 @@ impl PackageCard {
|
|||
cluster_id: i64,
|
||||
package_type: String,
|
||||
installed: Option<InstallSource>,
|
||||
metrics: GridMetrics,
|
||||
) -> Self {
|
||||
Self {
|
||||
item,
|
||||
cluster_id,
|
||||
package_type,
|
||||
installed,
|
||||
metrics,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -109,7 +113,7 @@ impl Component for PackageCard {
|
|||
rect()
|
||||
.vertical()
|
||||
.width(Size::flex(1.0))
|
||||
.height(Size::px(CARD_H))
|
||||
.height(Size::px(self.metrics.card_h))
|
||||
.corner_radius(CornerRadius::new_all(10.))
|
||||
.background(CARD_BG)
|
||||
.border(border_all_color(
|
||||
|
|
@ -131,7 +135,8 @@ impl Component for PackageCard {
|
|||
.margin(Gaps::new_all(1.))
|
||||
.corner_radius(CornerRadius::new(10., 10., 0., 0.))
|
||||
.overflow(Overflow::Clip)
|
||||
.child(PackageBanner::new(icon_url, BANNER_H))
|
||||
.child(PackageBanner::new(icon_url, self.metrics.banner_h))
|
||||
// Top-left of the banner the first thing worth knowing about a result and the opposite corner is the install button's
|
||||
.maybe_child(self.installed.map(|installed| {
|
||||
rect()
|
||||
.position(Position::new_absolute().top(8.).left(8.))
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ use oneclient_core::settings::ViewLayout;
|
|||
|
||||
use crate::components::{
|
||||
Dropdown, Icon, IconType, Pagination, ScrollArea, Segment, SegmentedControl, TextInput,
|
||||
cell_width, grid_columns_picker, resolved_columns,
|
||||
};
|
||||
use crate::hooks::use_cluster;
|
||||
use crate::hooks::{
|
||||
|
|
@ -43,15 +44,24 @@ use skeletons::{SkeletonListRow, skeleton_grid_row};
|
|||
|
||||
const CARD_BG: Color = Color::from_rgb(26, 34, 41);
|
||||
const CARD_NAME: Color = Color::from_rgb(213, 219, 255);
|
||||
const GRID_COLUMNS: usize = 3;
|
||||
const SCROLLBAR_GUTTER: f32 = 18.;
|
||||
const CARD_H: f32 = 240.;
|
||||
const BANNER_H: f32 = 100.;
|
||||
const CARD_TEXT_H: f32 = CARD_H - BANNER_H;
|
||||
const BANNER_RATIO: f32 = 0.35;
|
||||
const BANNER_MAX_H: f32 = 200.;
|
||||
const LIST_ROW_H: f32 = 78.;
|
||||
const GRID_SPACING: f32 = 12.;
|
||||
const LIST_SPACING: f32 = 8.;
|
||||
const SEARCH_DEBOUNCE_MS: u64 = 250;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub(super) struct GridMetrics {
|
||||
pub cols: usize,
|
||||
pub card_h: f32,
|
||||
pub banner_h: f32,
|
||||
}
|
||||
|
||||
fn type_title(package_type: &str) -> &'static str {
|
||||
match package_type {
|
||||
"shader" => "Shaders",
|
||||
|
|
@ -84,7 +94,9 @@ impl Component for Browser {
|
|||
|
||||
let query = use_state(|| saved.query.clone());
|
||||
let provider = use_state(|| saved.provider);
|
||||
let view_mode = use_view_state(&format!("browser.{package_type}")).layout;
|
||||
let view = use_view_state(&format!("browser.{package_type}"));
|
||||
let view_mode = view.layout;
|
||||
let grid_columns = view.columns;
|
||||
let compatible_only = use_browser_compat();
|
||||
let selected_categories = use_state(|| saved.categories.clone());
|
||||
let page = use_state(|| saved.page);
|
||||
|
|
@ -178,6 +190,16 @@ impl Component for Browser {
|
|||
|
||||
let mode = *view_mode.read();
|
||||
|
||||
let mut grid_width = use_state(|| 0f32);
|
||||
let cols = resolved_columns(*grid_columns.read());
|
||||
let card_w = cell_width(
|
||||
(*grid_width.read() - SCROLLBAR_GUTTER).max(0.),
|
||||
cols,
|
||||
GRID_SPACING,
|
||||
);
|
||||
let banner_h = (card_w * BANNER_RATIO).clamp(BANNER_H, BANNER_MAX_H);
|
||||
let card_h = banner_h + CARD_TEXT_H;
|
||||
|
||||
let fade_dep = (current_page, packages.is_empty(), pending, mode);
|
||||
let fade = use_animation_with_dependencies(&fade_dep, |conf, _| {
|
||||
conf.on_creation(OnCreation::Run);
|
||||
|
|
@ -198,10 +220,21 @@ impl Component for Browser {
|
|||
match mode {
|
||||
ViewLayout::Grid => {
|
||||
let rows: Vec<Vec<ProjectSummary>> =
|
||||
packages.chunks(GRID_COLUMNS).map(|c| c.to_vec()).collect();
|
||||
packages.chunks(cols).map(|c| c.to_vec()).collect();
|
||||
|
||||
sa.lazy(rows.len(), CARD_H, GRID_SPACING, move |i| {
|
||||
grid_row(rows[i].clone(), cluster_id, &pkg, &installed).into_element()
|
||||
sa.lazy(rows.len(), card_h, GRID_SPACING, move |i| {
|
||||
grid_row(
|
||||
rows[i].clone(),
|
||||
cluster_id,
|
||||
&pkg,
|
||||
&installed,
|
||||
GridMetrics {
|
||||
cols,
|
||||
card_h,
|
||||
banner_h,
|
||||
},
|
||||
)
|
||||
.into_element()
|
||||
})
|
||||
}
|
||||
ViewLayout::List => sa.lazy(packages.len(), LIST_ROW_H, LIST_SPACING, move |i| {
|
||||
|
|
@ -211,9 +244,14 @@ impl Component for Browser {
|
|||
} else if pending {
|
||||
match mode {
|
||||
ViewLayout::Grid => {
|
||||
let rows = BROWSE_PAGE_SIZE.div_ceil(GRID_COLUMNS);
|
||||
sa.lazy(rows, CARD_H, GRID_SPACING, |_| {
|
||||
skeleton_grid_row().into_element()
|
||||
let rows = BROWSE_PAGE_SIZE.div_ceil(cols);
|
||||
sa.lazy(rows, card_h, GRID_SPACING, move |_| {
|
||||
skeleton_grid_row(GridMetrics {
|
||||
cols,
|
||||
card_h,
|
||||
banner_h,
|
||||
})
|
||||
.into_element()
|
||||
})
|
||||
}
|
||||
ViewLayout::List => sa.lazy(BROWSE_PAGE_SIZE, LIST_ROW_H, LIST_SPACING, |_| {
|
||||
|
|
@ -240,7 +278,7 @@ impl Component for Browser {
|
|||
package_type: package_type.clone(),
|
||||
}),
|
||||
))
|
||||
.child(controls(provider, query, view_mode))
|
||||
.child(controls(provider, query, view_mode, grid_columns))
|
||||
.child(
|
||||
rect()
|
||||
.horizontal()
|
||||
|
|
@ -265,7 +303,13 @@ impl Component for Browser {
|
|||
.width(Size::fill())
|
||||
.height(Size::flex(1.0))
|
||||
.child(results)
|
||||
.opacity(fade_opacity),
|
||||
.opacity(fade_opacity)
|
||||
.on_sized(move |event: Event<SizedEventData>| {
|
||||
let w = event.data().area.width();
|
||||
if (w - *grid_width.peek()).abs() > 0.5 {
|
||||
*grid_width.write() = w;
|
||||
}
|
||||
}),
|
||||
)
|
||||
.maybe_child(pages.map(|pages| {
|
||||
Pagination::new(page, pages)
|
||||
|
|
@ -379,6 +423,7 @@ fn controls(
|
|||
provider: State<ProviderId>,
|
||||
query: State<String>,
|
||||
view_mode: State<ViewLayout>,
|
||||
grid_columns: State<u8>,
|
||||
) -> impl IntoElement {
|
||||
rect()
|
||||
.horizontal()
|
||||
|
|
@ -387,6 +432,10 @@ fn controls(
|
|||
.spacing(12.)
|
||||
.content(Content::Flex)
|
||||
.child(rect().width(Size::flex(1.0)))
|
||||
.maybe_child(
|
||||
(*view_mode.read() == ViewLayout::Grid)
|
||||
.then(|| grid_columns_picker(grid_columns, 30.).into_element()),
|
||||
)
|
||||
.child(
|
||||
SegmentedControl::new(view_mode)
|
||||
.equal_width(30.)
|
||||
|
|
|
|||
|
|
@ -27,7 +27,9 @@ fn use_skeleton_pulse() -> f32 {
|
|||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct SkeletonCard;
|
||||
struct SkeletonCard {
|
||||
metrics: GridMetrics,
|
||||
}
|
||||
|
||||
impl Component for SkeletonCard {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
|
|
@ -35,13 +37,13 @@ impl Component for SkeletonCard {
|
|||
rect()
|
||||
.vertical()
|
||||
.width(Size::flex(1.0))
|
||||
.height(Size::px(CARD_H))
|
||||
.height(Size::px(self.metrics.card_h))
|
||||
.corner_radius(CornerRadius::new_all(10.))
|
||||
.background(CARD_BG)
|
||||
.border(border_all_color(1., colors::component_border()))
|
||||
.overflow(Overflow::Clip)
|
||||
.opacity(pulse)
|
||||
.child(skeleton_block(Size::fill(), BANNER_H))
|
||||
.child(skeleton_block(Size::fill(), self.metrics.banner_h))
|
||||
.child(
|
||||
rect()
|
||||
.vertical()
|
||||
|
|
@ -88,13 +90,13 @@ impl Component for SkeletonListRow {
|
|||
}
|
||||
}
|
||||
|
||||
pub(super) fn skeleton_grid_row() -> impl IntoElement {
|
||||
pub(super) fn skeleton_grid_row(metrics: GridMetrics) -> impl IntoElement {
|
||||
rect()
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
.height(Size::px(CARD_H))
|
||||
.height(Size::px(metrics.card_h))
|
||||
.spacing(GRID_SPACING)
|
||||
.content(Content::Flex)
|
||||
.children((0..GRID_COLUMNS).map(|_| SkeletonCard.into_element()))
|
||||
.children((0..metrics.cols).map(move |_| SkeletonCard { metrics }.into_element()))
|
||||
.into_element()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,15 +4,18 @@ use super::*;
|
|||
|
||||
use oneclient_content::packages::types::GalleryImage;
|
||||
|
||||
use crate::components::{Button, Icon, IconType, OverlayPopup};
|
||||
use crate::hooks::{loaded_image, query_is_busy, use_cached_image};
|
||||
use crate::components::{
|
||||
Button, Icon, IconType, OverlayPopup, cell_width, grid_columns_picker, resolved_columns,
|
||||
};
|
||||
use crate::hooks::{loaded_image, query_is_busy, use_cached_image, use_view_state};
|
||||
use crate::theme::colors;
|
||||
use crate::ui::{border_all_color, flow_grid, grid_columns_for_width};
|
||||
use crate::ui::{border_all_color, flow_grid};
|
||||
|
||||
/// How wide a tile is allowed to get before the grid takes another column
|
||||
const MAX_COL_W: f32 = 400.;
|
||||
const GRID_GAP: f32 = 16.;
|
||||
const TILE_PREVIEW_H: f32 = 168.;
|
||||
const TILE_PREVIEW_RATIO: f32 = TILE_PREVIEW_H / MAX_COL_W;
|
||||
const TILE_PREVIEW_MAX_H: f32 = 420.;
|
||||
|
||||
/// Downscale ceilings for the image cache full-size tiles made the grid slow to fill
|
||||
const TILE_EDGE: u32 = 640;
|
||||
|
|
@ -32,6 +35,11 @@ impl Component for Gallery {
|
|||
let grid_width = use_state(|| 0f32);
|
||||
let mut viewing = use_state(|| None::<usize>);
|
||||
|
||||
let grid_columns = use_view_state("browser.gallery").columns;
|
||||
let cols = resolved_columns(*grid_columns.read());
|
||||
let tile_w = cell_width(*grid_width.read(), cols, GRID_GAP);
|
||||
let preview_h = (tile_w * TILE_PREVIEW_RATIO).clamp(TILE_PREVIEW_H, TILE_PREVIEW_MAX_H);
|
||||
|
||||
let tiles: Vec<Element> = self
|
||||
.images
|
||||
.iter()
|
||||
|
|
@ -40,6 +48,7 @@ impl Component for Gallery {
|
|||
let key = image.url.clone();
|
||||
GalleryTile {
|
||||
image: image.clone(),
|
||||
preview_h,
|
||||
on_open: (move |()| viewing.set(Some(idx))).into(),
|
||||
key: DiffKey::None,
|
||||
}
|
||||
|
|
@ -48,11 +57,21 @@ impl Component for Gallery {
|
|||
})
|
||||
.collect();
|
||||
|
||||
let cols = grid_columns_for_width(*grid_width.read(), MAX_COL_W, GRID_GAP);
|
||||
let images = self.images.clone();
|
||||
|
||||
rect()
|
||||
.vertical()
|
||||
.width(Size::fill())
|
||||
.spacing(10.)
|
||||
.child(
|
||||
rect()
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
.cross_align(Alignment::Center)
|
||||
.content(Content::Flex)
|
||||
.child(rect().width(Size::flex(1.0)))
|
||||
.child(grid_columns_picker(grid_columns, 30.)),
|
||||
)
|
||||
.child(flow_grid(tiles, cols, grid_width, GRID_GAP))
|
||||
.maybe_child((*viewing.read()).map(|start| {
|
||||
GalleryViewer {
|
||||
|
|
@ -68,6 +87,7 @@ impl Component for Gallery {
|
|||
#[derive(PartialEq)]
|
||||
struct GalleryTile {
|
||||
image: GalleryImage,
|
||||
preview_h: f32,
|
||||
on_open: EventHandler<()>,
|
||||
key: DiffKey,
|
||||
}
|
||||
|
|
@ -88,7 +108,7 @@ impl Component for GalleryTile {
|
|||
|
||||
let preview = rect()
|
||||
.width(Size::fill())
|
||||
.height(Size::px(TILE_PREVIEW_H))
|
||||
.height(Size::px(self.preview_h))
|
||||
.center()
|
||||
.overflow(Overflow::Clip)
|
||||
.background(colors::component_bg())
|
||||
|
|
@ -104,7 +124,6 @@ impl Component for GalleryTile {
|
|||
rect()
|
||||
.vertical()
|
||||
.width(Size::flex(1.0))
|
||||
.max_width(Size::px(MAX_COL_W))
|
||||
.corner_radius(CornerRadius::new_all(10.))
|
||||
.overflow(Overflow::Clip)
|
||||
.background(PANEL_BG)
|
||||
|
|
|
|||
|
|
@ -386,6 +386,7 @@ impl Component for PackageManager {
|
|||
let view = use_view_state("cluster.packages");
|
||||
let sort = view.sort;
|
||||
let layout = view.layout;
|
||||
let grid_columns = view.columns;
|
||||
let query = SearchQuery::new(&search.read());
|
||||
let sort_mode = sort
|
||||
.read()
|
||||
|
|
@ -438,6 +439,7 @@ impl Component for PackageManager {
|
|||
enabled_filter,
|
||||
hidden_filter,
|
||||
layout,
|
||||
grid_columns,
|
||||
cluster_id,
|
||||
package_type,
|
||||
))
|
||||
|
|
@ -450,6 +452,7 @@ impl Component for PackageManager {
|
|||
cluster_id,
|
||||
content_kind,
|
||||
card_layout,
|
||||
*grid_columns.read(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use oneclient_core::settings::ViewLayout;
|
|||
|
||||
use crate::components::{
|
||||
Button, CardLayout, Icon, IconType, PackageEntry, PackageRow, ScrollArea, ScrollAreaCtx,
|
||||
Segment, SegmentedControl, TabBar, TabItem, TextInput,
|
||||
Segment, SegmentedControl, TabBar, TabItem, TextInput, grid_columns_picker, resolved_columns,
|
||||
};
|
||||
use crate::hooks::{use_dispatch, use_overlay_claim};
|
||||
use crate::routes::Route;
|
||||
|
|
@ -140,6 +140,7 @@ pub(super) fn toolbar_bar(
|
|||
enabled_filter: State<EnabledFilter>,
|
||||
hidden_filter: State<HiddenFilter>,
|
||||
layout: State<ViewLayout>,
|
||||
grid_columns: State<u8>,
|
||||
cluster_id: i64,
|
||||
package_type: &'static str,
|
||||
) -> impl IntoElement {
|
||||
|
|
@ -193,6 +194,10 @@ pub(super) fn toolbar_bar(
|
|||
enabled_filter,
|
||||
hidden_filter,
|
||||
})
|
||||
.maybe_child(
|
||||
(*layout.read() == ViewLayout::Grid)
|
||||
.then(|| grid_columns_picker(grid_columns, 34.).into_element()),
|
||||
)
|
||||
.child(
|
||||
SegmentedControl::new(layout)
|
||||
.height(34.)
|
||||
|
|
@ -557,6 +562,7 @@ pub(super) struct ContentBox {
|
|||
cluster_id: i64,
|
||||
kind: ContentKind,
|
||||
layout: CardLayout,
|
||||
grid_columns: u8,
|
||||
}
|
||||
|
||||
impl ContentBox {
|
||||
|
|
@ -569,6 +575,7 @@ impl ContentBox {
|
|||
cluster_id: i64,
|
||||
kind: ContentKind,
|
||||
layout: CardLayout,
|
||||
grid_columns: u8,
|
||||
) -> Self {
|
||||
Self {
|
||||
items,
|
||||
|
|
@ -578,6 +585,7 @@ impl ContentBox {
|
|||
cluster_id,
|
||||
kind,
|
||||
layout,
|
||||
grid_columns,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -591,6 +599,7 @@ impl Component for ContentBox {
|
|||
let noun_plural = self.noun_plural;
|
||||
let kind = &self.kind;
|
||||
let layout = self.layout;
|
||||
let grid_columns = self.grid_columns;
|
||||
|
||||
let dispatch = use_dispatch();
|
||||
|
||||
|
|
@ -615,7 +624,7 @@ impl Component for ContentBox {
|
|||
.width(Size::fill())
|
||||
.height(Size::fill())
|
||||
.content(move |ctx: ScrollAreaCtx| {
|
||||
grid_content(&items, package_type, cluster_id, ctx).into_element()
|
||||
grid_content(&items, package_type, cluster_id, grid_columns, ctx).into_element()
|
||||
})
|
||||
.into_element(),
|
||||
});
|
||||
|
|
@ -680,11 +689,12 @@ fn grid_content(
|
|||
items: &[PackageEntry],
|
||||
package_type: &'static str,
|
||||
cluster_id: i64,
|
||||
grid_columns: u8,
|
||||
ctx: ScrollAreaCtx,
|
||||
) -> impl IntoElement {
|
||||
let count = items.len();
|
||||
let cols =
|
||||
(((ctx.viewport_w + GRID_GAP) / (GRID_MIN_W + GRID_GAP)).floor() as usize).clamp(1, 3);
|
||||
let fits = ((ctx.viewport_w + GRID_GAP) / (GRID_MIN_W + GRID_GAP)).floor() as usize;
|
||||
let cols = resolved_columns(grid_columns).min(fits.max(1));
|
||||
let rows_total = count.div_ceil(cols);
|
||||
let slot = CARD_GRID_H + GRID_GAP;
|
||||
|
||||
|
|
|
|||
|
|
@ -7,12 +7,13 @@ use oneclient_core::settings::ViewLayout;
|
|||
|
||||
use crate::components::{
|
||||
Button, ContextMenu, Icon, IconType, LocalImage, OverlayPopup, ScreenshotViewer, ScrollArea,
|
||||
Segment, SegmentedControl, open_folder_button,
|
||||
Segment, SegmentedControl, cell_width, grid_columns_picker, open_folder_button,
|
||||
resolved_columns,
|
||||
};
|
||||
use crate::hooks::{ScreenshotAction, query_is_loading, try_cluster_screenshots, use_cluster_screenshots, use_dispatch, use_screenshot_action, use_screenshot_folder_watch, use_view_state};
|
||||
use crate::layout::cluster_content;
|
||||
use crate::theme::colors;
|
||||
use crate::ui::{border_all_color, flow_grid, fmt_date, grid_columns_for_width};
|
||||
use crate::ui::{border_all_color, flow_grid, fmt_date};
|
||||
use crate::utils::{format_res, format_size};
|
||||
|
||||
use super::cluster_not_found;
|
||||
|
|
@ -22,6 +23,8 @@ const THUMB_EDGE: u32 = 480;
|
|||
const MAX_COL_W: f32 = 400.;
|
||||
const GRID_GAP: f32 = 16.;
|
||||
const TILE_PREVIEW_H: f32 = 168.;
|
||||
const TILE_PREVIEW_RATIO: f32 = TILE_PREVIEW_H / MAX_COL_W;
|
||||
const TILE_PREVIEW_MAX_H: f32 = 420.;
|
||||
|
||||
const CARD_BG: Color = Color::from_rgb(26, 34, 41);
|
||||
const CARD_NAME: Color = Color::from_rgb(213, 219, 255);
|
||||
|
|
@ -45,7 +48,9 @@ impl Component for ClusterScreenshots {
|
|||
|
||||
let shots = try_cluster_screenshots(&query).unwrap_or_default();
|
||||
|
||||
let view_mode = use_view_state("cluster.screenshots").layout;
|
||||
let view = use_view_state("cluster.screenshots");
|
||||
let view_mode = view.layout;
|
||||
let grid_columns = view.columns;
|
||||
let menu_dispatch = use_dispatch();
|
||||
|
||||
let mut edit_mode = use_state(|| false);
|
||||
|
|
@ -59,11 +64,16 @@ impl Component for ClusterScreenshots {
|
|||
&shots,
|
||||
folder,
|
||||
view_mode,
|
||||
grid_columns,
|
||||
edit_mode,
|
||||
selected,
|
||||
confirm_delete,
|
||||
);
|
||||
|
||||
let cols = resolved_columns(*grid_columns.read());
|
||||
let tile_w = cell_width(*grid_width.read(), cols, GRID_GAP);
|
||||
let preview_h = (tile_w * TILE_PREVIEW_RATIO).clamp(TILE_PREVIEW_H, TILE_PREVIEW_MAX_H);
|
||||
|
||||
let content: Element = if shots.is_empty() {
|
||||
empty_state(query_is_loading(&query))
|
||||
.into_element()
|
||||
|
|
@ -98,6 +108,7 @@ impl Component for ClusterScreenshots {
|
|||
info,
|
||||
selected: is_selected,
|
||||
edit_mode: editing,
|
||||
preview_h,
|
||||
on_activate: on_activate.into(),
|
||||
on_context,
|
||||
}
|
||||
|
|
@ -115,10 +126,7 @@ impl Component for ClusterScreenshots {
|
|||
}
|
||||
|
||||
match *view_mode.read() {
|
||||
ViewLayout::Grid => {
|
||||
let cols = grid_columns_for_width(*grid_width.read(), MAX_COL_W, GRID_GAP);
|
||||
flow_grid(items, cols, grid_width, GRID_GAP)
|
||||
}
|
||||
ViewLayout::Grid => flow_grid(items, cols, grid_width, GRID_GAP),
|
||||
ViewLayout::List => rect()
|
||||
.vertical()
|
||||
.width(Size::fill())
|
||||
|
|
@ -200,6 +208,7 @@ fn toolbar_row(
|
|||
shots: &[ScreenshotInfo],
|
||||
folder: Option<PathBuf>,
|
||||
view_mode: State<ViewLayout>,
|
||||
grid_columns: State<u8>,
|
||||
mut edit_mode: State<bool>,
|
||||
mut selected: State<HashSet<PathBuf>>,
|
||||
mut confirm_delete: State<bool>,
|
||||
|
|
@ -267,6 +276,10 @@ fn toolbar_row(
|
|||
);
|
||||
} else {
|
||||
right = right
|
||||
.maybe_child(
|
||||
(*view_mode.read() == ViewLayout::Grid)
|
||||
.then(|| grid_columns_picker(grid_columns, 36.).into_element()),
|
||||
)
|
||||
.child(
|
||||
SegmentedControl::new(view_mode)
|
||||
.equal_width(40.)
|
||||
|
|
@ -416,6 +429,7 @@ struct ScreenshotTile {
|
|||
info: ScreenshotInfo,
|
||||
selected: bool,
|
||||
edit_mode: bool,
|
||||
preview_h: f32,
|
||||
on_activate: EventHandler<()>,
|
||||
on_context: EventHandler<(f32, f32)>,
|
||||
}
|
||||
|
|
@ -440,7 +454,6 @@ impl Component for ScreenshotTile {
|
|||
rect()
|
||||
.vertical()
|
||||
.width(Size::flex(1.0))
|
||||
.max_width(Size::px(MAX_COL_W))
|
||||
.corner_radius(CornerRadius::new_all(10.))
|
||||
.background(CARD_BG)
|
||||
.overflow(Overflow::Clip)
|
||||
|
|
@ -456,7 +469,7 @@ impl Component for ScreenshotTile {
|
|||
})
|
||||
.child(preview_box(
|
||||
info.path.clone(),
|
||||
TILE_PREVIEW_H,
|
||||
self.preview_h,
|
||||
info.resolution,
|
||||
))
|
||||
.child(
|
||||
|
|
|
|||
|
|
@ -13,12 +13,25 @@ pub enum ViewLayout {
|
|||
List,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
|
||||
pub const DEFAULT_GRID_COLUMNS: u8 = 3;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
#[serde(default)]
|
||||
pub struct ViewState {
|
||||
pub layout: ViewLayout,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sort: Option<String>,
|
||||
pub columns: u8,
|
||||
}
|
||||
|
||||
impl Default for ViewState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
layout: ViewLayout::default(),
|
||||
sort: None,
|
||||
columns: DEFAULT_GRID_COLUMNS,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ mod launcher;
|
|||
|
||||
pub mod data_dir;
|
||||
|
||||
pub use launcher::{LauncherSettings, ViewLayout, ViewState};
|
||||
pub use launcher::{DEFAULT_GRID_COLUMNS, LauncherSettings, ViewLayout, ViewState};
|
||||
pub use oneclient_cluster::{
|
||||
GameSettingsProfile, PackageUpdateMode, ProfileUpdate, SettingsOsExtra,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue