Merge branch 'Polyfrost:onelauncher/main' into onelauncher/main
This commit is contained in:
commit
f08aab7d56
45 changed files with 660 additions and 250 deletions
|
|
@ -20,7 +20,7 @@ interface TargetConfig {
|
|||
}
|
||||
interface BuildTarget {
|
||||
updater: false | Updater;
|
||||
standalone: TargetConfig[];
|
||||
standalone: Array<TargetConfig>;
|
||||
}
|
||||
|
||||
const osTargets = {
|
||||
|
|
@ -67,7 +67,7 @@ const frontendBundle = 'apps/frontend/dist.tar.xz';
|
|||
const updaterName = `OneLauncher-Updater-${os}-${arch}`;
|
||||
const frontendName = `OneLauncher-Frontend-${os}-${arch}`;
|
||||
|
||||
const globFiles = async (pattern: string): Promise<string[]> => await (await glob.create(pattern)).glob();
|
||||
const globFiles = async (pattern: string): Promise<Array<string>> => await (await glob.create(pattern)).glob();
|
||||
|
||||
async function uploadFrontend() {
|
||||
if (!(await exists(frontendBundle))) {
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@
|
|||
"build": "vite build && tsc",
|
||||
"serve": "vite preview",
|
||||
"lint": "eslint",
|
||||
"format": "prettier",
|
||||
"check": "prettier --write . && eslint --fix"
|
||||
"check": "eslint --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/poppins": "catalog:",
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
// @ts-check
|
||||
|
||||
/** @type {import('prettier').Config} */
|
||||
const config = {
|
||||
semi: true,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
import { useEffect, type PropsWithChildren } from 'react';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { LoaderSuspense, Navbar } from '@/components';
|
||||
import { GameBackground } from '@/components/GameBackground';
|
||||
import useAppShellStore from '@/stores/appShellStore';
|
||||
import { AnimatedOutlet } from '@onelauncher/common/components';
|
||||
import { createFileRoute, useRouter } from '@tanstack/react-router';
|
||||
import { AnimatePresence } from 'motion/react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const Route = createFileRoute('/app')({
|
||||
component: RouteComponent,
|
||||
|
|
@ -40,12 +41,12 @@ function AppShell({
|
|||
const setPrevLocation = useAppShellStore(state => state.setPrevLocation);
|
||||
|
||||
useEffect(() => {
|
||||
const unsub = router.subscribe("onBeforeNavigate", (e) => {
|
||||
const unsub = router.subscribe('onBeforeNavigate', (e) => {
|
||||
setPrevLocation(e.fromLocation ?? null);
|
||||
});
|
||||
|
||||
return () => unsub();
|
||||
}, [router]);
|
||||
}, [router, setPrevLocation]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full w-full">
|
||||
|
|
|
|||
|
|
@ -25,5 +25,5 @@
|
|||
"skipLibCheck": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx", "prettier.config.js", "vite.config.js"]
|
||||
"include": ["**/*.ts", "**/*.tsx", "vite.config.ts"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@
|
|||
"path": "$DATA/OneLauncher"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"opener:allow-open-url",
|
||||
"opener:default"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { Provider, SearchResult } from '@/bindings.gen';
|
||||
import { abbreviateNumber, LOADERS, upperFirst } from '@/utils';
|
||||
import { Show, Tooltip } from '@onelauncher/common/components';
|
||||
import { Link, useNavigate } from '@tanstack/react-router';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { Download01Icon } from '@untitled-theme/icons-react';
|
||||
import { useMemo } from 'react';
|
||||
import { Focusable } from 'react-aria-components';
|
||||
|
|
@ -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 2xl:grid-cols-4 gap-2">
|
||||
<div className="grid grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 min-[1900px]:grid-cols-5! gap-2">
|
||||
{items.map(item => (
|
||||
<PackageItem key={item.project_id} {...item} provider={provider} />
|
||||
))}
|
||||
|
|
@ -22,7 +22,7 @@ export function PackageGrid({ items, provider }: { items: Array<SearchResult>; p
|
|||
}
|
||||
|
||||
export function PackageItem({ provider, ...item }: SearchResult & { provider: Provider }) {
|
||||
const loaders = useMemo(() => item.categories.filter(cat => includes(LOADERS, cat)), []);
|
||||
const loaders = useMemo(() => item.categories.filter(cat => includes(LOADERS, cat)), [item.categories]);
|
||||
|
||||
return (
|
||||
<Link
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ interface FormValidation {
|
|||
export function NewClusterCreate() {
|
||||
const queryClient = useQueryClient();
|
||||
const [currentStepIndex, setCurrentStepIndex] = useState(0);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [formData, setFormData] = useState<ClusterFormData>({
|
||||
loader: 'vanilla',
|
||||
clusterName: '',
|
||||
|
|
@ -63,6 +64,15 @@ export function NewClusterCreate() {
|
|||
}), {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['getClusters'] });
|
||||
// Modal'ı kapat ve formu sıfırla
|
||||
setIsModalOpen(false);
|
||||
setCurrentStepIndex(0);
|
||||
setFormData({
|
||||
loader: 'vanilla',
|
||||
clusterName: '',
|
||||
clusterVersion: '',
|
||||
clusterProvider: '',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -154,32 +164,30 @@ export function NewClusterCreate() {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<Modal.Trigger>
|
||||
<Button>
|
||||
<PlusIcon className="stroke-width-[2.2] size-5" />
|
||||
New cluster
|
||||
</Button>
|
||||
<Button onPress={() => setIsModalOpen(true)}>
|
||||
<PlusIcon className="stroke-width-[2.2] size-5" />
|
||||
New cluster
|
||||
</Button>
|
||||
|
||||
<Modal>
|
||||
<div className="min-w-sm flex flex-col rounded-lg bg-page text-center">
|
||||
<ModalHeader currentStep={currentStepConfig} />
|
||||
<Modal isDismissable isOpen={isModalOpen} onOpenChange={setIsModalOpen}>
|
||||
<div className="min-w-sm flex flex-col rounded-lg bg-page text-center">
|
||||
<ModalHeader currentStep={currentStepConfig} />
|
||||
|
||||
<div className="flex flex-col rounded-b-lg border border-white/5">
|
||||
<div className="p-3">
|
||||
{stepContent}
|
||||
</div>
|
||||
|
||||
<ModalFooter
|
||||
isFirstStep={isFirstStep}
|
||||
isNextDisabled={isNextDisabled}
|
||||
nextButtonText={nextButtonText}
|
||||
onBack={handleBack}
|
||||
onNext={handleNext}
|
||||
/>
|
||||
<div className="flex flex-col rounded-b-lg border border-white/5">
|
||||
<div className="p-3">
|
||||
{stepContent}
|
||||
</div>
|
||||
|
||||
<ModalFooter
|
||||
isFirstStep={isFirstStep}
|
||||
isNextDisabled={isNextDisabled}
|
||||
nextButtonText={nextButtonText}
|
||||
onBack={handleBack}
|
||||
onNext={handleNext}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
</Modal.Trigger>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,68 @@
|
|||
import type { DialogProps } from 'react-aria-components';
|
||||
import type { VariantProps } from 'tailwind-variants';
|
||||
import {
|
||||
Dialog as AriaDialog,
|
||||
Modal as AriaModal,
|
||||
DialogTrigger,
|
||||
ModalOverlay,
|
||||
} from 'react-aria-components';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { tv } from 'tailwind-variants';
|
||||
|
||||
interface ModalProps extends DialogProps { }
|
||||
const modalVariants = tv({
|
||||
slots: {
|
||||
overlay: [
|
||||
'fixed inset-0 z-50',
|
||||
'flex items-center justify-center',
|
||||
'bg-black/50 p-4',
|
||||
|
||||
'data-[entering]:animate-in data-[entering]:fade-in data-[entering]:duration-200',
|
||||
'data-[exiting]:animate-out data-[exiting]:fade-out data-[exiting]:duration-150',
|
||||
],
|
||||
modal: [
|
||||
'data-[entering]:animate-in data-[entering]:zoom-in-95 data-[entering]:duration-200',
|
||||
'data-[exiting]:animate-out data-[exiting]:zoom-out-95 data-[exiting]:duration-150',
|
||||
],
|
||||
dialog: [
|
||||
'outline-none',
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
export interface ModalProps extends DialogProps, VariantProps<typeof modalVariants> {
|
||||
isOpen?: boolean;
|
||||
onOpenChange?: (isOpen: boolean) => void;
|
||||
isDismissable?: boolean;
|
||||
overlayClassName?: string;
|
||||
modalClassName?: string;
|
||||
}
|
||||
|
||||
function Modal({
|
||||
className,
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
isDismissable = true,
|
||||
overlayClassName,
|
||||
modalClassName,
|
||||
...props
|
||||
}: ModalProps) {
|
||||
const { overlay, modal, dialog } = modalVariants();
|
||||
|
||||
const isControlled = isOpen !== undefined || onOpenChange !== undefined;
|
||||
|
||||
const overlayProps = {
|
||||
className: twMerge(overlay(), overlayClassName),
|
||||
isDismissable,
|
||||
...(isControlled && { isOpen, onOpenChange }),
|
||||
};
|
||||
|
||||
function Modal({ ...props }: ModalProps) {
|
||||
return (
|
||||
<ModalOverlay className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4" isDismissable>
|
||||
<AriaModal>
|
||||
<AriaDialog {...props} />
|
||||
<ModalOverlay {...overlayProps}>
|
||||
<AriaModal className={twMerge(modal(), modalClassName)}>
|
||||
<AriaDialog
|
||||
className={twMerge(dialog(), className)}
|
||||
{...props}
|
||||
/>
|
||||
</AriaModal>
|
||||
</ModalOverlay>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -32,7 +32,12 @@ export function BrowserProvider(props: any) {
|
|||
const [provider, setProvider] = useState<Provider>(PROVIDERS[0]);
|
||||
const navigate = useNavigate();
|
||||
const [query, setQuery] = useState<SearchQuery>({
|
||||
filters: null,
|
||||
filters: {
|
||||
categories: null,
|
||||
game_versions: null,
|
||||
loaders: null,
|
||||
package_types: ['mod'],
|
||||
},
|
||||
query: null,
|
||||
limit: 18 as unknown as bigint,
|
||||
offset: null,
|
||||
|
|
@ -42,7 +47,7 @@ export function BrowserProvider(props: any) {
|
|||
useEffect(() => {
|
||||
if (query.filters?.categories || query.query)
|
||||
navigate({ to: '/app/browser/search' });
|
||||
}, [query]);
|
||||
}, [navigate, query]);
|
||||
|
||||
const context = useMemo<BrowserControllerType>(() => ({
|
||||
cluster,
|
||||
|
|
@ -70,6 +75,6 @@ export function usePackageData(provider: Provider, slug: string, options?: Omit<
|
|||
return useCommand(key, () => bindings.core.getPackage(provider, slug), options);
|
||||
}
|
||||
|
||||
export function usePackageVersions(provider: Provider, slug: string, { mc_versions, loaders, offset, limit, ...options }: { mc_versions?: Array<string> | null; loaders?: Array<GameLoader> | null; offset?: number; limit: number } & Omit<UndefinedInitialDataOptions<Paginated<ManagedVersion>>, 'queryKey' | 'queryFn'>) {
|
||||
return useCommand('getPackageVersions', () => bindings.core.getPackageVersions(provider, slug, mc_versions ?? null, loaders ?? null, (offset ?? 0) as unknown as bigint, limit as unknown as bigint), options);
|
||||
export function usePackageVersions(provider: Provider, slug: string, { mc_versions, loaders, offset, limit, ...options }: { mc_versions?: Array<string> | null; loaders?: Array<GameLoader> | null; offset?: number; limit: number } & Omit<UndefinedInitialDataOptions<Paginated<ManagedVersion>>, 'queryKey' | 'queryFn'>, key: false | BindingCommands | (string & {}) = `getPackageVersions.${provider}.${slug}.${mc_versions?.join('')}.${loaders?.join('')}`) {
|
||||
return useCommand(key, () => bindings.core.getPackageVersions(provider, slug, mc_versions ?? null, loaders ?? null, (offset ?? 0) as unknown as bigint, limit as unknown as bigint), options);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,54 +0,0 @@
|
|||
import type { ClusterModel } from '@/bindings.gen';
|
||||
import { bindings } from '@/main';
|
||||
import { useCommand } from '@onelauncher/common';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function useRecentCluster() {
|
||||
const result = useCommand('getClusters', bindings.core.getClusters);
|
||||
const [cluster, setCluster] = useState<ClusterModel | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!result.data)
|
||||
return;
|
||||
|
||||
let mostRecentCluster: ClusterModel | undefined;
|
||||
|
||||
for (const c of result.data) {
|
||||
if (!mostRecentCluster) {
|
||||
mostRecentCluster = c;
|
||||
continue;
|
||||
}
|
||||
|
||||
const currentPlayed = mostRecentCluster.last_played;
|
||||
const newPlayed = c.last_played;
|
||||
|
||||
if (typeof currentPlayed !== 'string' && typeof newPlayed === 'string') {
|
||||
mostRecentCluster = c;
|
||||
}
|
||||
else if (typeof currentPlayed === 'string' && typeof newPlayed === 'string') {
|
||||
const playedAt = new Date(currentPlayed);
|
||||
const clusterPlayedAt = new Date(newPlayed);
|
||||
|
||||
if (clusterPlayedAt > playedAt)
|
||||
mostRecentCluster = c;
|
||||
}
|
||||
}
|
||||
|
||||
setCluster(mostRecentCluster);
|
||||
}, [result.data]);
|
||||
|
||||
return cluster;
|
||||
}
|
||||
|
||||
export function useClusters() {
|
||||
const result = useCommand('getClusters', bindings.core.getClusters);
|
||||
const [clusters, setClusters] = useState<Array<ClusterModel> | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!result.data)
|
||||
return;
|
||||
setClusters(result.data);
|
||||
}, [result.data]);
|
||||
|
||||
return clusters;
|
||||
}
|
||||
167
apps/onelauncher/frontend/src/hooks/useCluster.tsx
Normal file
167
apps/onelauncher/frontend/src/hooks/useCluster.tsx
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
import type { ClusterModel } from '@/bindings.gen';
|
||||
import type { ModalProps } from '@/components/overlay/Modal';
|
||||
import Modal from '@/components/overlay/Modal';
|
||||
import { bindings } from '@/main';
|
||||
import { useCommand } from '@onelauncher/common';
|
||||
import { Button, Dropdown } from '@onelauncher/common/components';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Heading } from 'react-aria-components';
|
||||
|
||||
export function useRecentCluster() {
|
||||
const result = useCommand('getClusters', bindings.core.getClusters);
|
||||
const [cluster, setCluster] = useState<ClusterModel | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!result.data)
|
||||
return;
|
||||
|
||||
let mostRecentCluster: ClusterModel | undefined;
|
||||
|
||||
for (const c of result.data) {
|
||||
if (!mostRecentCluster) {
|
||||
mostRecentCluster = c;
|
||||
continue;
|
||||
}
|
||||
|
||||
const currentPlayed = mostRecentCluster.last_played;
|
||||
const newPlayed = c.last_played;
|
||||
|
||||
if (typeof currentPlayed !== 'string' && typeof newPlayed === 'string') {
|
||||
mostRecentCluster = c;
|
||||
}
|
||||
else if (typeof currentPlayed === 'string' && typeof newPlayed === 'string') {
|
||||
const playedAt = new Date(currentPlayed);
|
||||
const clusterPlayedAt = new Date(newPlayed);
|
||||
|
||||
if (clusterPlayedAt > playedAt)
|
||||
mostRecentCluster = c;
|
||||
}
|
||||
}
|
||||
|
||||
setCluster(mostRecentCluster);
|
||||
}, [result.data]);
|
||||
|
||||
return cluster;
|
||||
}
|
||||
|
||||
export function useClusters() {
|
||||
const result = useCommand('getClusters', bindings.core.getClusters);
|
||||
const [clusters, setClusters] = useState<Array<ClusterModel> | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!result.data)
|
||||
return;
|
||||
setClusters(result.data);
|
||||
}, [result.data]);
|
||||
|
||||
return clusters;
|
||||
}
|
||||
|
||||
type ChooseClusterModalProps = ModalProps & {
|
||||
clusters?: Array<ClusterModel>;
|
||||
confirmText?: string;
|
||||
selected?: (clusters: Array<ClusterModel>) => number;
|
||||
onSelected?: (cluster: ClusterModel) => void;
|
||||
};
|
||||
|
||||
export function ChooseClusterModal({ clusters: clusterList, selected: defaultSelected, onSelected, confirmText, ...props }: ChooseClusterModalProps) {
|
||||
const clusters = useClusters();
|
||||
const [selected, setSelected] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!clusters)
|
||||
return;
|
||||
|
||||
const newSelected = defaultSelected?.(clusters);
|
||||
|
||||
if (newSelected)
|
||||
setSelected(newSelected);
|
||||
}, [clusters, defaultSelected]);
|
||||
|
||||
function chooseCluster() {
|
||||
if (clusters !== undefined) {
|
||||
const cluster = clusters[selected] as ClusterModel | undefined;
|
||||
if (cluster !== undefined)
|
||||
onSelected?.(cluster);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal {...props}>
|
||||
<div className="min-w-sm flex flex-col rounded-lg bg-page text-center p-4 gap-2">
|
||||
|
||||
<Heading className="text-xl font-semibold" slot="title">Select a Cluster</Heading>
|
||||
<Dropdown
|
||||
onSelectionChange={(index) => {
|
||||
setSelected(index as number);
|
||||
}}
|
||||
selectedKey={selected}
|
||||
>
|
||||
{clusters?.map((cluster, index) => (
|
||||
<Dropdown.Item id={index} key={cluster.id}>
|
||||
{cluster.name}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
children="Close"
|
||||
color="secondary"
|
||||
slot="close"
|
||||
/>
|
||||
<Button
|
||||
children={confirmText ?? 'Save'}
|
||||
onClick={chooseCluster}
|
||||
slot="close"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
// return (
|
||||
// <Modal
|
||||
// {...props}
|
||||
// buttons={[
|
||||
// <Button
|
||||
// buttonStyle="secondary"
|
||||
// children="Close"
|
||||
// onClick={props.hide}
|
||||
// />,
|
||||
// <Button
|
||||
// children="Save"
|
||||
// onClick={chooseCluster}
|
||||
// />,
|
||||
// ]}
|
||||
// children={(
|
||||
// <Switch>
|
||||
// <Match when={clusters() !== undefined}>
|
||||
// <Dropdown
|
||||
// disabled={clusters()?.length === 0}
|
||||
// onChange={setSelected}
|
||||
// selected={selected}
|
||||
// >
|
||||
// <Switch>
|
||||
// <Match when={(clusters()?.length || 0) > 0}>
|
||||
// <For each={clusters()!}>
|
||||
// {cluster => (
|
||||
// <Dropdown.Row>{cluster.meta.name}</Dropdown.Row>
|
||||
// )}
|
||||
// </For>
|
||||
// </Match>
|
||||
// <Match when>
|
||||
// <Dropdown.Row>No clusters found</Dropdown.Row>
|
||||
// </Match>
|
||||
// </Switch>
|
||||
// </Dropdown>
|
||||
// </Match>
|
||||
// <Match when>
|
||||
// <div>Loading...</div>
|
||||
// </Match>
|
||||
// </Switch>
|
||||
// )}
|
||||
// title="Choose a cluster"
|
||||
// />
|
||||
// );
|
||||
}
|
||||
|
|
@ -46,20 +46,20 @@ function usePagination(options: PaginationOptions) {
|
|||
return pages;
|
||||
}
|
||||
|
||||
function PaginationBtn(props: { page: number }) {
|
||||
function PaginationBtn({ page: pageIndex }: { page: number }) {
|
||||
return (
|
||||
<Button
|
||||
className="p-2 text-lg min-w-8"
|
||||
color={props.page === page ? 'primary' : 'secondary'}
|
||||
color={pageIndex === page ? 'primary' : 'secondary'}
|
||||
|
||||
onClick={() => {
|
||||
if (props.page === page)
|
||||
if (pageIndex === page)
|
||||
return;
|
||||
|
||||
setPage(props.page);
|
||||
setPage(pageIndex);
|
||||
}}
|
||||
>
|
||||
<span className="text-trim">{props.page}</span>
|
||||
<span className="text-trim">{pageIndex}</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import type { Settings } from '@/bindings.gen';
|
||||
import { bindings } from '@/main';
|
||||
import { useCommand } from '@onelauncher/common';
|
||||
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
||||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
interface SettingsControllerType {
|
||||
settings: Settings | undefined;
|
||||
|
|
@ -12,7 +12,7 @@ interface SettingsControllerType {
|
|||
setSettingsToSave: (settings: Partial<Settings> | null) => void;
|
||||
settingsChanged: boolean;
|
||||
setSettingsChanged: (changed: boolean) => void;
|
||||
createSetting: <K extends keyof Settings, V = Settings[K]>(name: K, value: V) => [V, (value: V) => void];
|
||||
createSetting: <K extends keyof Settings, V = Settings[K]>(name: K, value: V) => [V, (v: V) => void];
|
||||
}
|
||||
|
||||
const SettingsContext = createContext<SettingsControllerType | null>(null);
|
||||
|
|
@ -51,14 +51,14 @@ export function SettingsProvider({ children }: SettingsProviderProps) {
|
|||
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
}, []);
|
||||
|
||||
const saveChangedSettings = () => {
|
||||
const saveChangedSettings = useCallback(() => {
|
||||
if (!settings)
|
||||
return;
|
||||
|
||||
writeQuery.refetch();
|
||||
|
||||
setSettingsChanged(false);
|
||||
};
|
||||
}, [settings, writeQuery]);
|
||||
|
||||
const handleSetSettingsToSave = useCallback((newSettings: Partial<Settings> | null) => {
|
||||
setSettingsToSave(newSettings);
|
||||
|
|
@ -86,7 +86,7 @@ export function SettingsProvider({ children }: SettingsProviderProps) {
|
|||
return [value, setValue];
|
||||
}, [settings]);
|
||||
|
||||
const controller: SettingsControllerType = {
|
||||
const controller: SettingsControllerType = useMemo(() => ({
|
||||
settings,
|
||||
saveChangedSettings,
|
||||
settingsToSave,
|
||||
|
|
@ -94,7 +94,7 @@ export function SettingsProvider({ children }: SettingsProviderProps) {
|
|||
settingsChanged,
|
||||
setSettingsChanged,
|
||||
createSetting,
|
||||
};
|
||||
}), [createSetting, handleSetSettingsToSave, saveChangedSettings, settings, settingsChanged, settingsToSave]);
|
||||
|
||||
if (settings === undefined)
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ function Featured() {
|
|||
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">
|
||||
|
|
@ -40,7 +41,7 @@ function Featured() {
|
|||
<div className="w-full p-1">
|
||||
<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-64 min-w-52 flex flex-col gap-y-1 p-4">
|
||||
<div className="max-w-64 min-[1900px]:max-w-96 min-w-52 flex flex-col gap-y-1 p-4">
|
||||
<h2 className="text-[1.5rem] font-semibold">{featuredPackage.data?.name}</h2>
|
||||
|
||||
<Show when={false}>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,22 @@
|
|||
import type { ClusterModel, ManagedPackage, ManagedUser, ManagedVersion, Paginated, Provider } from '@/bindings.gen';
|
||||
import type { ClusterModel, ManagedPackage, ManagedUser, ManagedVersion, PackageDonationUrl, Provider } from '@/bindings.gen';
|
||||
import type { HTMLProps } from 'react';
|
||||
import Modal from '@/components/overlay/Modal';
|
||||
import { useBrowserContext, usePackageData, usePackageVersions } from '@/hooks/useBrowser';
|
||||
import { useClusters } from '@/hooks/useCluster';
|
||||
import { ChooseClusterModal, useClusters } from '@/hooks/useCluster';
|
||||
import usePagination from '@/hooks/usePagination';
|
||||
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 { Button, Show, Tooltip } from '@onelauncher/common/components';
|
||||
import { createFileRoute, Link } from '@tanstack/react-router';
|
||||
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||
import { CalendarIcon, ChevronDownIcon, ChevronUpIcon, ClockRewindIcon, Download01Icon, File02Icon, LinkExternal01Icon } from '@untitled-theme/icons-react';
|
||||
import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Collection, ListBox, ListBoxItem, Popover, Pressable, Select, Tab, TabList, TabPanel, Tabs } from 'react-aria-components';
|
||||
import { createContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Cell, Collection, Column, ListBox, ListBoxItem, Popover, Pressable, Row, Select, Tab, Table, TableBody, TableHeader, 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,
|
||||
|
|
@ -22,14 +26,29 @@ function includes<T, TArray extends T>(list: { includes: (arg0: TArray) => boole
|
|||
return list.includes(element as unknown as TArray);
|
||||
}
|
||||
|
||||
function CustomA({ href, children, includeIcon, className, ...rest }: { href: string; children: any; includeIcon?: boolean } & HTMLProps<HTMLAnchorElement>) {
|
||||
return (
|
||||
<a
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
interface PackageContextType {
|
||||
pkg: ManagedPackage | undefined;
|
||||
versions: Paginated<ManagedVersion> | undefined;
|
||||
}
|
||||
|
||||
const PackageContext = createContext<PackageContextType>({
|
||||
pkg: undefined,
|
||||
versions: undefined,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
|
|
@ -37,15 +56,12 @@ function RouteComponent() {
|
|||
if (!includes(PROVIDERS, provider))
|
||||
throw new Error('Invalid provider');
|
||||
const packageData = usePackageData(provider, slug, {});
|
||||
const browserContext = useBrowserContext();
|
||||
const { data: versions } = usePackageVersions(provider, slug, {
|
||||
mc_versions: browserContext.cluster ? [browserContext.cluster.mc_version] : null,
|
||||
loaders: browserContext.cluster ? [browserContext.cluster.mc_loader] : null,
|
||||
limit: 50,
|
||||
});
|
||||
const packageContextValue = useMemo(() => ({
|
||||
pkg: packageData.data,
|
||||
}), [packageData.data]);
|
||||
|
||||
return (
|
||||
<PackageContext value={{ pkg: packageData.data, versions }}>
|
||||
<PackageContext value={packageContextValue}>
|
||||
<Show when={packageData.isSuccess && !packageData.isFetching}>
|
||||
<div className="h-full flex flex-1 flex-row items-start gap-x-4">
|
||||
<BrowserSidebar package={packageData.data!} />
|
||||
|
|
@ -56,31 +72,39 @@ function RouteComponent() {
|
|||
<Tab className="uppercase p-2.5 text-trim rounded-md selected:bg-component-bg-pressed disabled:hidden" id="versions">Versions</Tab>
|
||||
<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">
|
||||
<Markdown rehypePlugins={[rehypeRaw]}>{packageData.data?.body}</Markdown>
|
||||
</TabPanel>
|
||||
<TabPanel id="versions">
|
||||
versions
|
||||
</TabPanel>
|
||||
<TabPanel id="gallery" className="flex flex-wrap gap-2 justify-center">
|
||||
<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 prose-img:inline-block prose-a:inline-block" id="about">
|
||||
<div className="h-full min-h-full flex-1 w-full rounded-lg bg-component-bg p-3">
|
||||
<Markdown
|
||||
components={{
|
||||
a: ({ node, children, ...props }) => <CustomA children={children} href={props.href as string} />,
|
||||
}}
|
||||
rehypePlugins={[rehypeRaw]}
|
||||
remarkPlugins={[remarkGfm]}
|
||||
>
|
||||
{packageData.data?.body}
|
||||
</Markdown>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel id="versions">
|
||||
<Versions />
|
||||
</TabPanel>
|
||||
<TabPanel className="flex flex-wrap gap-2 justify-center" id="gallery">
|
||||
|
||||
{packageData.data?.gallery.map(item => (
|
||||
<Modal.Trigger key={item.url}>
|
||||
<Pressable>
|
||||
<div className='rounded-md overflow-hidden bg-component-bg-pressed outline-0 h-64 flex flex-col relative'>
|
||||
<img className='h-full' src={item.thumbnail_url} />
|
||||
{item.title && <div className='absolute w-full bottom-0 bg-component-bg-disabled/80 p-2'>{item.title}</div>}
|
||||
</div>
|
||||
</Pressable>
|
||||
<Modal className='w-full' >
|
||||
<img className='rounded-xl' src={item.url} />
|
||||
</Modal>
|
||||
</Modal.Trigger>
|
||||
))}
|
||||
{packageData.data?.gallery.map(item => (
|
||||
<Modal.Trigger key={item.url}>
|
||||
<Pressable>
|
||||
<div aria-label="Loaders" className="rounded-md overflow-hidden bg-component-bg-pressed outline-0 h-64 flex flex-col relative">
|
||||
<img className="h-full" src={item.thumbnail_url} />
|
||||
{item.title && <div className="absolute w-full bottom-0 bg-component-bg-disabled/80 p-2">{item.title}</div>}
|
||||
</div>
|
||||
</Pressable>
|
||||
<Modal className="w-full">
|
||||
<img className="rounded-xl" src={item.url} />
|
||||
</Modal>
|
||||
</Modal.Trigger>
|
||||
))}
|
||||
|
||||
</TabPanel>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
</div>
|
||||
|
|
@ -89,16 +113,24 @@ 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();
|
||||
|
||||
const createdAt = useMemo(() => pkg.created ? new Date(pkg.created) : null, []);
|
||||
const updatedAt = useMemo(() => pkg.updated ? new Date(pkg.updated) : null, []);
|
||||
const createdAt = useMemo(() => pkg.created ? new Date(pkg.created) : null, [pkg.created]);
|
||||
const updatedAt = useMemo(() => pkg.updated ? new Date(pkg.updated) : null, [pkg.updated]);
|
||||
|
||||
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 || ''} />
|
||||
|
|
@ -131,14 +163,36 @@ 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 className="text-link hover:text-link-hover" href={getPackageUrl(pkg)} includeIcon>
|
||||
{provider}
|
||||
{' '}
|
||||
Page
|
||||
</CustomA>
|
||||
{(Object.entries(pkg.links) as Array<[keyof typeof pkg.links, typeof pkg.links[keyof typeof pkg.links]]>).filter(a => a[1]).map(link =>
|
||||
typeof link[1] == 'string'
|
||||
? (
|
||||
<CustomA
|
||||
children={upperFirst(link[0])}
|
||||
className="text-link hover:text-link-hover"
|
||||
href={link[1]}
|
||||
includeIcon
|
||||
key={link[0]}
|
||||
/>
|
||||
)
|
||||
: (link[1] as Array<PackageDonationUrl>).map(donationLink => (
|
||||
<CustomA
|
||||
children={upperFirst(donationLink.id)}
|
||||
className="text-link hover:text-link-hover"
|
||||
href={donationLink.url}
|
||||
includeIcon
|
||||
key={donationLink.id}
|
||||
/>
|
||||
)))}
|
||||
</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>
|
||||
|
|
@ -193,17 +247,21 @@ function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) {
|
|||
|
||||
function InstallButton() {
|
||||
const triggerRef = useRef<HTMLDivElement>(null);
|
||||
const { provider } = Route.useParams();
|
||||
const { provider, slug } = Route.useParams();
|
||||
if (!includes(PROVIDERS, provider))
|
||||
throw new Error('invalid provider');
|
||||
const [open, setOpen] = useState(false);
|
||||
const clusters = useClusters();
|
||||
const browserContext = useBrowserContext();
|
||||
const { versions } = useContext(PackageContext);
|
||||
const { data: versions } = usePackageVersions(provider, slug, {
|
||||
mc_versions: browserContext.cluster ? [browserContext.cluster.mc_version] : [],
|
||||
loaders: browserContext.cluster ? [browserContext.cluster.mc_loader] : [],
|
||||
limit: 1,
|
||||
});
|
||||
const version = useMemo(() => {
|
||||
if (!versions || !browserContext.cluster)
|
||||
return undefined;
|
||||
return versions.items.findLast(version =>
|
||||
version.mc_versions.includes(browserContext.cluster!.mc_version)
|
||||
&& version.loaders.includes(browserContext.cluster!.mc_loader));
|
||||
return versions.items[0];
|
||||
}, [browserContext.cluster, versions]);
|
||||
|
||||
function download() {
|
||||
|
|
@ -248,7 +306,7 @@ function InstallButton() {
|
|||
aria-label="cluster" isOpen={open}
|
||||
onOpenChange={setOpen}
|
||||
onSelectionChange={(e) => {
|
||||
const cluster = clusters?.find(cluster => cluster.id as unknown as number === e);
|
||||
const cluster = clusters?.find(item => item.id as unknown as number === e);
|
||||
if (cluster)
|
||||
browserContext.setCluster(cluster);
|
||||
}}
|
||||
|
|
@ -303,6 +361,97 @@ function Author({ author }: { author: ManagedUser }) {
|
|||
);
|
||||
}
|
||||
|
||||
function colorForType(type: string) {
|
||||
switch (type.toLowerCase()) {
|
||||
case 'release':
|
||||
return 'bg-code-trace/30';
|
||||
case 'snapshot':
|
||||
return 'bg-code-debug/30';
|
||||
case 'beta':
|
||||
return 'bg-code-warn/30';
|
||||
case 'alpha':
|
||||
return 'bg-code-error/30';
|
||||
default:
|
||||
return 'bg-border/30';
|
||||
}
|
||||
}
|
||||
|
||||
function Versions() {
|
||||
const { provider, slug } = Route.useParams();
|
||||
if (!includes(PROVIDERS, provider))
|
||||
throw new Error('invalid provider');
|
||||
const [offset, setOffset] = useState(0);
|
||||
const { data: versions } = usePackageVersions(provider, slug, {
|
||||
limit: 20,
|
||||
offset,
|
||||
});
|
||||
|
||||
const pagination = usePagination({
|
||||
itemsCount: versions?.total as unknown as number,
|
||||
itemsPerPage: versions?.limit as unknown as number,
|
||||
});
|
||||
useEffect(() => {
|
||||
setOffset(pagination.offset);
|
||||
}, [pagination.offset]);
|
||||
useEffect(() => {
|
||||
pagination.reset();
|
||||
}, [versions?.total]);
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex justify-between">
|
||||
<pagination.Navigation />
|
||||
</div>
|
||||
<Table className="border-separate border-spacing-x-none border-spacing-y-1">
|
||||
<TableHeader className="text-left">
|
||||
<Column className="pr-2" />
|
||||
<Column className="pr-4" isRowHeader>Name</Column>
|
||||
<Column className="pr-4">Game Versions</Column>
|
||||
<Column className="pr-4">Loaders</Column>
|
||||
<Column className="pr-4">Created</Column>
|
||||
<Column className="pr-4">Downloads</Column>
|
||||
<Column className="pr-4" />
|
||||
</TableHeader>
|
||||
<TableBody className="">
|
||||
{versions?.items.map(item =>
|
||||
<VersionRow key={item.version_id} version={item} />)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function VersionRow({ version }: { version: ManagedVersion }) {
|
||||
const { provider } = Route.useParams();
|
||||
if (!includes(PROVIDERS, provider))
|
||||
throw new Error('invalid provider');
|
||||
return (
|
||||
<Row className="my-2 bg-page-elevated px-4 [&>td]:py-4">
|
||||
<Cell className="p-4 my-2 bg-component-bg rounded-l-xl">
|
||||
<Tooltip text={upperFirst(version.release_type)}>
|
||||
<div className={`${colorForType(version.release_type)} h-8 w-8 flex items-center justify-center rounded-md`}>
|
||||
<span className="font-bold">{version.release_type.charAt(0).toUpperCase()}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Cell>
|
||||
<Cell className="p-4 px-2 bg-component-bg">{version.display_name}</Cell>
|
||||
<Cell className="p-4 pl-0 bg-component-bg">{version.mc_versions.join(', ')}</Cell>
|
||||
<Cell className="p-4 pl-0 bg-component-bg">{version.loaders.map(upperFirst).join(', ')}</Cell>
|
||||
<Cell className="p-4 pl-0 bg-component-bg">{formatAsRelative(Date.parse(version.published))}</Cell>
|
||||
<Cell className="p-4 pl-0 bg-component-bg">{version.downloads}</Cell>
|
||||
<Cell className="p-4 pl-0 bg-component-bg rounded-r-xl">
|
||||
<Modal.Trigger>
|
||||
<Button
|
||||
children={<Download01Icon />}
|
||||
color="secondary"
|
||||
size="icon"
|
||||
/>
|
||||
<ChooseClusterModal confirmText="Download" onSelected={cluster => downloadPackage(cluster, provider, version, true)} />
|
||||
</Modal.Trigger>
|
||||
</Cell>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
function downloadPackage(cluster: ClusterModel, provider: Provider, version: ManagedVersion, skipCompatibility = false) {
|
||||
return bindings.core.downloadPackage(provider, version.project_id, version.version_id, cluster.id, skipCompatibility);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import type { Filters } from '@/bindings.gen';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import ProviderIcon from '@/components/content/ProviderIcon';
|
||||
import { BrowserProvider, useBrowserContext } from '@/hooks/useBrowser';
|
||||
import { useClusters } from '@/hooks/useCluster';
|
||||
import { PROVIDERS } from '@/utils';
|
||||
import { browserCategories } from '@/utils/browser';
|
||||
import { AnimatedOutlet, Dropdown, Show, TextField } from '@onelauncher/common/components';
|
||||
import { createFileRoute, Outlet } from '@tanstack/react-router';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { SearchMdIcon } from '@untitled-theme/icons-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
|
|
@ -27,10 +28,10 @@ function RouteComponent() {
|
|||
);
|
||||
}
|
||||
|
||||
export function BrowserLayout(props: any) {
|
||||
export function BrowserLayout({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<div className="relative h-full flex flex-1 flex-col items-center gap-2">
|
||||
<div className="h-full w-full max-w-screen-2xl flex flex-1 flex-col items-center gap-y-2">
|
||||
<div className="h-full w-full 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 />
|
||||
|
|
@ -42,7 +43,7 @@ export function BrowserLayout(props: any) {
|
|||
|
||||
<div className="h-full flex flex-col gap-y-4">
|
||||
<div className="h-full flex-1">
|
||||
{props.children}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -56,11 +57,14 @@ export function BrowserLayout(props: any) {
|
|||
function BrowserSidebar() {
|
||||
const context = useBrowserContext();
|
||||
const clusters = useClusters();
|
||||
|
||||
useEffect(() => {
|
||||
const game_versions = context.cluster?.mc_version ? [context.cluster.mc_version] : null;
|
||||
const loaders = context.cluster?.mc_loader ? [context.cluster.mc_loader] : null;
|
||||
context.setQuery({ ...context.query, filters: { ...defaultFilters, ...context.query.filters, game_versions, loaders } });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- loop
|
||||
}, [context.cluster]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div className="flex flex-col gap-y-4">
|
||||
|
|
@ -68,7 +72,7 @@ function BrowserSidebar() {
|
|||
<h6 className="my-1 uppercase opacity-60">Active Cluster</h6>
|
||||
<Dropdown
|
||||
onSelectionChange={(id) => {
|
||||
const cluster = clusters?.find(cluster => cluster.id.toString() === id);
|
||||
const cluster = clusters?.find(item => item.id.toString() === id);
|
||||
context.setCluster(cluster);
|
||||
}}
|
||||
selectedKey={context.cluster?.id.toString()}
|
||||
|
|
@ -107,7 +111,7 @@ const defaultFilters: Filters = {
|
|||
|
||||
function BrowserCategories() {
|
||||
const context = useBrowserContext();
|
||||
const categories = browserCategories.byPackageType('mod', context.provider);
|
||||
const categories = browserCategories.byPackageType((context.query.filters?.package_types ?? ['mod'])[0], context.provider);
|
||||
|
||||
function switchCategory(category: string) {
|
||||
const newCategories = context.query.filters?.categories?.includes(category)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import type { Paginated, SearchResult } from '@/bindings.gen';
|
||||
import type { PaginationOptions } from '@/hooks/usePagination';
|
||||
import { PackageGrid } from '@/components/content/PackageItem';
|
||||
import { useBrowserContext, useBrowserSearch } from '@/hooks/useBrowser';
|
||||
import usePagination from '@/hooks/usePagination';
|
||||
import { Show } from '@onelauncher/common/components';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { BrowserLayout } from './route';
|
||||
|
||||
export const Route = createFileRoute('/app/browser/search')({
|
||||
|
|
@ -25,10 +23,12 @@ function RouteComponent() {
|
|||
function Search() {
|
||||
const context = useBrowserContext();
|
||||
const search = useBrowserSearch(context.provider, context.query, {
|
||||
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
search.refetch();
|
||||
}, [context.provider, context.query, context.cluster]);
|
||||
}, [context.provider, context.query, context.cluster, search]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -44,19 +44,23 @@ function Search() {
|
|||
function Results({ results }: { results: Paginated<SearchResult> }) {
|
||||
const context = useBrowserContext();
|
||||
const [oldTotal, setOldTotal] = useState(results.total);
|
||||
|
||||
const pagination = usePagination({
|
||||
itemsCount: results.total as unknown as number,
|
||||
itemsPerPage: context.query.limit as unknown as number,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
context.setQuery({ ...context.query, offset: pagination.offset as unknown as bigint });
|
||||
}, [pagination.page]);
|
||||
}, [context, pagination.offset, pagination.page]);
|
||||
|
||||
useEffect(() => {
|
||||
if (oldTotal === results.total)
|
||||
return;
|
||||
pagination.reset();
|
||||
setOldTotal(results.total);
|
||||
}, [results.total]);
|
||||
}, [oldTotal, pagination, results.total]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="w-full flex justify-end my-2">
|
||||
|
|
|
|||
|
|
@ -48,9 +48,7 @@ function RouteComponent() {
|
|||
})();
|
||||
}, [cluster.data?.folder_name]);
|
||||
|
||||
const openClusterDir = async () => {
|
||||
openPath(launcherDir);
|
||||
};
|
||||
const openClusterDir = () => openPath(launcherDir);
|
||||
|
||||
async function handleEditMode() {
|
||||
if (edit)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import Modal from '@/components/overlay/Modal';
|
||||
import ScrollableContainer from '@/components/ScrollableContainer';
|
||||
import { bindings } from '@/main';
|
||||
import { useCommand } from '@onelauncher/common';
|
||||
|
|
@ -6,6 +7,7 @@ import { createFileRoute } from '@tanstack/react-router';
|
|||
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||
import { dataDir, join } from '@tauri-apps/api/path';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Pressable } from 'react-aria-components';
|
||||
import Sidebar from '../settings/route';
|
||||
|
||||
export const Route = createFileRoute('/app/cluster/screenshots')({
|
||||
|
|
@ -60,15 +62,20 @@ function ScreenshotEntry({
|
|||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="flex flex-col items-center gap-3 rounded-xl bg-component-bg p-3 active:bg-component-bg-pressed hover:bg-component-bg-hover hover:opacity-80 cursor-pointer"
|
||||
>
|
||||
<img
|
||||
alt={path}
|
||||
className="aspect-video w-full rounded-lg"
|
||||
src={imagePath}
|
||||
/>
|
||||
</div>
|
||||
<Modal.Trigger>
|
||||
<Pressable>
|
||||
<div className="rounded-md overflow-hidden bg-component-bg-pressed outline-0 h-64 flex flex-col relative">
|
||||
<img
|
||||
alt={path}
|
||||
className="aspect-video w-full rounded-lg"
|
||||
src={imagePath}
|
||||
/>
|
||||
</div>
|
||||
</Pressable>
|
||||
<Modal className="w-full">
|
||||
<img className="rounded-xl" src={imagePath} />
|
||||
</Modal>
|
||||
</Modal.Trigger>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { NewClusterCreate } from '@/components/launcher/cluster/ClusterCreation'
|
|||
import { useRecentCluster } from '@/hooks/useCluster';
|
||||
import { bindings } from '@/main';
|
||||
import { formatAsDuration, upperFirst } from '@/utils';
|
||||
import { useCommand, useCommandMut, useCommandSuspense } from '@onelauncher/common';
|
||||
import { useCommand, useCommandMut } from '@onelauncher/common';
|
||||
import { Button, Show } from '@onelauncher/common/components';
|
||||
import { createFileRoute, Link } from '@tanstack/react-router';
|
||||
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||
|
|
|
|||
|
|
@ -23,9 +23,7 @@ function RouteComponent() {
|
|||
})();
|
||||
}, []);
|
||||
|
||||
const openLauncherDir = async () => {
|
||||
openPath(launcherDir);
|
||||
};
|
||||
const openLauncherDir = () => openPath(launcherDir);
|
||||
|
||||
const [discordRpc, setDiscordRpc] = createSetting('discord_enabled', settings?.discord_enabled);
|
||||
|
||||
|
|
|
|||
|
|
@ -86,27 +86,28 @@ function Sidebar(props: SidebarProps) {
|
|||
|
||||
return (
|
||||
<div className="w-56 flex flex-col pr-2">
|
||||
{Object.keys(links).map((section, i) => (
|
||||
<div className="flex flex-col gap-y-2" key={i}>
|
||||
{Object.keys(links).map(section => (
|
||||
<div className="flex flex-col gap-y-2" key={section}>
|
||||
<div>
|
||||
<p className="m-1.5 mt-5 text-xs text-fg-secondary font-medium">{section.toUpperCase()}</p>
|
||||
<div className="flex flex-col gap-y-1 fill-fg-primary text-fg-primary font-medium">
|
||||
{links[section].map((link, i) => {
|
||||
// eslint yaps too much tbh
|
||||
if (!link)
|
||||
{links[section].map((entry) => {
|
||||
if (!entry)
|
||||
return '';
|
||||
|
||||
const [Component, label, link, params] = entry;
|
||||
|
||||
return (
|
||||
<a
|
||||
className={
|
||||
`px-3 py-1 rounded-md text-md border hover:bg-component-bg-hover active:bg-component-bg-pressed ${isActive(link[2], link[3]) ? 'bg-component-bg border-component-bg-pressed' : 'border-transparent'}`
|
||||
`px-3 py-1 rounded-md text-md border hover:bg-component-bg-hover active:bg-component-bg-pressed ${isActive(link, params) ? 'bg-component-bg border-component-bg-pressed' : 'border-transparent'}`
|
||||
}
|
||||
key={i}
|
||||
onClick={() => goto(link[2], link[3])}
|
||||
key={link}
|
||||
onClick={() => goto(link, params)}
|
||||
>
|
||||
<span className="flex flex-row items-center gap-x-3 *:w-5">
|
||||
{link[0]}
|
||||
{link[1]}
|
||||
{Component}
|
||||
{label}
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ function RouteComponent() {
|
|||
setIsForwardButtonEnabled(true);
|
||||
}, [currentStepIndex, tasksStage, tasksCompleted]);
|
||||
|
||||
const next = useCallback(async () => {
|
||||
const next = useCallback(() => {
|
||||
const isLast = currentStepIndex === steps.length - 1;
|
||||
|
||||
if (isLast) {
|
||||
|
|
@ -140,14 +140,17 @@ function RouteComponent() {
|
|||
setTasksMessage('Starting setup...');
|
||||
|
||||
const tasksToRun = [
|
||||
async () => {
|
||||
() => new Promise<void>((resolve) => {
|
||||
setTasksMessage(`Setting language to ${language?.lang}...`);
|
||||
|
||||
console.log('Language task completed for:', language?.lang);
|
||||
},
|
||||
async () => {
|
||||
|
||||
resolve();
|
||||
}),
|
||||
() => new Promise<void>((resolve) => {
|
||||
if (importInstancesMap.size === 0) {
|
||||
console.log('No instances selected for import.');
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -165,7 +168,9 @@ function RouteComponent() {
|
|||
throw e;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
resolve();
|
||||
}),
|
||||
];
|
||||
|
||||
const results = await Promise.allSettled(tasksToRun.map(task => task()));
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import type { PackageType, Provider } from '@/bindings.gen';
|
||||
|
||||
export const BROWSER_VIEWS: Array<string> = ['grid', 'list'] as const;
|
||||
|
||||
export interface CategoryItem {
|
||||
|
|
@ -6,7 +8,7 @@ export interface CategoryItem {
|
|||
}
|
||||
|
||||
export const browserCategories = {
|
||||
byPackageType(packageType: 'mod' | string & {}, provider: string): Array<CategoryItem> {
|
||||
byPackageType(packageType: PackageType, provider: Provider): Array<CategoryItem> {
|
||||
switch (packageType) {
|
||||
case 'mod':
|
||||
return this.mod(provider);
|
||||
|
|
|
|||
|
|
@ -41,12 +41,12 @@ export function int32ToBigInt([high, low]: [number, number]): bigint {
|
|||
}
|
||||
|
||||
export function randomString(len: number = 6, charSet: string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') {
|
||||
let randomString = '';
|
||||
let str = '';
|
||||
for (let i = 0; i < len; i++) {
|
||||
const randomPos = Math.floor(Math.random() * charSet.length);
|
||||
randomString += charSet.substring(randomPos, randomPos + 1);
|
||||
str += charSet.substring(randomPos, randomPos + 1);
|
||||
}
|
||||
return randomString;
|
||||
return str;
|
||||
}
|
||||
|
||||
export function abbreviateNumber(n: number | bigint, locale: string = 'en-US'): string {
|
||||
|
|
|
|||
|
|
@ -27,5 +27,5 @@
|
|||
"skipLibCheck": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx", "vite.config.js"]
|
||||
"include": ["**/*.ts", "**/*.tsx", "vite.config.ts"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,22 @@ export default defineConfig(
|
|||
|
||||
return config;
|
||||
}),
|
||||
{
|
||||
ignores: [
|
||||
'apps/onelauncher/old_frontend',
|
||||
'apps/*/desktop/capabilities',
|
||||
'apps/*/desktop/gen',
|
||||
'**/*.gen.*',
|
||||
],
|
||||
// overrides: [
|
||||
// {
|
||||
// files: ['vite.config.js'],
|
||||
// parserOptions: {
|
||||
// project: null, // disables type-aware linting for this file
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'sort-imports': 'off', // Replaced by perfectionist/sort-named-imports',
|
||||
|
|
@ -31,7 +47,14 @@ export default defineConfig(
|
|||
'ts/no-use-before-define': 'off',
|
||||
'style/function-paren-newline': ['error', 'consistent'],
|
||||
'react/no-context-provider': 'off',
|
||||
"prefer-const": "off" // disabled due to maximum call stack size
|
||||
'prefer-const': 'off', // disabled due to maximum call stack size
|
||||
'eslint-comments/no-unlimited-disable': 'off',
|
||||
'eslint-comments/require-description': ['error', {
|
||||
ignore: [
|
||||
'eslint-disable',
|
||||
],
|
||||
}],
|
||||
'no-shadow': 'off',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
"scripts": {
|
||||
"prep": "pnpm scripts prep",
|
||||
"typecheck": "pnpm -r typecheck",
|
||||
"bootstrap": "cargo clean && ./packages/scripts/setup.sh && pnpm install && pnpm prep && pnpm codegen",
|
||||
"bootstrap": "cargo clean && ./packages/scripts/setup.sh && pnpm install && pnpm prep",
|
||||
|
||||
"core": "pnpm --filter @onelauncher/core -- ",
|
||||
"migrate": "sea-orm-cli migrate -d ./packages/migration",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ interface MappedCategoryItem {
|
|||
id: string;
|
||||
}
|
||||
|
||||
type MappingList = Record<string, MappedCategoryItem[]>;
|
||||
type MappingList = Record<string, Array<MappedCategoryItem>>;
|
||||
|
||||
const packageTypes = ['mod', 'modpack', 'shader', 'resourcepack'] as const;
|
||||
type PackageType = typeof packageTypes[number];
|
||||
|
|
@ -43,7 +43,7 @@ async function fetchCategoriesCurseforge(packageType: PackageType): Promise<Mapp
|
|||
parentCategoryId?: number;
|
||||
};
|
||||
|
||||
const data = (await response.json() as { data: CFCategory[] }).data;
|
||||
const data = (await response.json() as { data: Array<CFCategory> }).data;
|
||||
|
||||
// TODO: Add more calssIds
|
||||
const classIds: Record<PackageType, number> = {
|
||||
|
|
@ -79,7 +79,7 @@ async function fetchCategoriesModrinth(packageType: PackageType): Promise<Mappin
|
|||
header: string;
|
||||
}
|
||||
|
||||
const data = await response.json() as ModrinthCategory[];
|
||||
const data = await response.json() as Array<ModrinthCategory>;
|
||||
|
||||
const mapping: MappingList = {};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { exec } from 'node:child_process';
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import { consola } from 'consola';
|
||||
import mustache from 'mustache';
|
||||
|
|
@ -77,3 +78,16 @@ catch (error) {
|
|||
consola.error(error);
|
||||
env.__exit(1);
|
||||
}
|
||||
|
||||
consola.start('building the web_commons package...');
|
||||
exec('pnpm -w web_common build', { cwd: env.__root }, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
consola.error(`web_commons build failed: ${error.message}`);
|
||||
env.__exit(1);
|
||||
}
|
||||
if (stderr)
|
||||
consola.error(stderr);
|
||||
|
||||
consola.success('web_commons package built successfully!');
|
||||
env.__exit(0);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ if (cargoConfig.env && typeof cargoConfig.env === 'object')
|
|||
|
||||
function targetFilter(filter: 'b' | 't') {
|
||||
const filters = filter === 'b' ? ['-b', '--bundles'] : ['-t', '--target'];
|
||||
return (_: string, idx: number, args: string[]) => {
|
||||
return (_: string, idx: number, args: Array<string>) => {
|
||||
if (idx === 0)
|
||||
return false;
|
||||
const prev = args[idx - 1] ?? '';
|
||||
|
|
|
|||
|
|
@ -45,9 +45,9 @@ export async function which(cmd: TemplateStringsArray): Promise<boolean> {
|
|||
return type() === 'Windows_NT'
|
||||
? where(cmd[0]!)
|
||||
: Promise.any(
|
||||
Array.from(new Set(process.env.PATH?.split(':')))
|
||||
.map(p => fs.access(join(p, cmd[0]!), fs.constants.X_OK)),
|
||||
).then(_ => true, _ => false);
|
||||
Array.from(new Set(process.env.PATH?.split(':')))
|
||||
.map(p => fs.access(join(p, cmd[0]!), fs.constants.X_OK)),
|
||||
).then(_ => true, _ => false);
|
||||
}
|
||||
|
||||
export async function awaitLock(file: string): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export async function tauriUpdateKey(env: CheckedEnvironment): Promise<string |
|
|||
return keys.publicKey;
|
||||
}
|
||||
|
||||
export async function patchTauri(env: CheckedEnvironment, targets: string[], args: string[]): Promise<string[]> {
|
||||
export async function patchTauri(env: CheckedEnvironment, targets: Array<string>, args: Array<string>): Promise<Array<string>> {
|
||||
consola.start('patching tauri configuration...');
|
||||
if (args.findIndex(a => ['--config', '-c'].includes(a)) !== -1)
|
||||
throw new Error('custom tauri build configuration is not supported!');
|
||||
|
|
@ -55,7 +55,7 @@ export async function patchTauri(env: CheckedEnvironment, targets: string[], arg
|
|||
const osType = type();
|
||||
const tauriPatch: {
|
||||
build: {
|
||||
features: string[];
|
||||
features: Array<string>;
|
||||
};
|
||||
bundle: {
|
||||
macOS: { minimumSystemVersion: string };
|
||||
|
|
@ -107,10 +107,10 @@ export async function patchTauri(env: CheckedEnvironment, targets: string[], arg
|
|||
if (
|
||||
(targets.includes('aarch64-apple-darwin')
|
||||
|| (targets.length === 0 && process.arch === 'arm64'))
|
||||
&& (macOSStore.minimumVersion == null || semver.lt(
|
||||
semver.coerce(macOSStore.minimumVersion)!,
|
||||
semver.coerce(macOSStore.defaultArm64)!,
|
||||
))
|
||||
&& (macOSStore.minimumVersion == null || semver.lt(
|
||||
semver.coerce(macOSStore.minimumVersion)!,
|
||||
semver.coerce(macOSStore.defaultArm64)!,
|
||||
))
|
||||
) {
|
||||
macOSStore.minimumVersion = macOSStore.defaultArm64;
|
||||
consola.debug(`setting minimum macOS version to ${macOSStore.minimumVersion}...`);
|
||||
|
|
|
|||
|
|
@ -1,13 +1,20 @@
|
|||
[package]
|
||||
name = "serde_json_path_to_error"
|
||||
version = "0.2.0"
|
||||
authors = [ "Polyfrost", "Ethan Brierley <ethanboxx@gmail.com>" ]
|
||||
authors = [
|
||||
"Polyfrost",
|
||||
"Ethan Brierley <ethanboxx@gmail.com>"
|
||||
]
|
||||
license = "MIT/Apache-2.0"
|
||||
readme = "README.md"
|
||||
description = """
|
||||
A drop in replacement for serde_json where detailed errors are the default
|
||||
"""
|
||||
keywords = [ "serde", "json", "serialization" ]
|
||||
keywords = [
|
||||
"serde",
|
||||
"json",
|
||||
"serialization"
|
||||
]
|
||||
documentation = "https://docs.rs/serde_json_path_to_error/"
|
||||
repository = "https://github.com/eopb/serde_json_path_to_error"
|
||||
edition = "2024"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint-disable eslint-comments/require-description -- i trust that one github discussion user on this */
|
||||
/* eslint-disable react/no-unstable-default-props */
|
||||
|
||||
// Taken from https://github.com/TanStack/router/discussions/823#discussioncomment-12765059
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { JSX, ReactNode } from 'react';
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import type { JSX } from 'react';
|
||||
import React, { createContext, useCallback, useContext, useMemo } from 'react';
|
||||
|
||||
interface SelectListContextType {
|
||||
onSelect: (value: string) => void;
|
||||
|
|
@ -30,7 +30,7 @@ export const SelectList: React.FC<SelectListProps> & { Item: React.FC<SelectList
|
|||
multiple = false,
|
||||
children,
|
||||
}) => {
|
||||
const handleSelect = (itemValue: string): void => {
|
||||
const handleSelect = useCallback((itemValue: string): void => {
|
||||
if (multiple) {
|
||||
const currentValues = Array.isArray(value) ? value : [];
|
||||
const newValues = currentValues.includes(itemValue)
|
||||
|
|
@ -41,20 +41,20 @@ export const SelectList: React.FC<SelectListProps> & { Item: React.FC<SelectList
|
|||
else {
|
||||
onChange(itemValue);
|
||||
}
|
||||
};
|
||||
}, [multiple, onChange, value]);
|
||||
|
||||
const isSelected = (itemValue: string): boolean => {
|
||||
const isSelected = useCallback((itemValue: string): boolean => {
|
||||
if (multiple)
|
||||
return Array.isArray(value) && value.includes(itemValue);
|
||||
|
||||
return value === itemValue;
|
||||
};
|
||||
}, [multiple, value]);
|
||||
|
||||
const contextValue: SelectListContextType = {
|
||||
const contextValue: SelectListContextType = useMemo(() => ({
|
||||
onSelect: handleSelect,
|
||||
isSelected,
|
||||
multiple,
|
||||
};
|
||||
}), [handleSelect, isSelected, multiple]);
|
||||
|
||||
return (
|
||||
<SelectListContext.Provider value={contextValue}>
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ import { tv } from 'tailwind-variants';
|
|||
|
||||
export const tooltipVariants = tv({
|
||||
base: [
|
||||
'px-3 py-2 text-sm font-medium text-white bg-gray-900 rounded-lg shadow-lg',
|
||||
'border border-gray-700 max-w-xs z-50',
|
||||
'px-3 py-2 text-sm font-medium text-white bg-component-bg rounded-lg shadow-lg',
|
||||
'border border-component-border max-w-xs z-50',
|
||||
'data-[entering]:animate-in data-[entering]:fade-in data-[entering]:zoom-in-95',
|
||||
'data-[exiting]:animate-out data-[exiting]:fade-out data-[exiting]:zoom-out-95',
|
||||
],
|
||||
|
|
|
|||
26
pnpm-lock.yaml
generated
26
pnpm-lock.yaml
generated
|
|
@ -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: {}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ catalog:
|
|||
# ---
|
||||
# dependencies
|
||||
# ---
|
||||
|
||||
|
||||
# react + libraries
|
||||
react: ^19.0.0
|
||||
react-aria-components: ^1.8.0
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue