Merge branch 'onelauncher/main' of https://github.com/Polyfrost/OneLauncher into onelauncher/main

This commit is contained in:
Szedann 2025-07-22 19:28:11 +02:00
commit e45da0d4db
35 changed files with 191 additions and 140 deletions

View file

@ -20,7 +20,7 @@ interface TargetConfig {
} }
interface BuildTarget { interface BuildTarget {
updater: false | Updater; updater: false | Updater;
standalone: TargetConfig[]; standalone: Array<TargetConfig>;
} }
const osTargets = { const osTargets = {
@ -67,7 +67,7 @@ const frontendBundle = 'apps/frontend/dist.tar.xz';
const updaterName = `OneLauncher-Updater-${os}-${arch}`; const updaterName = `OneLauncher-Updater-${os}-${arch}`;
const frontendName = `OneLauncher-Frontend-${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() { async function uploadFrontend() {
if (!(await exists(frontendBundle))) { if (!(await exists(frontendBundle))) {

View file

@ -8,8 +8,7 @@
"build": "vite build && tsc", "build": "vite build && tsc",
"serve": "vite preview", "serve": "vite preview",
"lint": "eslint", "lint": "eslint",
"format": "prettier", "check": "eslint --fix"
"check": "prettier --write . && eslint --fix"
}, },
"dependencies": { "dependencies": {
"@fontsource/poppins": "catalog:", "@fontsource/poppins": "catalog:",

View file

@ -1,10 +0,0 @@
// @ts-check
/** @type {import('prettier').Config} */
const config = {
semi: true,
singleQuote: true,
trailingComma: 'all',
};
export default config;

View file

@ -1,10 +1,11 @@
import { useEffect, type PropsWithChildren } from 'react'; import type { PropsWithChildren } from 'react';
import { LoaderSuspense, Navbar } from '@/components'; import { LoaderSuspense, Navbar } from '@/components';
import { GameBackground } from '@/components/GameBackground'; import { GameBackground } from '@/components/GameBackground';
import useAppShellStore from '@/stores/appShellStore'; import useAppShellStore from '@/stores/appShellStore';
import { AnimatedOutlet } from '@onelauncher/common/components'; import { AnimatedOutlet } from '@onelauncher/common/components';
import { createFileRoute, useRouter } from '@tanstack/react-router'; import { createFileRoute, useRouter } from '@tanstack/react-router';
import { AnimatePresence } from 'motion/react'; import { AnimatePresence } from 'motion/react';
import { useEffect } from 'react';
export const Route = createFileRoute('/app')({ export const Route = createFileRoute('/app')({
component: RouteComponent, component: RouteComponent,
@ -40,12 +41,12 @@ function AppShell({
const setPrevLocation = useAppShellStore(state => state.setPrevLocation); const setPrevLocation = useAppShellStore(state => state.setPrevLocation);
useEffect(() => { useEffect(() => {
const unsub = router.subscribe("onBeforeNavigate", (e) => { const unsub = router.subscribe('onBeforeNavigate', (e) => {
setPrevLocation(e.fromLocation ?? null); setPrevLocation(e.fromLocation ?? null);
}); });
return () => unsub(); return () => unsub();
}, [router]); }, [router, setPrevLocation]);
return ( return (
<div className="flex flex-col h-full w-full"> <div className="flex flex-col h-full w-full">

View file

@ -25,5 +25,5 @@
"skipLibCheck": true, "skipLibCheck": true,
"noUncheckedSideEffectImports": true "noUncheckedSideEffectImports": true
}, },
"include": ["**/*.ts", "**/*.tsx", "prettier.config.js", "vite.config.js"] "include": ["**/*.ts", "**/*.tsx", "vite.config.ts"]
} }

View file

@ -1,7 +1,7 @@
import type { Provider, SearchResult } from '@/bindings.gen'; import type { Provider, SearchResult } from '@/bindings.gen';
import { abbreviateNumber, LOADERS, upperFirst } from '@/utils'; import { abbreviateNumber, LOADERS, upperFirst } from '@/utils';
import { Show, Tooltip } from '@onelauncher/common/components'; 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 { Download01Icon } from '@untitled-theme/icons-react';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { Focusable } from 'react-aria-components'; import { Focusable } from 'react-aria-components';
@ -22,7 +22,7 @@ export function PackageGrid({ items, provider }: { items: Array<SearchResult>; p
} }
export function PackageItem({ provider, ...item }: SearchResult & { provider: Provider }) { 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 ( return (
<Link <Link

View file

@ -36,7 +36,7 @@ export function BrowserProvider(props: any) {
categories: null, categories: null,
game_versions: null, game_versions: null,
loaders: null, loaders: null,
package_types: ["mod"] package_types: ['mod'],
}, },
query: null, query: null,
limit: 18 as unknown as bigint, limit: 18 as unknown as bigint,
@ -47,7 +47,7 @@ export function BrowserProvider(props: any) {
useEffect(() => { useEffect(() => {
if (query.filters?.categories || query.query) if (query.filters?.categories || query.query)
navigate({ to: '/app/browser/search' }); navigate({ to: '/app/browser/search' });
}, [query]); }, [navigate, query]);
const context = useMemo<BrowserControllerType>(() => ({ const context = useMemo<BrowserControllerType>(() => ({
cluster, cluster,

View file

@ -1,10 +1,10 @@
import type { ClusterModel } from '@/bindings.gen'; import type { ClusterModel } from '@/bindings.gen';
import type { ModalProps } from '@/components/overlay/Modal'; import type { ModalProps } from '@/components/overlay/Modal';
import Modal from '@/components/overlay/Modal';
import { bindings } from '@/main'; import { bindings } from '@/main';
import { useCommand } from '@onelauncher/common'; import { useCommand } from '@onelauncher/common';
import { useEffect, useState } from 'react';
import Modal from '@/components/overlay/Modal';
import { Button, Dropdown } from '@onelauncher/common/components'; import { Button, Dropdown } from '@onelauncher/common/components';
import { useEffect, useState } from 'react';
import { Heading } from 'react-aria-components'; import { Heading } from 'react-aria-components';
export function useRecentCluster() { export function useRecentCluster() {
@ -58,26 +58,29 @@ export function useClusters() {
} }
type ChooseClusterModalProps = ModalProps & { type ChooseClusterModalProps = ModalProps & {
clusters?: ClusterModel[]; clusters?: Array<ClusterModel>;
confirmText?: string; confirmText?: string;
selected?: (clusters: ClusterModel[]) => number; selected?: (clusters: Array<ClusterModel>) => number;
onSelected?: (cluster: ClusterModel) => void; onSelected?: (cluster: ClusterModel) => void;
}; };
export function ChooseClusterModal({ clusters: clusterList, selected: defaultSelected, onSelected, confirmText, ...props }: ChooseClusterModalProps) {
export function ChooseClusterModal({clusters: clusterList, selected: defaultSelected, onSelected, confirmText, ...props}: ChooseClusterModalProps) { const clusters = useClusters();
const clusters = useClusters()
const [selected, setSelected] = useState(0); const [selected, setSelected] = useState(0);
useEffect(() => { useEffect(() => {
if (!clusters) return; if (!clusters)
const newSelected = defaultSelected?.(clusters) return;
if (newSelected) setSelected(newSelected)
}, [clusters]) const newSelected = defaultSelected?.(clusters);
if (newSelected)
setSelected(newSelected);
}, [clusters, defaultSelected]);
function chooseCluster() { function chooseCluster() {
if (clusters !== undefined && selected !== undefined) { if (clusters !== undefined) {
const cluster = clusters[selected]; const cluster = clusters[selected] as ClusterModel | undefined;
if (cluster !== undefined) if (cluster !== undefined)
onSelected?.(cluster); onSelected?.(cluster);
} }
@ -87,35 +90,35 @@ export function ChooseClusterModal({clusters: clusterList, selected: defaultSele
<Modal {...props}> <Modal {...props}>
<div className="min-w-sm flex flex-col rounded-lg bg-page text-center p-4 gap-2"> <div className="min-w-sm flex flex-col rounded-lg bg-page text-center p-4 gap-2">
<Heading slot='title' className='text-xl font-semibold'>Select a Cluster</Heading> <Heading className="text-xl font-semibold" slot="title">Select a Cluster</Heading>
<Dropdown <Dropdown
onSelectionChange={(index) => { onSelectionChange={(index) => {
setSelected(index as number) setSelected(index as number);
}} }}
selectedKey={selected} selectedKey={selected}
> >
{clusters?.map((cluster, index) => ( {clusters?.map((cluster, index) => (
<Dropdown.Item id={index} key={cluster.id}> <Dropdown.Item id={index} key={cluster.id}>
{cluster.name} {cluster.name}
</Dropdown.Item> </Dropdown.Item>
))} ))}
</Dropdown> </Dropdown>
<div className='flex gap-2'> <div className="flex gap-2">
<Button <Button
color="secondary" children="Close"
children="Close" color="secondary"
slot="close" slot="close"
/> />
<Button <Button
children={confirmText ?? "Save"} children={confirmText ?? 'Save'}
slot="close" onClick={chooseCluster}
onClick={chooseCluster} slot="close"
/> />
</div> </div>
</div> </div>
</Modal> </Modal>
) );
// return ( // return (
// <Modal // <Modal

View file

@ -46,20 +46,20 @@ function usePagination(options: PaginationOptions) {
return pages; return pages;
} }
function PaginationBtn(props: { page: number }) { function PaginationBtn({ page: pageIndex }: { page: number }) {
return ( return (
<Button <Button
className="p-2 text-lg min-w-8" className="p-2 text-lg min-w-8"
color={props.page === page ? 'primary' : 'secondary'} color={pageIndex === page ? 'primary' : 'secondary'}
onClick={() => { onClick={() => {
if (props.page === page) if (pageIndex === page)
return; return;
setPage(props.page); setPage(pageIndex);
}} }}
> >
<span className="text-trim">{props.page}</span> <span className="text-trim">{pageIndex}</span>
</Button> </Button>
); );
} }

View file

@ -3,7 +3,7 @@
import type { Settings } from '@/bindings.gen'; import type { Settings } from '@/bindings.gen';
import { bindings } from '@/main'; import { bindings } from '@/main';
import { useCommand } from '@onelauncher/common'; 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 { interface SettingsControllerType {
settings: Settings | undefined; settings: Settings | undefined;
@ -12,7 +12,7 @@ interface SettingsControllerType {
setSettingsToSave: (settings: Partial<Settings> | null) => void; setSettingsToSave: (settings: Partial<Settings> | null) => void;
settingsChanged: boolean; settingsChanged: boolean;
setSettingsChanged: (changed: boolean) => void; 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); const SettingsContext = createContext<SettingsControllerType | null>(null);
@ -51,14 +51,14 @@ export function SettingsProvider({ children }: SettingsProviderProps) {
return () => window.removeEventListener('beforeunload', handleBeforeUnload); return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, []); }, []);
const saveChangedSettings = () => { const saveChangedSettings = useCallback(() => {
if (!settings) if (!settings)
return; return;
writeQuery.refetch(); writeQuery.refetch();
setSettingsChanged(false); setSettingsChanged(false);
}; }, [settings, writeQuery]);
const handleSetSettingsToSave = useCallback((newSettings: Partial<Settings> | null) => { const handleSetSettingsToSave = useCallback((newSettings: Partial<Settings> | null) => {
setSettingsToSave(newSettings); setSettingsToSave(newSettings);
@ -86,7 +86,7 @@ export function SettingsProvider({ children }: SettingsProviderProps) {
return [value, setValue]; return [value, setValue];
}, [settings]); }, [settings]);
const controller: SettingsControllerType = { const controller: SettingsControllerType = useMemo(() => ({
settings, settings,
saveChangedSettings, saveChangedSettings,
settingsToSave, settingsToSave,
@ -94,7 +94,7 @@ export function SettingsProvider({ children }: SettingsProviderProps) {
settingsChanged, settingsChanged,
setSettingsChanged, setSettingsChanged,
createSetting, createSetting,
}; }), [createSetting, handleSetSettingsToSave, saveChangedSettings, settings, settingsChanged, settingsToSave]);
if (settings === undefined) if (settings === undefined)
return null; return null;

View file

@ -32,6 +32,7 @@ function Featured() {
const navigate = useNavigate(); const navigate = useNavigate();
const featuredImageIndex = useMemo(() => featuredPackage.data?.gallery.findIndex(image => image.featured), [featuredPackage.data]); const featuredImageIndex = useMemo(() => featuredPackage.data?.gallery.findIndex(image => image.featured), [featuredPackage.data]);
const [selectedImage, setSelectedImage] = useState(featuredImageIndex ?? 0); const [selectedImage, setSelectedImage] = useState(featuredImageIndex ?? 0);
return ( return (
<Show when={featuredPackage.isSuccess}> <Show when={featuredPackage.isSuccess}>
<div className="flex flex-col gap-y-1"> <div className="flex flex-col gap-y-1">

View file

@ -56,9 +56,12 @@ function RouteComponent() {
if (!includes(PROVIDERS, provider)) if (!includes(PROVIDERS, provider))
throw new Error('Invalid provider'); throw new Error('Invalid provider');
const packageData = usePackageData(provider, slug, {}); const packageData = usePackageData(provider, slug, {});
const packageContextValue = useMemo(() => ({
pkg: packageData.data,
}), [packageData.data]);
return ( return (
<PackageContext value={{ pkg: packageData.data }}> <PackageContext value={packageContextValue}>
<Show when={packageData.isSuccess && !packageData.isFetching}> <Show when={packageData.isSuccess && !packageData.isFetching}>
<div className="h-full flex flex-1 flex-row items-start gap-x-4"> <div className="h-full flex flex-1 flex-row items-start gap-x-4">
<BrowserSidebar package={packageData.data!} /> <BrowserSidebar package={packageData.data!} />
@ -121,8 +124,8 @@ function getPackageUrl(pkg: ManagedPackage): string {
function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) { function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) {
const { provider } = Route.useParams(); const { provider } = Route.useParams();
const createdAt = useMemo(() => pkg.created ? new Date(pkg.created) : null, []); const createdAt = useMemo(() => pkg.created ? new Date(pkg.created) : null, [pkg.created]);
const updatedAt = useMemo(() => pkg.updated ? new Date(pkg.updated) : null, []); const updatedAt = useMemo(() => pkg.updated ? new Date(pkg.updated) : null, [pkg.updated]);
const authors = useCommand('getUsersFromAuthor', () => bindings.core.getUsersFromAuthor(provider as Provider, pkg.author)); const authors = useCommand('getUsersFromAuthor', () => bindings.core.getUsersFromAuthor(provider as Provider, pkg.author));
@ -303,7 +306,7 @@ function InstallButton() {
aria-label="cluster" isOpen={open} aria-label="cluster" isOpen={open}
onOpenChange={setOpen} onOpenChange={setOpen}
onSelectionChange={(e) => { 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) if (cluster)
browserContext.setCluster(cluster); browserContext.setCluster(cluster);
}} }}
@ -382,6 +385,7 @@ function Versions() {
limit: 20, limit: 20,
offset, offset,
}); });
const pagination = usePagination({ const pagination = usePagination({
itemsCount: versions?.total as unknown as number, itemsCount: versions?.total as unknown as number,
itemsPerPage: versions?.limit as unknown as number, itemsPerPage: versions?.limit as unknown as number,

View file

@ -1,11 +1,12 @@
import type { Filters } from '@/bindings.gen'; import type { Filters } from '@/bindings.gen';
import type { PropsWithChildren } from 'react';
import ProviderIcon from '@/components/content/ProviderIcon'; import ProviderIcon from '@/components/content/ProviderIcon';
import { BrowserProvider, useBrowserContext } from '@/hooks/useBrowser'; import { BrowserProvider, useBrowserContext } from '@/hooks/useBrowser';
import { useClusters } from '@/hooks/useCluster'; import { useClusters } from '@/hooks/useCluster';
import { PROVIDERS } from '@/utils'; import { PROVIDERS } from '@/utils';
import { browserCategories } from '@/utils/browser'; import { browserCategories } from '@/utils/browser';
import { AnimatedOutlet, Dropdown, Show, TextField } from '@onelauncher/common/components'; 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 { SearchMdIcon } from '@untitled-theme/icons-react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@ -27,7 +28,7 @@ function RouteComponent() {
); );
} }
export function BrowserLayout(props: any) { export function BrowserLayout({ children }: PropsWithChildren) {
return ( return (
<div className="relative h-full flex flex-1 flex-col items-center gap-2"> <div className="relative h-full flex flex-1 flex-col items-center gap-2">
<div className="h-full w-full 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">
@ -42,7 +43,7 @@ export function BrowserLayout(props: any) {
<div className="h-full flex flex-col gap-y-4"> <div className="h-full flex flex-col gap-y-4">
<div className="h-full flex-1"> <div className="h-full flex-1">
{props.children} {children}
</div> </div>
</div> </div>
@ -56,11 +57,14 @@ export function BrowserLayout(props: any) {
function BrowserSidebar() { function BrowserSidebar() {
const context = useBrowserContext(); const context = useBrowserContext();
const clusters = useClusters(); const clusters = useClusters();
useEffect(() => { useEffect(() => {
const game_versions = context.cluster?.mc_version ? [context.cluster.mc_version] : null; const game_versions = context.cluster?.mc_version ? [context.cluster.mc_version] : null;
const loaders = context.cluster?.mc_loader ? [context.cluster.mc_loader] : null; const loaders = context.cluster?.mc_loader ? [context.cluster.mc_loader] : null;
context.setQuery({ ...context.query, filters: { ...defaultFilters, ...context.query.filters, game_versions, loaders } }); context.setQuery({ ...context.query, filters: { ...defaultFilters, ...context.query.filters, game_versions, loaders } });
// eslint-disable-next-line react-hooks/exhaustive-deps -- loop
}, [context.cluster]); }, [context.cluster]);
return ( return (
<div className="flex flex-col gap-y-4"> <div className="flex flex-col gap-y-4">
<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> <h6 className="my-1 uppercase opacity-60">Active Cluster</h6>
<Dropdown <Dropdown
onSelectionChange={(id) => { onSelectionChange={(id) => {
const cluster = clusters?.find(cluster => cluster.id.toString() === id); const cluster = clusters?.find(item => item.id.toString() === id);
context.setCluster(cluster); context.setCluster(cluster);
}} }}
selectedKey={context.cluster?.id.toString()} selectedKey={context.cluster?.id.toString()}

View file

@ -1,11 +1,9 @@
import type { Paginated, SearchResult } from '@/bindings.gen'; import type { Paginated, SearchResult } from '@/bindings.gen';
import type { PaginationOptions } from '@/hooks/usePagination';
import { PackageGrid } from '@/components/content/PackageItem'; import { PackageGrid } from '@/components/content/PackageItem';
import { useBrowserContext, useBrowserSearch } from '@/hooks/useBrowser'; import { useBrowserContext, useBrowserSearch } from '@/hooks/useBrowser';
import usePagination from '@/hooks/usePagination'; import usePagination from '@/hooks/usePagination';
import { Show } from '@onelauncher/common/components';
import { createFileRoute } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useState } from 'react';
import { BrowserLayout } from './route'; import { BrowserLayout } from './route';
export const Route = createFileRoute('/app/browser/search')({ export const Route = createFileRoute('/app/browser/search')({
@ -25,10 +23,12 @@ function RouteComponent() {
function Search() { function Search() {
const context = useBrowserContext(); const context = useBrowserContext();
const search = useBrowserSearch(context.provider, context.query, { const search = useBrowserSearch(context.provider, context.query, {
}); });
useEffect(() => { useEffect(() => {
search.refetch(); search.refetch();
}, [context.provider, context.query, context.cluster]); }, [context.provider, context.query, context.cluster, search]);
return ( return (
<> <>
@ -44,19 +44,23 @@ function Search() {
function Results({ results }: { results: Paginated<SearchResult> }) { function Results({ results }: { results: Paginated<SearchResult> }) {
const context = useBrowserContext(); const context = useBrowserContext();
const [oldTotal, setOldTotal] = useState(results.total); const [oldTotal, setOldTotal] = useState(results.total);
const pagination = usePagination({ const pagination = usePagination({
itemsCount: results.total as unknown as number, itemsCount: results.total as unknown as number,
itemsPerPage: context.query.limit as unknown as number, itemsPerPage: context.query.limit as unknown as number,
}); });
useEffect(() => { useEffect(() => {
context.setQuery({ ...context.query, offset: pagination.offset as unknown as bigint }); context.setQuery({ ...context.query, offset: pagination.offset as unknown as bigint });
}, [pagination.page]); }, [context, pagination.offset, pagination.page]);
useEffect(() => { useEffect(() => {
if (oldTotal === results.total) if (oldTotal === results.total)
return; return;
pagination.reset(); pagination.reset();
setOldTotal(results.total); setOldTotal(results.total);
}, [results.total]); }, [oldTotal, pagination, results.total]);
return ( return (
<div> <div>
<div className="w-full flex justify-end my-2"> <div className="w-full flex justify-end my-2">

View file

@ -48,9 +48,7 @@ function RouteComponent() {
})(); })();
}, [cluster.data?.folder_name]); }, [cluster.data?.folder_name]);
const openClusterDir = async () => { const openClusterDir = () => openPath(launcherDir);
openPath(launcherDir);
};
async function handleEditMode() { async function handleEditMode() {
if (edit) if (edit)

View file

@ -5,7 +5,7 @@ import { NewClusterCreate } from '@/components/launcher/cluster/ClusterCreation'
import { useRecentCluster } from '@/hooks/useCluster'; import { useRecentCluster } from '@/hooks/useCluster';
import { bindings } from '@/main'; import { bindings } from '@/main';
import { formatAsDuration, upperFirst } from '@/utils'; 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 { Button, Show } from '@onelauncher/common/components';
import { createFileRoute, Link } from '@tanstack/react-router'; import { createFileRoute, Link } from '@tanstack/react-router';
import { convertFileSrc } from '@tauri-apps/api/core'; import { convertFileSrc } from '@tauri-apps/api/core';

View file

@ -23,9 +23,7 @@ function RouteComponent() {
})(); })();
}, []); }, []);
const openLauncherDir = async () => { const openLauncherDir = () => openPath(launcherDir);
openPath(launcherDir);
};
const [discordRpc, setDiscordRpc] = createSetting('discord_enabled', settings?.discord_enabled); const [discordRpc, setDiscordRpc] = createSetting('discord_enabled', settings?.discord_enabled);

View file

@ -86,27 +86,28 @@ function Sidebar(props: SidebarProps) {
return ( return (
<div className="w-56 flex flex-col pr-2"> <div className="w-56 flex flex-col pr-2">
{Object.keys(links).map((section, i) => ( {Object.keys(links).map(section => (
<div className="flex flex-col gap-y-2" key={i}> <div className="flex flex-col gap-y-2" key={section}>
<div> <div>
<p className="m-1.5 mt-5 text-xs text-fg-secondary font-medium">{section.toUpperCase()}</p> <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"> <div className="flex flex-col gap-y-1 fill-fg-primary text-fg-primary font-medium">
{links[section].map((link, i) => { {links[section].map((entry) => {
// eslint yaps too much tbh if (!entry)
if (!link)
return ''; return '';
const [Component, label, link, params] = entry;
return ( return (
<a <a
className={ 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} key={link}
onClick={() => goto(link[2], link[3])} onClick={() => goto(link, params)}
> >
<span className="flex flex-row items-center gap-x-3 *:w-5"> <span className="flex flex-row items-center gap-x-3 *:w-5">
{link[0]} {Component}
{link[1]} {label}
</span> </span>
</a> </a>
); );

View file

@ -104,7 +104,7 @@ function RouteComponent() {
setIsForwardButtonEnabled(true); setIsForwardButtonEnabled(true);
}, [currentStepIndex, tasksStage, tasksCompleted]); }, [currentStepIndex, tasksStage, tasksCompleted]);
const next = useCallback(async () => { const next = useCallback(() => {
const isLast = currentStepIndex === steps.length - 1; const isLast = currentStepIndex === steps.length - 1;
if (isLast) { if (isLast) {
@ -140,14 +140,17 @@ function RouteComponent() {
setTasksMessage('Starting setup...'); setTasksMessage('Starting setup...');
const tasksToRun = [ const tasksToRun = [
async () => { () => new Promise<void>((resolve) => {
setTasksMessage(`Setting language to ${language?.lang}...`); setTasksMessage(`Setting language to ${language?.lang}...`);
console.log('Language task completed for:', language?.lang); console.log('Language task completed for:', language?.lang);
},
async () => { resolve();
}),
() => new Promise<void>((resolve) => {
if (importInstancesMap.size === 0) { if (importInstancesMap.size === 0) {
console.log('No instances selected for import.'); console.log('No instances selected for import.');
resolve();
return; return;
} }
@ -165,7 +168,9 @@ function RouteComponent() {
throw e; throw e;
} }
} }
},
resolve();
}),
]; ];
const results = await Promise.allSettled(tasksToRun.map(task => task())); const results = await Promise.allSettled(tasksToRun.map(task => task()));

View file

@ -1,4 +1,4 @@
import type { PackageType, Provider } from "@/bindings.gen"; import type { PackageType, Provider } from '@/bindings.gen';
export const BROWSER_VIEWS: Array<string> = ['grid', 'list'] as const; export const BROWSER_VIEWS: Array<string> = ['grid', 'list'] as const;

View file

@ -41,12 +41,12 @@ export function int32ToBigInt([high, low]: [number, number]): bigint {
} }
export function randomString(len: number = 6, charSet: string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') { export function randomString(len: number = 6, charSet: string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') {
let randomString = ''; let str = '';
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
const randomPos = Math.floor(Math.random() * charSet.length); 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 { export function abbreviateNumber(n: number | bigint, locale: string = 'en-US'): string {

View file

@ -27,5 +27,5 @@
"skipLibCheck": true, "skipLibCheck": true,
"noUncheckedSideEffectImports": true "noUncheckedSideEffectImports": true
}, },
"include": ["**/*.ts", "**/*.tsx", "vite.config.js"] "include": ["**/*.ts", "**/*.tsx", "vite.config.ts"]
} }

View file

@ -23,6 +23,22 @@ export default defineConfig(
return config; 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: { rules: {
'sort-imports': 'off', // Replaced by perfectionist/sort-named-imports', 'sort-imports': 'off', // Replaced by perfectionist/sort-named-imports',
@ -31,7 +47,14 @@ export default defineConfig(
'ts/no-use-before-define': 'off', 'ts/no-use-before-define': 'off',
'style/function-paren-newline': ['error', 'consistent'], 'style/function-paren-newline': ['error', 'consistent'],
'react/no-context-provider': 'off', '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',
}, },
}, },
); );

View file

@ -23,7 +23,7 @@
"scripts": { "scripts": {
"prep": "pnpm scripts prep", "prep": "pnpm scripts prep",
"typecheck": "pnpm -r typecheck", "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 -- ", "core": "pnpm --filter @onelauncher/core -- ",
"migrate": "sea-orm-cli migrate -d ./packages/migration", "migrate": "sea-orm-cli migrate -d ./packages/migration",

View file

@ -7,7 +7,7 @@ interface MappedCategoryItem {
id: string; id: string;
} }
type MappingList = Record<string, MappedCategoryItem[]>; type MappingList = Record<string, Array<MappedCategoryItem>>;
const packageTypes = ['mod', 'modpack', 'shader', 'resourcepack'] as const; const packageTypes = ['mod', 'modpack', 'shader', 'resourcepack'] as const;
type PackageType = typeof packageTypes[number]; type PackageType = typeof packageTypes[number];
@ -43,7 +43,7 @@ async function fetchCategoriesCurseforge(packageType: PackageType): Promise<Mapp
parentCategoryId?: number; 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 // TODO: Add more calssIds
const classIds: Record<PackageType, number> = { const classIds: Record<PackageType, number> = {
@ -79,7 +79,7 @@ async function fetchCategoriesModrinth(packageType: PackageType): Promise<Mappin
header: string; header: string;
} }
const data = await response.json() as ModrinthCategory[]; const data = await response.json() as Array<ModrinthCategory>;
const mapping: MappingList = {}; const mapping: MappingList = {};

View file

@ -1,3 +1,4 @@
import { exec } from 'node:child_process';
import { readFile, writeFile } from 'node:fs/promises'; import { readFile, writeFile } from 'node:fs/promises';
import { consola } from 'consola'; import { consola } from 'consola';
import mustache from 'mustache'; import mustache from 'mustache';
@ -77,3 +78,16 @@ catch (error) {
consola.error(error); consola.error(error);
env.__exit(1); 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);
});

View file

@ -29,7 +29,7 @@ if (cargoConfig.env && typeof cargoConfig.env === 'object')
function targetFilter(filter: 'b' | 't') { function targetFilter(filter: 'b' | 't') {
const filters = filter === 'b' ? ['-b', '--bundles'] : ['-t', '--target']; const filters = filter === 'b' ? ['-b', '--bundles'] : ['-t', '--target'];
return (_: string, idx: number, args: string[]) => { return (_: string, idx: number, args: Array<string>) => {
if (idx === 0) if (idx === 0)
return false; return false;
const prev = args[idx - 1] ?? ''; const prev = args[idx - 1] ?? '';

View file

@ -45,9 +45,9 @@ export async function which(cmd: TemplateStringsArray): Promise<boolean> {
return type() === 'Windows_NT' return type() === 'Windows_NT'
? where(cmd[0]!) ? where(cmd[0]!)
: Promise.any( : Promise.any(
Array.from(new Set(process.env.PATH?.split(':'))) Array.from(new Set(process.env.PATH?.split(':')))
.map(p => fs.access(join(p, cmd[0]!), fs.constants.X_OK)), .map(p => fs.access(join(p, cmd[0]!), fs.constants.X_OK)),
).then(_ => true, _ => false); ).then(_ => true, _ => false);
} }
export async function awaitLock(file: string): Promise<void> { export async function awaitLock(file: string): Promise<void> {

View file

@ -47,7 +47,7 @@ export async function tauriUpdateKey(env: CheckedEnvironment): Promise<string |
return keys.publicKey; 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...'); consola.start('patching tauri configuration...');
if (args.findIndex(a => ['--config', '-c'].includes(a)) !== -1) if (args.findIndex(a => ['--config', '-c'].includes(a)) !== -1)
throw new Error('custom tauri build configuration is not supported!'); 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 osType = type();
const tauriPatch: { const tauriPatch: {
build: { build: {
features: string[]; features: Array<string>;
}; };
bundle: { bundle: {
macOS: { minimumSystemVersion: string }; macOS: { minimumSystemVersion: string };
@ -107,10 +107,10 @@ export async function patchTauri(env: CheckedEnvironment, targets: string[], arg
if ( if (
(targets.includes('aarch64-apple-darwin') (targets.includes('aarch64-apple-darwin')
|| (targets.length === 0 && process.arch === 'arm64')) || (targets.length === 0 && process.arch === 'arm64'))
&& (macOSStore.minimumVersion == null || semver.lt( && (macOSStore.minimumVersion == null || semver.lt(
semver.coerce(macOSStore.minimumVersion)!, semver.coerce(macOSStore.minimumVersion)!,
semver.coerce(macOSStore.defaultArm64)!, semver.coerce(macOSStore.defaultArm64)!,
)) ))
) { ) {
macOSStore.minimumVersion = macOSStore.defaultArm64; macOSStore.minimumVersion = macOSStore.defaultArm64;
consola.debug(`setting minimum macOS version to ${macOSStore.minimumVersion}...`); consola.debug(`setting minimum macOS version to ${macOSStore.minimumVersion}...`);

View file

@ -1,13 +1,20 @@
[package] [package]
name = "serde_json_path_to_error" name = "serde_json_path_to_error"
version = "0.2.0" version = "0.2.0"
authors = [ "Polyfrost", "Ethan Brierley <ethanboxx@gmail.com>" ] authors = [
"Polyfrost",
"Ethan Brierley <ethanboxx@gmail.com>"
]
license = "MIT/Apache-2.0" license = "MIT/Apache-2.0"
readme = "README.md" readme = "README.md"
description = """ description = """
A drop in replacement for serde_json where detailed errors are the default 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/" documentation = "https://docs.rs/serde_json_path_to_error/"
repository = "https://github.com/eopb/serde_json_path_to_error" repository = "https://github.com/eopb/serde_json_path_to_error"
edition = "2024" edition = "2024"

View file

@ -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 */ /* eslint-disable react/no-unstable-default-props */
// Taken from https://github.com/TanStack/router/discussions/823#discussioncomment-12765059 // Taken from https://github.com/TanStack/router/discussions/823#discussioncomment-12765059

View file

@ -1,5 +1,5 @@
import type { JSX, ReactNode } from 'react'; import type { JSX } from 'react';
import React, { createContext, useContext } from 'react'; import React, { createContext, useCallback, useContext, useMemo } from 'react';
interface SelectListContextType { interface SelectListContextType {
onSelect: (value: string) => void; onSelect: (value: string) => void;
@ -30,7 +30,7 @@ export const SelectList: React.FC<SelectListProps> & { Item: React.FC<SelectList
multiple = false, multiple = false,
children, children,
}) => { }) => {
const handleSelect = (itemValue: string): void => { const handleSelect = useCallback((itemValue: string): void => {
if (multiple) { if (multiple) {
const currentValues = Array.isArray(value) ? value : []; const currentValues = Array.isArray(value) ? value : [];
const newValues = currentValues.includes(itemValue) const newValues = currentValues.includes(itemValue)
@ -41,20 +41,20 @@ export const SelectList: React.FC<SelectListProps> & { Item: React.FC<SelectList
else { else {
onChange(itemValue); onChange(itemValue);
} }
}; }, [multiple, onChange, value]);
const isSelected = (itemValue: string): boolean => { const isSelected = useCallback((itemValue: string): boolean => {
if (multiple) if (multiple)
return Array.isArray(value) && value.includes(itemValue); return Array.isArray(value) && value.includes(itemValue);
return value === itemValue; return value === itemValue;
}; }, [multiple, value]);
const contextValue: SelectListContextType = { const contextValue: SelectListContextType = useMemo(() => ({
onSelect: handleSelect, onSelect: handleSelect,
isSelected, isSelected,
multiple, multiple,
}; }), [handleSelect, isSelected, multiple]);
return ( return (
<SelectListContext.Provider value={contextValue}> <SelectListContext.Provider value={contextValue}>

View file

@ -36,7 +36,7 @@ catalog:
# --- # ---
# dependencies # dependencies
# --- # ---
# react + libraries # react + libraries
react: ^19.0.0 react: ^19.0.0
react-aria-components: ^1.8.0 react-aria-components: ^1.8.0