feat: links and gfm support

This commit is contained in:
Szedann 2025-07-21 11:38:12 +02:00
parent 6b42f77ce0
commit c0b6443432
5 changed files with 68 additions and 15 deletions

View file

@ -35,6 +35,7 @@
"react-dom": "catalog:",
"react-markdown": "^10.1.0",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",
"tailwind-merge": "catalog:",
"tailwind-variants": "catalog:",
"tailwindcss": "catalog:",

View file

@ -1,10 +1,10 @@
import type { ClusterModel, ManagedPackage, ManagedUser, ManagedVersion, Paginated, Provider } from '@/bindings.gen';
import type { JSX } from 'react';
import type { ClusterModel, ManagedPackage, ManagedUser, ManagedVersion, PackageDonationUrl, Paginated, Provider } from '@/bindings.gen';
import type { HTMLProps, JSX } from 'react';
import Modal from '@/components/overlay/Modal';
import { useBrowserContext, usePackageData, usePackageVersions } from '@/hooks/useBrowser';
import { useClusters } from '@/hooks/useCluster';
import { bindings } from '@/main';
import { abbreviateNumber, formatAsRelative, PROVIDERS } from '@/utils';
import { abbreviateNumber, formatAsRelative, PROVIDERS, upperFirst } from '@/utils';
import { useCommand } from '@onelauncher/common';
import { Button, Popup, Show, Tooltip } from '@onelauncher/common/components';
import { createFileRoute, Link } from '@tanstack/react-router';
@ -14,6 +14,8 @@ import { createContext, useContext, useEffect, useMemo, useRef, useState } from
import { Collection, ListBox, ListBoxItem, Popover, Pressable, Select, Tab, TabList, TabPanel, Tabs } from 'react-aria-components';
import Markdown from 'react-markdown';
import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm';
import { twMerge } from 'tailwind-merge';
export const Route = createFileRoute('/app/browser/package/$provider/$slug')({
component: RouteComponent,
@ -23,17 +25,19 @@ function includes<T, TArray extends T>(list: { includes: (arg0: TArray) => boole
return list.includes(element as unknown as TArray);
}
function CustomA({ href, children }: { href: string; children: any }) {
function CustomA({ href, children, includeIcon, className, ...rest }: { href: string; children: any, includeIcon?: boolean } & HTMLProps<HTMLAnchorElement>) {
return (
<a
className="text-fg-primary underline"
className={twMerge("text-fg-primary underline", className)}
href={href}
onClick={(e) => {
e.preventDefault();
openUrl(href);
}}
{...rest}
>
{children}
{includeIcon && <LinkExternal01Icon className='inline w-4 ml-1'/>}
</a>
);
}
@ -73,11 +77,13 @@ function RouteComponent() {
<Tab className="uppercase p-2.5 text-trim rounded-md selected:bg-component-bg-pressed disabled:hidden" id="gallery" isDisabled={packageData.data?.gallery.length === 0}>Gallery</Tab>
</TabList>
<div className="h-full min-h-full flex-1 w-full rounded-lg bg-component-bg p-3">
<TabPanel className="prose prose-invert prose-sm max-w-none" id="about">
<TabPanel className="prose prose-invert prose-sm max-w-none prose-code:before:content-none prose-code:after:content-none prose-code:bg-component-bg-disabled prose-code:rounded-sm prose-code:p-1! prose-code:text-trim" id="about">
<Markdown
components={{
a: ({ node, children, ...props }) => <CustomA children={children} href={props.href as string} />,
}} rehypePlugins={[rehypeRaw]}
}}
rehypePlugins={[rehypeRaw]}
remarkPlugins={[remarkGfm]}
>
{packageData.data?.body}
</Markdown>
@ -111,6 +117,14 @@ function RouteComponent() {
);
}
function getPackageUrl(pkg: ManagedPackage): string {
switch (pkg.provider){
case 'Modrinth': return `https://modrinth.com/project/${pkg.slug}`
case 'Curseforge': return `https://www.curseforge.com/minecraft/${pkg.package_type}s/${pkg.slug}`
case 'SkyClient': return ``
}
}
function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) {
const { provider } = Route.useParams();
@ -120,7 +134,7 @@ function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) {
const authors = useCommand('getUsersFromAuthor', () => bindings.core.getUsersFromAuthor(provider as Provider, pkg.author));
return (
<div className="sticky top-0 z-1 max-w-60 min-w-54 flex flex-col gap-y-4">
<div className="z-1 max-w-60 min-w-54 flex flex-col gap-y-4 mb-6">
<div className="flex flex-col overflow-hidden rounded-lg bg-component-bg">
<div className="relative h-28 flex items-center justify-center overflow-hidden">
<img alt={`Icon for ${pkg.name}`} className="absolute z-0 max-w-none w-7/6 blur-xl" src={pkg.icon_url || ''} />
@ -153,14 +167,23 @@ function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) {
<InstallButton />
{/* <div className="flex flex-col gap-2 rounded-lg bg-component-bg p-3">
<div className="flex flex-col rounded-lg bg-component-bg p-3">
<h4 className="text-fg-primary font-bold">Links</h4>
<Link href={getPackageUrl(contentPackage)} includeIcon>
{contentPackage.provider}
{' '}
Page
</Link>
</div> */}
<div className='flex flex-col'>
<CustomA href={getPackageUrl(pkg)} includeIcon className='text-link hover:text-link-hover'>
{provider}
{' '}
Page
</CustomA>
{(Object.entries(pkg.links) as [keyof typeof pkg.links, typeof pkg.links[keyof typeof pkg.links]][]).filter(a=>a[1]).map(link=>
typeof link[1] == "string"
? <CustomA href={link[1]} children={upperFirst(link[0])} className='text-link hover:text-link-hover' includeIcon/>
: <div>
{(link[1] as PackageDonationUrl[]).map(donationLink=><CustomA href={donationLink.url} className='text-link hover:text-link-hover' children={upperFirst(donationLink.id)} includeIcon/>)}
</div>
)}
</div>
</div>
<div className="flex flex-col gap-2 rounded-lg bg-component-bg p-3">
<h4 className="text-fg-primary font-bold">Authors</h4>

View file

@ -77,6 +77,7 @@ pub struct SearchResult {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ManagedPackage {
pub id: String,
pub slug: String,
pub provider: Provider,
pub package_type: PackageType,
pub name: String,

View file

@ -346,6 +346,7 @@ struct ModrinthPackage {
pub icon_url: Option<String>,
#[serde(alias = "project_id")]
pub id: String,
pub slug: String,
pub team: String,
#[serde(default)]
pub organization: Option<String>,
@ -373,6 +374,7 @@ impl From<ModrinthPackage> for ManagedPackage {
Self {
provider: Provider::Modrinth,
id: value.id,
slug: value.slug,
package_type: value.project_type,
name: value.title,
short_desc: value.description,

26
pnpm-lock.yaml generated
View file

@ -399,6 +399,9 @@ importers:
rehype-raw:
specifier: ^7.0.0
version: 7.0.0
remark-gfm:
specifier: ^4.0.1
version: 4.0.1
tailwind-merge:
specifier: 'catalog:'
version: 3.2.0
@ -5051,12 +5054,18 @@ packages:
rehype-raw@7.0.0:
resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==}
remark-gfm@4.0.1:
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
remark-parse@11.0.0:
resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==}
remark-rehype@11.1.2:
resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==}
remark-stringify@11.0.0:
resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
remove-accents@0.5.0:
resolution: {integrity: sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==}
@ -11304,6 +11313,17 @@ snapshots:
hast-util-raw: 9.1.0
vfile: 6.0.3
remark-gfm@4.0.1:
dependencies:
'@types/mdast': 4.0.4
mdast-util-gfm: 3.1.0
micromark-extension-gfm: 3.0.0
remark-parse: 11.0.0
remark-stringify: 11.0.0
unified: 11.0.5
transitivePeerDependencies:
- supports-color
remark-parse@11.0.0:
dependencies:
'@types/mdast': 4.0.4
@ -11321,6 +11341,12 @@ snapshots:
unified: 11.0.5
vfile: 6.0.3
remark-stringify@11.0.0:
dependencies:
'@types/mdast': 4.0.4
mdast-util-to-markdown: 2.1.2
unified: 11.0.5
remove-accents@0.5.0: {}
require-from-string@2.0.2: {}