Skip to content

Commit aa6c3f3

Browse files
committed
feat(ui): Featured package
1 parent b4100a4 commit aa6c3f3

File tree

7 files changed

+152
-98
lines changed

7 files changed

+152
-98
lines changed

apps/desktop/src/api/commands/mod.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,18 @@ macro_rules! collect_commands {
8181
install_update,
8282
// Other
8383
set_window_style,
84-
get_program_info
84+
get_program_info,
85+
get_featured_packages,
8586
]
8687
}};
8788
}
8889

90+
#[specta::specta]
91+
#[tauri::command]
92+
pub async fn get_featured_packages() -> Result<Vec<onelauncher::package::content::FeaturedPackage>, String> {
93+
Ok(onelauncher::package::content::get_featured_packages().await?)
94+
}
95+
8996
#[specta::specta]
9097
#[tauri::command]
9198
pub fn get_program_info() -> Result<super::statics::ProgramInfo, String> {

apps/frontend/src/ui/hooks/useBrowser.tsx

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import type { Cluster, ManagedPackage, PackageType, Providers, ProviderSearchQuery, SearchResult } from '@onelauncher/client/bindings';
1+
import type { Cluster, FeaturedPackage, PackageType, Providers, ProviderSearchQuery, SearchResult } from '@onelauncher/client/bindings';
22
import { useNavigate } from '@solidjs/router';
33
import { bridge } from '~imports';
44
import { createModal } from '~ui/components/overlay/Modal';
55
import BrowserPackage from '~ui/pages/browser/BrowserPackage';
66
import { PROVIDERS } from '~utils';
7-
import { type Accessor, type Context, createContext, createEffect, createSignal, on, onMount, type ParentProps, type Setter, useContext } from 'solid-js';
7+
import { type Accessor, type Context, createContext, createEffect, createResource, createSignal, on, onMount, type ParentProps, type Resource, type Setter, useContext } from 'solid-js';
88
import { ChooseClusterModal, useRecentCluster } from './useCluster';
99
import { tryResult } from './useCommand';
1010

@@ -27,14 +27,22 @@ interface BrowserControllerType {
2727
setPackageType: Setter<PackageType>;
2828

2929
popularPackages: Accessor<PopularPackages | undefined>;
30-
featuredPackage: Accessor<ManagedPackage | undefined>;
30+
featuredPackage: Resource<FeaturedPackage[]>;
3131
};
3232

3333
const BrowserContext = createContext() as Context<BrowserControllerType>;
3434

3535
export function BrowserProvider(props: ParentProps) {
3636
const [popularPackages, setPopularPackages] = createSignal<PopularPackages | undefined>();
37-
const [featuredPackage, setFeaturedPackage] = createSignal<ManagedPackage | undefined>();
37+
const [featuredPackage] = createResource(async () => {
38+
try {
39+
return await tryResult(bridge.commands.getFeaturedPackages);
40+
}
41+
catch (e) {
42+
console.error('Failed to fetch featured packages', e);
43+
return [];
44+
}
45+
});
3846

3947
// Used for the current "Browser Mode". It'll only show packages of the selected type
4048
const [packageType, setPackageType] = createSignal<PackageType>('mod');
@@ -139,13 +147,6 @@ export function BrowserProvider(props: ParentProps) {
139147
}, {} as PopularPackages);
140148

141149
setPopularPackages(popularPackages);
142-
143-
// TODO: Better algorithm for selecting a featured package
144-
const firstPackage = popularPackages.Modrinth[0] || popularPackages.Curseforge[0];
145-
if (firstPackage !== undefined) {
146-
const featuredPackage = await tryResult(() => bridge.commands.getProviderPackage('Modrinth', firstPackage.project_id));
147-
setFeaturedPackage(featuredPackage);
148-
}
149150
});
150151

151152
createEffect(() => {

apps/frontend/src/ui/pages/browser/BrowserMain.tsx

Lines changed: 50 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import type { ManagedPackage, Providers } from '@onelauncher/client/bindings';
1+
import type { Providers } from '@onelauncher/client/bindings';
22
import { ChevronRightIcon } from '@untitled-theme/icons-solid';
33
import OneConfigLogo from '~assets/logos/oneconfig.svg?component-solid';
44
import Button from '~ui/components/base/Button';
55
import SearchResultsContainer from '~ui/components/content/SearchResults';
6+
import Spinner from '~ui/components/Spinner';
67
import useBrowser from '~ui/hooks/useBrowser';
7-
import { For, onMount, Show } from 'solid-js';
8+
import { createMemo, For, onMount, Show } from 'solid-js';
89
import { BrowserContent } from './BrowserRoot';
910

1011
function BrowserMain() {
@@ -20,64 +21,71 @@ function BrowserMain() {
2021
return (
2122
<BrowserContent>
2223
<div class="flex flex-col gap-8">
23-
<Show when={browser.popularPackages() !== undefined && browser.featuredPackage() !== undefined}>
24-
<Featured package={browser.featuredPackage()!} />
24+
<Featured />
2525

26-
<For each={Object.entries(browser.popularPackages()!)}>
27-
{([provider, results]) => (
28-
<SearchResultsContainer
29-
collapsable
30-
header={provider}
31-
provider={provider as Providers}
32-
results={results}
33-
/>
34-
)}
35-
</For>
36-
</Show>
26+
<Spinner.Suspense>
27+
<Show when={browser.popularPackages() !== undefined}>
28+
<For each={Object.entries(browser.popularPackages()!)}>
29+
{([provider, results]) => (
30+
<SearchResultsContainer
31+
collapsable
32+
header={provider}
33+
provider={provider as Providers}
34+
results={results}
35+
/>
36+
)}
37+
</For>
38+
</Show>
39+
</Spinner.Suspense>
3740
</div>
3841
</BrowserContent>
3942
);
4043
}
4144

42-
interface FeaturedProps {
43-
package: ManagedPackage;
44-
}
45-
46-
function Featured(props: FeaturedProps) {
45+
function Featured() {
4746
const browser = useBrowser();
4847

48+
const featured = createMemo(() => browser.featuredPackage()?.[0]);
49+
4950
const open = () => {
50-
browser.displayPackage(props.package.id, props.package.provider);
51+
const pkg = featured();
52+
if (!pkg)
53+
return;
54+
browser.displayPackage(pkg.id, pkg.provider);
5155
};
5256

5357
return (
54-
<div class="flex flex-col gap-y-1">
55-
<h5 class="ml-2">Featured</h5>
56-
<div class="w-full flex flex-row overflow-hidden rounded-lg bg-page-elevated">
57-
<div class="w-full p-1">
58-
<img alt="" class="aspect-ratio-video w-full rounded-md object-cover object-center" src="" />
59-
</div>
60-
<div class="max-w-84 min-w-52 flex flex-col gap-y-1 p-4">
61-
<h2>{props.package.title}</h2>
62-
63-
<div class="w-fit flex flex-row items-center gap-x-1 rounded-lg bg-border/10 px-1.5 py-1 text-fg-primary transition hover:opacity-80">
64-
<OneConfigLogo class="h-3.5 w-3.5" />
65-
<span class="text-sm font-medium">OneConfig Integrated</span>
58+
<Show when={featured()}>
59+
<div class="flex flex-col gap-y-1">
60+
<h5 class="ml-2">Featured</h5>
61+
<div class="w-full flex flex-row overflow-hidden rounded-lg bg-page-elevated">
62+
<div class="w-full p-1">
63+
<img alt={`${featured()?.title} thumbnail`} class="aspect-ratio-video h-full w-full rounded-md object-cover object-center" src={featured()?.thumbnail} />
6664
</div>
65+
<div class="max-w-84 min-w-52 flex flex-col gap-y-1 p-4">
66+
<h2>{featured()?.title}</h2>
6767

68-
<p class="mt-1 flex-1">Lorem ipsum, dolor sit amet consectetur adipisicing elit. Aliquid, veniam odio. Animi quia corporis id fugiat, libero commodi. Repudiandae repellat placeat sed tempora molestias id consequuntur, corrupti ullam mollitia amet?</p>
68+
<Show when={featured()?.oneconfig}>
69+
<div class="w-fit flex flex-row items-center gap-x-1 rounded-lg bg-border/10 px-1.5 py-1 text-fg-primary transition hover:opacity-80">
70+
<OneConfigLogo class="h-3.5 w-3.5" />
71+
<span class="text-sm font-medium">OneConfig Integrated</span>
72+
</div>
73+
</Show>
6974

70-
<div class="flex flex-row justify-end">
71-
<Button
72-
buttonStyle="ghost"
73-
children="View Package"
74-
iconRight={<ChevronRightIcon />}
75-
onClick={open}
76-
/>
75+
<p class="mt-1 flex-1 leading-normal">{featured()?.description}</p>
76+
77+
<div class="flex flex-row justify-end">
78+
<Button
79+
buttonStyle="ghost"
80+
children="View Package"
81+
iconRight={<ChevronRightIcon />}
82+
onClick={open}
83+
/>
84+
</div>
7785
</div>
7886
</div>
7987
</div>
80-
</div>
88+
</Show>
8189
);
8290
}
8391

apps/frontend/uno.config.ts

Lines changed: 43 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -42,85 +42,85 @@ export default defineConfig({
4242
},
4343

4444
colors: {
45-
white: 'rgba(var(--clr-white), <alpha-value>)',
46-
black: 'rgba(var(--clr-black), <alpha-value>)',
45+
white: 'rgba(var(--clr-white))',
46+
black: 'rgba(var(--clr-black))',
4747

48-
border: 'rgba(var(--clr-border), <alpha-value>)',
48+
border: 'rgba(var(--clr-border))',
4949

5050
fg: {
5151
primary: {
52-
DEFAULT: 'rgba(var(--clr-fg-primary), <alpha-value>)',
53-
hover: 'rgba(var(--clr-fg-primary-hover), <alpha-value>)',
54-
pressed: 'rgba(var(--clr-fg-primary-pressed), <alpha-value>)',
55-
disabled: 'rgba(var(--clr-fg-primary-disabled), <alpha-value>)',
52+
DEFAULT: 'rgba(var(--clr-fg-primary))',
53+
hover: 'rgba(var(--clr-fg-primary-hover))',
54+
pressed: 'rgba(var(--clr-fg-primary-pressed))',
55+
disabled: 'rgba(var(--clr-fg-primary-disabled))',
5656
},
5757
secondary: {
58-
DEFAULT: 'rgba(var(--clr-fg-secondary), <alpha-value>)',
59-
hover: 'rgba(var(--clr-fg-secondary-hover), <alpha-value>)',
60-
pressed: 'rgba(var(--clr-fg-secondary-pressed), <alpha-value>)',
61-
disabled: 'rgba(var(--clr-fg-secondary-disabled), <alpha-value>)',
58+
DEFAULT: 'rgba(var(--clr-fg-secondary))',
59+
hover: 'rgba(var(--clr-fg-secondary-hover))',
60+
pressed: 'rgba(var(--clr-fg-secondary-pressed))',
61+
disabled: 'rgba(var(--clr-fg-secondary-disabled))',
6262
},
6363
},
6464

6565
brand: {
66-
DEFAULT: 'rgba(var(--clr-brand), <alpha-value>)',
67-
hover: 'rgba(var(--clr-brand-hover), <alpha-value>)',
68-
pressed: 'rgba(var(--clr-brand-pressed), <alpha-value>)',
69-
disabled: 'rgba(var(--clr-brand-disabled), <alpha-value>)',
66+
DEFAULT: 'rgba(var(--clr-brand))',
67+
hover: 'rgba(var(--clr-brand-hover))',
68+
pressed: 'rgba(var(--clr-brand-pressed))',
69+
disabled: 'rgba(var(--clr-brand-disabled))',
7070
},
7171

7272
onbrand: {
73-
DEFAULT: 'rgba(var(--clr-onbrand), <alpha-value>)',
74-
hover: 'rgba(var(--clr-onbrand-hover), <alpha-value>)',
75-
pressed: 'rgba(var(--clr-onbrand-pressed), <alpha-value>)',
76-
disabled: 'rgba(var(--clr-onbrand-disabled), <alpha-value>)',
73+
DEFAULT: 'rgba(var(--clr-onbrand))',
74+
hover: 'rgba(var(--clr-onbrand-hover))',
75+
pressed: 'rgba(var(--clr-onbrand-pressed))',
76+
disabled: 'rgba(var(--clr-onbrand-disabled))',
7777
},
7878

7979
danger: {
80-
DEFAULT: 'rgba(var(--clr-danger), <alpha-value>)',
81-
hover: 'rgba(var(--clr-danger-hover), <alpha-value>)',
82-
pressed: 'rgba(var(--clr-danger-pressed), <alpha-value>)',
83-
disabled: 'rgba(var(--clr-danger-disabled), <alpha-value>)',
80+
DEFAULT: 'rgba(var(--clr-danger))',
81+
hover: 'rgba(var(--clr-danger-hover))',
82+
pressed: 'rgba(var(--clr-danger-pressed))',
83+
disabled: 'rgba(var(--clr-danger-disabled))',
8484
},
8585

8686
success: {
87-
DEFAULT: 'rgba(var(--clr-success), <alpha-value>)',
88-
hover: 'rgba(var(--clr-success-hover), <alpha-value>)',
89-
pressed: 'rgba(var(--clr-success-pressed), <alpha-value>)',
90-
disabled: 'rgba(var(--clr-success-disabled), <alpha-value>)',
87+
DEFAULT: 'rgba(var(--clr-success))',
88+
hover: 'rgba(var(--clr-success-hover))',
89+
pressed: 'rgba(var(--clr-success-pressed))',
90+
disabled: 'rgba(var(--clr-success-disabled))',
9191
},
9292

9393
component: {
9494
bg: {
95-
DEFAULT: 'rgba(var(--clr-component-bg), <alpha-value>)',
96-
hover: 'rgba(var(--clr-component-bg-hover), <alpha-value>)',
97-
pressed: 'rgba(var(--clr-component-bg-pressed), <alpha-value>)',
98-
disabled: 'rgba(var(--clr-component-bg-disabled), <alpha-value>)',
95+
DEFAULT: 'rgba(var(--clr-component-bg))',
96+
hover: 'rgba(var(--clr-component-bg-hover))',
97+
pressed: 'rgba(var(--clr-component-bg-pressed))',
98+
disabled: 'rgba(var(--clr-component-bg-disabled))',
9999
},
100100
},
101101

102102
code: {
103-
info: 'rgba(var(--clr-code-info), <alpha-value>)',
104-
warn: 'rgba(var(--clr-code-warn), <alpha-value>)',
105-
error: 'rgba(var(--clr-code-error), <alpha-value>)',
106-
debug: 'rgba(var(--clr-code-debug), <alpha-value>)',
107-
trace: 'rgba(var(--clr-code-trace), <alpha-value>)',
103+
info: 'rgba(var(--clr-code-info))',
104+
warn: 'rgba(var(--clr-code-warn))',
105+
error: 'rgba(var(--clr-code-error))',
106+
debug: 'rgba(var(--clr-code-debug))',
107+
trace: 'rgba(var(--clr-code-trace))',
108108
},
109109

110110
link: {
111-
DEFAULT: 'rgba(var(--clr-link), <alpha-value>)',
112-
hover: 'rgba(var(--clr-link-hover), <alpha-value>)',
113-
pressed: 'rgba(var(--clr-link-pressed), <alpha-value>)',
111+
DEFAULT: 'rgba(var(--clr-link))',
112+
hover: 'rgba(var(--clr-link-hover))',
113+
pressed: 'rgba(var(--clr-link-pressed))',
114114
disabled: 'rgba(var(--clr-link-disabled))',
115115
},
116116

117117
page: {
118-
DEFAULT: 'rgba(var(--clr-page), <alpha-value>)',
119-
elevated: 'rgba(var(--clr-page-elevated), <alpha-value>)',
120-
pressed: 'rgba(var(--clr-page-pressed), <alpha-value>)',
118+
DEFAULT: 'rgba(var(--clr-page))',
119+
elevated: 'rgba(var(--clr-page-elevated))',
120+
pressed: 'rgba(var(--clr-page-pressed))',
121121
},
122122

123-
secondary: 'rgba(var(--clr-secondary), <alpha-value>)',
123+
secondary: 'rgba(var(--clr-secondary))',
124124
},
125125
extend: {
126126
height: {

packages/client/src/bindings.ts

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core/src/api/package/content/mod.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
use std::collections::HashMap;
66

77
use modrinth::{Facet, FacetOperation};
8+
use reqwest::Method;
89
use serde::{Deserialize, Serialize};
910

1011
use crate::data::{Loader, ManagedPackage, ManagedUser, ManagedVersion, PackageType};
1112
use crate::package::content::modrinth::FacetBuilder;
1213
use crate::store::{Author, PackageBody, ProviderSearchResults};
14+
use crate::utils::http::fetch_json;
1315
use crate::utils::pagination::Pagination;
14-
use crate::Result;
16+
use crate::{Result, State};
1517

1618
mod curseforge;
1719
mod modrinth;
@@ -218,6 +220,29 @@ impl Providers {
218220
}
219221
}
220222

223+
#[cfg_attr(feature = "specta", derive(specta::Type))]
224+
#[derive(Debug, Serialize, Deserialize)]
225+
pub struct FeaturedPackage {
226+
pub package_type: PackageType,
227+
pub provider: Providers,
228+
pub id: String,
229+
pub title: String,
230+
pub description: String,
231+
pub thumbnail: String,
232+
pub oneconfig: bool,
233+
}
234+
235+
pub async fn get_featured_packages() -> Result<Vec<FeaturedPackage>> {
236+
let state = State::get().await?;
237+
fetch_json(
238+
Method::GET,
239+
crate::constants::FEATURED_PACKAGES_URL,
240+
None,
241+
None,
242+
&state.fetch_semaphore
243+
).await
244+
}
245+
221246
fn build_facets(
222247
builder: &mut FacetBuilder,
223248
categories: Option<Vec<String>>,

0 commit comments

Comments
 (0)