feat: featured gallery

This commit is contained in:
Szedann 2025-07-20 20:56:33 +02:00
parent 6634e0648e
commit 2d02f64239
6 changed files with 66 additions and 24 deletions

View file

@ -13,7 +13,7 @@ function includes<T, TArray extends T>(list: { includes: (arg0: TArray) => boole
export function PackageGrid({ items, provider }: { items: Array<SearchResult>; provider: Provider }) {
return (
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
<div className="grid grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-2">
{items.map(item => (
<PackageItem key={item.project_id} {...item} provider={provider} />
))}
@ -45,11 +45,11 @@ export function PackageItem({ provider, ...item }: SearchResult & { provider: Pr
<img alt={`Icon for ${item.title}`} className="absolute z-0 max-w-none w-7/6 opacity-50 blur-xl" src={item.icon_url} />
<img
alt={`Icon for ${item.title}`}
className="relative z-1 aspect-ratio-square rounded-md image-render-auto w-2/5"
className="relative z-1 aspect-ratio-square rounded-lg image-render-auto h-5/6"
src={item.icon_url}
/>
</Show>
<Tooltip text={loaders.map(upperFirst).join(', ')}>
<Tooltip className="bg-component-bg-disabled" text={loaders.map(upperFirst).join(', ')}>
<Focusable>
<div className="flex flex-col rounded-full bg-component-bg/70 border-component-border/70 border p-1 absolute top-0 right-0 m-2">
{loaders.toSpliced(loaders.length > 3 ? 2 : 3).map(loader => <LoaderIcon className="w-5 m-1" key={loader} loader={loader} />)}

View file

@ -27,19 +27,20 @@ function RouteComponent() {
function Featured() {
// const context = useBrowserContext();
const [provider, slug] = useMemo<[Provider, string]>(() => ['Modrinth', 'sodium'], []);
const [provider, slug] = useMemo<[Provider, string]>(() => ['Modrinth', 'iris'], []);
const featuredPackage = usePackageData(provider, slug, {}, 'getFeatured');
const navigate = useNavigate();
const featuredImageIndex = useMemo(() => featuredPackage.data?.gallery.findIndex(image => image.featured), [featuredPackage.data]);
const [selectedImage, setSelectedImage] = useState(featuredImageIndex ?? 0);
return (
<Show when={featuredPackage.isSuccess}>
<div className="flex flex-col gap-y-1">
<h5 className="ml-2">Featured</h5>
<div className="w-full flex flex-row overflow-hidden rounded-lg bg-page-elevated">
<h5 className="ml-2 uppercase opacity-60">Featured</h5>
<div className="w-full flex flex-col lg:flex-row overflow-hidden rounded-lg bg-page-elevated">
<div className="w-full p-1">
<img alt="thumbnail" className="aspect-ratio-video h-full rounded-md object-cover object-center" src={featuredPackage.data?.icon_url ?? ''} />
<img alt="thumbnail" className="aspect-video h-full w-full rounded-md object-cover object-center" src={featuredPackage.data?.gallery[selectedImage].url} />
</div>
<div className="max-w-84 min-w-52 flex flex-col gap-y-1 p-4">
<div className="max-w-64 min-w-52 flex flex-col gap-y-1 p-4">
<h2>{featuredPackage.data?.name}</h2>
<Show when={false}>
@ -49,7 +50,20 @@ function Featured() {
</div>
</Show>
<p className="mt-1 flex-1 leading-normal">{featuredPackage.data?.short_desc}</p>
<p className="my-1 flex-1 leading-normal">{featuredPackage.data?.short_desc}</p>
<div className="grid grid-cols-3 gap-2 h-full">
{featuredPackage.data?.gallery.map(
(image, index) => (
<img
className="aspect-video object-center object-cover rounded-md"
key={image.url}
onClick={() => setSelectedImage(index)}
src={image.url}
/>
),
)}
</div>
<div className="flex flex-row justify-end">
<Button color="ghost" onClick={() => navigate({ to: '/app/browser/package/$provider/$slug', params: { provider, slug } })}>
@ -77,7 +91,7 @@ function Lists() {
function List({ provider }: { provider: Provider }) {
const search = useBrowserSearch(provider, {
filters: null,
limit: 18 as unknown as bigint,
limit: 24 as unknown as bigint,
offset: null,
query: null,
sort: null,
@ -85,7 +99,7 @@ function List({ provider }: { provider: Provider }) {
const [expanded, setExpanded] = useState(false);
return (
<div>
<h5 className="uppercase p-3 opacity-60">{provider}</h5>
<h5 className="uppercase m-2 opacity-60">{provider}</h5>
<div className={`relative overflow-hidden ${expanded ? '' : 'h-128'}`}>
{search.isSuccess
? <PackageGrid items={search.data.items} provider={provider} />

View file

@ -30,14 +30,14 @@ function RouteComponent() {
export function BrowserLayout(props: any) {
return (
<div className="relative h-full flex flex-1 flex-col items-center gap-2">
<div className="h-full w-full max-w-screen-xl flex flex-1 flex-col items-center gap-y-2">
<div className="h-full w-full max-w-screen-2xl flex flex-1 flex-col items-center gap-y-2">
<div className="grid grid-cols-[220px_auto_220px] w-full gap-x-6">
<div />
<BrowserToolbar />
<div />
</div>
<div className="grid grid-cols-[220px_auto_220px] w-full max-w-screen-xl gap-x-6 pb-8">
<div className="grid grid-cols-[220px_auto_220px] w-full gap-x-6 pb-8">
<BrowserCategories />
<div className="h-full flex flex-col gap-y-4">

View file

@ -97,6 +97,7 @@ pub struct ManagedPackage {
pub links: PackageLinks,
pub status: PackageStatus,
pub downloads: usize,
pub gallery: Vec<PackageGallery>
}
#[onelauncher_macro::specta]
@ -205,6 +206,16 @@ pub struct PackageDonationUrl {
pub url: String,
}
#[onelauncher_macro::specta]
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackageGallery {
pub url: String,
pub thumbnail_url: String,
pub title: Option<String>,
pub description: Option<String>,
pub featured: Option<bool>
}
/// https://api.modrinth.com/v2/tag/donation_platform
#[onelauncher_macro::specta]
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]

View file

@ -1,6 +1,6 @@
use super::ProviderExt;
use crate::api::packages::data::{
ManagedPackage, ManagedUser, ManagedVersion, ManagedVersionDependency, ManagedVersionFile, PackageAuthor, PackageDependencyType, PackageDonationUrl, PackageLicense, PackageLinks, PackageReleaseType, PackageSide, PackageStatus, SearchQuery, SearchResult, DEFAULT_LIMIT
ManagedPackage, ManagedUser, ManagedVersion, ManagedVersionDependency, ManagedVersionFile, PackageAuthor, PackageDependencyType, PackageDonationUrl, PackageGallery, PackageLicense, PackageLinks, PackageReleaseType, PackageSide, PackageStatus, SearchQuery, SearchResult, DEFAULT_LIMIT
};
use crate::error::LauncherResult;
use crate::utils::http;
@ -363,7 +363,7 @@ struct ModrinthPackage {
#[serde(default)]
pub license: Option<PackageLicense>,
// #[serde(default)]
// pub gallery: Vec<Gallery>,
pub gallery: Vec<ModrinthGallery>,
#[serde(default)]
pub status: PackageStatus,
}
@ -405,6 +405,7 @@ impl From<ModrinthPackage> for ManagedPackage {
},
status: value.status,
downloads: value.downloads,
gallery: value.gallery.into_iter().map(Into::into).collect()
}
}
}
@ -532,6 +533,29 @@ impl From<ModrinthUser> for ManagedUser {
}
}
#[derive(Deserialize)]
pub struct ModrinthGallery {
#[serde(rename = "raw_url")]
pub url: String,
#[serde(rename = "url")]
pub thumbnail_url: String,
pub title: Option<String>,
pub description: Option<String>,
pub featured: Option<bool>
}
impl From<ModrinthGallery> for PackageGallery {
fn from(value: ModrinthGallery) -> Self {
Self {
url: value.url,
thumbnail_url: value.thumbnail_url,
title: value.title,
description: value.description,
featured: value.featured
}
}
}
#[allow(dead_code)]
enum FacetOperation {
NotEq,

View file

@ -110,13 +110,6 @@ body {
display: none;
}
h1 { @apply text-[2rem] font-semibold; line-height: 1.5 !important; }
h2 { @apply text-[1.5rem] font-semibold; line-height: 1.25 !important; }
h3 { @apply text-[1.25rem] font-medium; }
h4 { @apply text-[1rem] font-medium; }
h5 { @apply text-[0.875rem] text-fg-secondary font-bold uppercase; }
h6 { @apply text-[0.75rem] text-fg-secondary font-medium uppercase; }
code {
@apply rounded-md p-2 text-left;
}
}