feat: global game settings and notifications
notification system needs some work still
This commit is contained in:
parent
1a099f6c3b
commit
855d421eaa
15 changed files with 515 additions and 48 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import type { ToOptions } from '@tanstack/react-router';
|
||||
import { bindings } from '@/main';
|
||||
import useNotifications from '@/hooks/useNotification';
|
||||
import { useCommand } from '@onelauncher/common';
|
||||
import { Button, Menu } from '@onelauncher/common/components';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
|
|
@ -8,9 +9,12 @@ import { MenuTrigger } from 'react-aria-components';
|
|||
import OneLauncherText from '../assets/logos/onelauncher_text.svg';
|
||||
import PlayerHead from './content/PlayerHead';
|
||||
import AccountPopup from './overlay/popups/AccountPopup';
|
||||
import NotificationPopup from './overlay/popups/NotificationPopup';
|
||||
|
||||
function Navbar() {
|
||||
const defaultUser = useCommand('getDefaultUser', () => bindings.core.getDefaultUser(false));
|
||||
const { list } = useNotifications();
|
||||
const notificationCount = Object.keys(list).length;
|
||||
|
||||
return (
|
||||
<div className="h-15 min-h-15 flex flex-row items-center *:flex-1">
|
||||
|
|
@ -37,24 +41,19 @@ function Navbar() {
|
|||
{/* Notification Manager Button */}
|
||||
<div className="relative">
|
||||
<MenuTrigger>
|
||||
<Button color="ghost" size="icon">
|
||||
<Button className="relative" color="ghost" size="icon">
|
||||
<Bell01Icon />
|
||||
{notificationCount > 0 && (
|
||||
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center font-medium">
|
||||
{notificationCount > 99 ? '99+' : notificationCount}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Menu.Popover placement="bottom right">
|
||||
<Menu>
|
||||
<Menu.Item>Hello World</Menu.Item>
|
||||
</Menu>
|
||||
<NotificationPopup />
|
||||
</Menu.Popover>
|
||||
</MenuTrigger>
|
||||
|
||||
{/* <NotificationPopup
|
||||
className="mt-2"
|
||||
mount={notificationButtonContainer}
|
||||
ref={el => el.classList.add('right-0')}
|
||||
setVisible={setNotificationMenuOpen}
|
||||
visible={notificationMenuOpen}
|
||||
/> */}
|
||||
</div>
|
||||
|
||||
<MenuTrigger>
|
||||
|
|
|
|||
|
|
@ -262,9 +262,9 @@ function ClusterInformation({
|
|||
|
||||
<Option header="Loader">
|
||||
{/* TODO: fixme */}
|
||||
<Dropdown defaultSelectedKey="vanilla" onSelectionChange={e => onClusterLoaderChange(e)}>
|
||||
<Dropdown defaultSelectedKey="vanilla" onSelectionChange={e => onClusterLoaderChange(e.toString() as GameLoader)}>
|
||||
{LOADERS.map(loader => (
|
||||
<Dropdown.Item key={loader}>
|
||||
<Dropdown.Item id={loader} key={loader}>
|
||||
<div className="flex flex-row">
|
||||
<LoaderIcon className="size-5" loader={loader as GameLoader} />
|
||||
{' '}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,132 @@
|
|||
import type { NotificationData } from '@/hooks/useNotification';
|
||||
import useNotifications from '@/hooks/useNotification';
|
||||
import { Button } from '@onelauncher/common/components';
|
||||
import { XIcon } from '@untitled-theme/icons-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
interface ToastNotification extends NotificationData {
|
||||
id: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface NotificationToastProps {
|
||||
notification: ToastNotification;
|
||||
onRemove: (id: string) => void;
|
||||
}
|
||||
|
||||
function NotificationToast({ notification, onRemove }: NotificationToastProps) {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [isRemoving, setIsRemoving] = useState(false);
|
||||
|
||||
const handleRemove = useCallback(() => {
|
||||
setIsRemoving(true);
|
||||
setTimeout(() => {
|
||||
onRemove(notification.id);
|
||||
}, 300);
|
||||
}, [notification.id, onRemove]);
|
||||
|
||||
useEffect(() => {
|
||||
// Trigger entrance animation
|
||||
const timer = setTimeout(() => setIsVisible(true), 50);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Auto-remove after 5 seconds if no progress indicator
|
||||
if (notification.fraction === undefined) {
|
||||
const timer = setTimeout(handleRemove, 5000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [notification.fraction, handleRemove]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
'transform transition-all duration-300 ease-in-out',
|
||||
'bg-component-bg border border-component-border rounded-lg shadow-lg',
|
||||
'p-4 mb-3 max-w-sm',
|
||||
isVisible && !isRemoving ? 'translate-x-0 opacity-100' : 'translate-x-full opacity-0',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-semibold text-fg-primary text-sm truncate">
|
||||
{notification.title}
|
||||
</h4>
|
||||
<p className="text-fg-secondary text-xs mt-1 break-words">
|
||||
{notification.message}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
className="flex-shrink-0 h-6 w-6 p-0"
|
||||
color="ghost"
|
||||
onClick={handleRemove}
|
||||
size="icon"
|
||||
>
|
||||
<XIcon className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{notification.fraction !== undefined && (
|
||||
<div className="w-full bg-component-bg-hover rounded-full h-2 mt-3">
|
||||
<div
|
||||
className="bg-brand h-2 rounded-full transition-all duration-300"
|
||||
style={{ width: `${Math.max(0, Math.min(100, notification.fraction))}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NotificationToasts() {
|
||||
const { list } = useNotifications();
|
||||
const [toasts, setToasts] = useState<Array<ToastNotification>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentToasts = Object.entries(list).map(([id, data]) => ({
|
||||
id,
|
||||
...data,
|
||||
timestamp: Date.now(),
|
||||
}));
|
||||
|
||||
setToasts((prev) => {
|
||||
const existingIds = new Set(prev.map(t => t.id));
|
||||
const newToasts = currentToasts.filter(toast => !existingIds.has(toast.id));
|
||||
|
||||
const updatedToasts = prev.map((toast) => {
|
||||
const updated = currentToasts.find(t => t.id === toast.id);
|
||||
return updated ? { ...toast, ...updated } : toast;
|
||||
});
|
||||
|
||||
const currentIds = new Set(currentToasts.map(t => t.id));
|
||||
const filteredToasts = updatedToasts.filter(toast => currentIds.has(toast.id));
|
||||
|
||||
return [...filteredToasts, ...newToasts];
|
||||
});
|
||||
}, [list]);
|
||||
|
||||
const removeToast = (id: string) => {
|
||||
setToasts(prev => prev.filter(toast => toast.id !== id));
|
||||
};
|
||||
|
||||
if (toasts.length === 0)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-4 right-4 z-50 pointer-events-none">
|
||||
<div className="pointer-events-auto">
|
||||
{toasts.map(toast => (
|
||||
<NotificationToast
|
||||
key={toast.id}
|
||||
notification={toast}
|
||||
onRemove={removeToast}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default NotificationToasts;
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
import type { NotificationData } from '@/hooks/useNotification';
|
||||
import useNotifications from '@/hooks/useNotification';
|
||||
import { Button, Menu } from '@onelauncher/common/components';
|
||||
import { XIcon } from '@untitled-theme/icons-react';
|
||||
|
||||
interface NotificationItemProps {
|
||||
id: string;
|
||||
data: NotificationData;
|
||||
onRemove: (id: string) => void;
|
||||
}
|
||||
|
||||
function NotificationItem({ id, data, onRemove }: NotificationItemProps) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 p-3 bg-component-bg border border-component-border rounded-lg">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-semibold text-fg-primary text-sm truncate">
|
||||
{data.title}
|
||||
</h4>
|
||||
<p className="text-fg-secondary text-xs mt-1 break-words">
|
||||
{data.message}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
className="flex-shrink-0 h-6 w-6 p-0"
|
||||
color="ghost"
|
||||
onClick={() => onRemove(id)}
|
||||
size="icon"
|
||||
>
|
||||
<XIcon className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{data.fraction !== undefined && (
|
||||
<div className="w-full bg-component-bg-hover rounded-full h-2">
|
||||
<div
|
||||
className="bg-brand h-2 rounded-full transition-all duration-300"
|
||||
style={{ width: `${Math.max(0, Math.min(100, data.fraction))}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NotificationPopup() {
|
||||
const { list, clear, remove } = useNotifications();
|
||||
const notificationEntries = Object.entries(list);
|
||||
|
||||
const removeNotification = (id: string) => {
|
||||
remove(id);
|
||||
};
|
||||
|
||||
if (notificationEntries.length === 0)
|
||||
return (
|
||||
<Menu className="min-w-80 max-w-96">
|
||||
<Menu.Item>
|
||||
<div className="p-4 text-center">
|
||||
<p className="text-fg-secondary text-sm">No notifications</p>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<Menu className="min-w-80 max-w-96 max-h-96 overflow-y-auto">
|
||||
<Menu.Item>
|
||||
<div className="flex items-center justify-between p-3 border-b border-component-border">
|
||||
<h3 className="font-semibold text-fg-primary">Notifications</h3>
|
||||
{notificationEntries.length > 0 && (
|
||||
<Button
|
||||
className="text-xs"
|
||||
color="ghost"
|
||||
onClick={clear}
|
||||
size="normal"
|
||||
>
|
||||
Clear All
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Menu.Item>
|
||||
|
||||
{notificationEntries.map(([id, data]) => (
|
||||
<Menu.Item key={id}>
|
||||
<div className="p-2">
|
||||
<NotificationItem
|
||||
data={data}
|
||||
id={id}
|
||||
onRemove={removeNotification}
|
||||
/>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
export default NotificationPopup;
|
||||
81
apps/onelauncher/frontend/src/hooks/useNotification.tsx
Normal file
81
apps/onelauncher/frontend/src/hooks/useNotification.tsx
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import type { UnlistenFn } from '@tauri-apps/api/event';
|
||||
import type { ReactNode } from 'react';
|
||||
import { bindings } from '@/main';
|
||||
import { randomString } from '@/utils/index';
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
export interface NotificationData {
|
||||
title: string;
|
||||
message: string;
|
||||
fraction?: number | undefined;
|
||||
}
|
||||
|
||||
type Notifications = Record<string, NotificationData>;
|
||||
|
||||
export interface HookReturn {
|
||||
list: Notifications;
|
||||
set: (id: string, data: NotificationData) => void;
|
||||
create: (data: NotificationData) => void;
|
||||
remove: (id: string) => void;
|
||||
clear: () => void;
|
||||
}
|
||||
|
||||
interface NotificationContextValue {
|
||||
notifications: Notifications;
|
||||
setNotifications: React.Dispatch<React.SetStateAction<Notifications>>;
|
||||
}
|
||||
|
||||
const NotificationContext = createContext<NotificationContextValue | undefined>(undefined);
|
||||
|
||||
interface NotificationProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function NotificationProvider({ children }: NotificationProviderProps) {
|
||||
const [notifications, setNotifications] = useState<Notifications>({});
|
||||
|
||||
bindings.events.ingress.on((e) => {
|
||||
setNotifications(prev => ({
|
||||
...prev,
|
||||
[e.id]: {
|
||||
title: `${e.ingress_type}`,
|
||||
message: e.message,
|
||||
},
|
||||
}));
|
||||
}).then(u => u());
|
||||
|
||||
const ctx = {
|
||||
notifications,
|
||||
setNotifications,
|
||||
};
|
||||
|
||||
return (
|
||||
<NotificationContext.Provider value={ctx}>
|
||||
{children}
|
||||
</NotificationContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function useNotifications(): HookReturn {
|
||||
const context = useContext(NotificationContext);
|
||||
|
||||
if (!context)
|
||||
throw new Error('useNotifications must be used within a NotificationProvider');
|
||||
|
||||
const { notifications, setNotifications } = context;
|
||||
|
||||
const ctx: HookReturn = {
|
||||
list: notifications,
|
||||
set: (id, data) => setNotifications(prev => ({ ...prev, [id]: data })),
|
||||
create: data => ctx.set(randomString(6), data),
|
||||
remove: id => setNotifications((prev) => {
|
||||
const { [id]: removed, ...rest } = prev;
|
||||
return rest;
|
||||
}),
|
||||
clear: () => setNotifications({}),
|
||||
};
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export default useNotifications;
|
||||
20
apps/onelauncher/frontend/src/hooks/usePopState.ts
Normal file
20
apps/onelauncher/frontend/src/hooks/usePopState.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { useLocation } from '@tanstack/react-router';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export default function usePopState(onRouteChange: void | any) {
|
||||
const location = useLocation();
|
||||
const prevLocationRef = useRef(location);
|
||||
|
||||
useEffect(() => {
|
||||
const prevLocation = prevLocationRef.current;
|
||||
|
||||
// Only run if the pathname actually changed
|
||||
if (prevLocation.pathname !== location.pathname)
|
||||
onRouteChange?.({
|
||||
from: prevLocation,
|
||||
to: location,
|
||||
});
|
||||
|
||||
prevLocationRef.current = location;
|
||||
}, [location, onRouteChange]);
|
||||
}
|
||||
|
|
@ -4,8 +4,9 @@ import { createRouter, RouterProvider } from '@tanstack/react-router';
|
|||
import { StrictMode } from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { createTauRPCProxy } from './bindings.gen';
|
||||
import { routeTree } from './routeTree.gen';
|
||||
import { NotificationProvider } from './hooks/useNotification';
|
||||
|
||||
import { routeTree } from './routeTree.gen';
|
||||
import './fonts';
|
||||
import 'overlayscrollbars/overlayscrollbars.css';
|
||||
import './styles/global.css';
|
||||
|
|
@ -55,7 +56,9 @@ if (rootElement && !rootElement.innerHTML) {
|
|||
root.render(
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={router} />
|
||||
<NotificationProvider>
|
||||
<RouterProvider router={router} />
|
||||
</NotificationProvider>
|
||||
</QueryClientProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import type { HookReturn } from '@/hooks/useNotification';
|
||||
import type { QueryClient } from '@tanstack/react-query';
|
||||
import type { NavigateOptions, ToOptions } from '@tanstack/react-router';
|
||||
import NotificationToasts from '@/components/overlay/NotificationToasts';
|
||||
import WindowFrame from '@/components/WindowFrame';
|
||||
import { AnimatedOutletProvider } from '@onelauncher/common/components';
|
||||
import { createRootRouteWithContext, Outlet, useRouter } from '@tanstack/react-router';
|
||||
|
|
@ -34,6 +36,7 @@ function RootRoute() {
|
|||
|
||||
<Outlet />
|
||||
|
||||
<NotificationToasts />
|
||||
<TanStackRouterDevtools />
|
||||
</div>
|
||||
</RouterProvider>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import Modal from '@/components/overlay/Modal';
|
||||
import useNotifications from '@/hooks/useNotification';
|
||||
import { bindings } from '@/main';
|
||||
import { useCommand } from '@onelauncher/common';
|
||||
import { Button, Dropdown, TextField } from '@onelauncher/common/components';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
|
|
@ -7,6 +10,31 @@ export const Route = createFileRoute('/app/browser/')({
|
|||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const { create, list, clear } = useNotifications();
|
||||
const result = useCommand('read_settings', bindings.core.readSettings);
|
||||
|
||||
const addSimpleNotif = () => {
|
||||
create({
|
||||
message: 'This is a simple notification message',
|
||||
title: 'Simple Notification',
|
||||
});
|
||||
};
|
||||
|
||||
const addProgressNotif = () => {
|
||||
create({
|
||||
message: 'Download in progress...',
|
||||
title: 'Downloading File',
|
||||
fraction: Math.floor(Math.random() * 100),
|
||||
});
|
||||
};
|
||||
|
||||
const addErrorNotif = () => {
|
||||
create({
|
||||
message: 'Something went wrong while processing your request',
|
||||
title: 'Error',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>asdasdas</p>
|
||||
|
|
@ -26,6 +54,31 @@ function RouteComponent() {
|
|||
<Dropdown.Item>Slmalr</Dropdown.Item>
|
||||
<Dropdown.Item>Slmalr 2</Dropdown.Item>
|
||||
</Dropdown>
|
||||
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button onClick={addSimpleNotif}>Add Simple Notification</Button>
|
||||
<Button onClick={addProgressNotif}>Add Progress Notification</Button>
|
||||
<Button onClick={addErrorNotif}>Add Error Notification</Button>
|
||||
<Button onClick={clear}>Clear All Notifications</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<h3 className="font-semibold mb-2">
|
||||
Current Notifications (
|
||||
{Object.keys(list).length}
|
||||
):
|
||||
</h3>
|
||||
<pre className="text-xs bg-component-bg-hover p-2 rounded overflow-auto max-h-40">
|
||||
{JSON.stringify(list, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<h3>Settings</h3>
|
||||
<pre className="text-xs bg-component-bg-hover p-2 rounded overflow-auto max-h-40">
|
||||
{JSON.stringify(result.data, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,11 +37,6 @@ function RouteComponent() {
|
|||
subscribed: false,
|
||||
});
|
||||
|
||||
bindings.events.process.on((e) => {
|
||||
// eslint-disable-next-line no-console -- ok
|
||||
console.log(e);
|
||||
});
|
||||
|
||||
function handleEditMode() {
|
||||
if (edit)
|
||||
editing.refetch();
|
||||
|
|
@ -192,7 +187,12 @@ function Banner({
|
|||
</Show>
|
||||
|
||||
{/* <ClusterCover class="h-full w-full object-cover" cluster={props.cluster} override={props.newCover()} /> */}
|
||||
<img src={image()} />
|
||||
<img
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).src = DefaultInstancePhoto;
|
||||
}}
|
||||
src={image()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full flex flex-col justify-between gap-y-.5 overflow-hidden text-fg-primary">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { ClusterModel, IngressType } from '@/bindings.gen';
|
||||
import type { ClusterModel } from '@/bindings.gen';
|
||||
import DefaultBanner from '@/assets/images/default_banner.png';
|
||||
import DefaultInstancePhoto from '@/assets/images/default_instance_cover.jpg';
|
||||
import { NewClusterCreate } from '@/components/launcher/cluster/ClusterCreation';
|
||||
|
|
@ -6,10 +6,10 @@ import useRecentCluster from '@/hooks/useCluster';
|
|||
import { bindings } from '@/main';
|
||||
import { upperFirst } from '@/utils';
|
||||
import { useCommand } from '@onelauncher/common';
|
||||
import { Button, Show, TextField } from '@onelauncher/common/components';
|
||||
import { createFileRoute, Link, useNavigate } from '@tanstack/react-router';
|
||||
import { Button, Show } from '@onelauncher/common/components';
|
||||
import { createFileRoute, Link } from '@tanstack/react-router';
|
||||
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||
import { PlayIcon, Server01Icon } from '@untitled-theme/icons-react';
|
||||
import { PlayIcon } from '@untitled-theme/icons-react';
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||
|
||||
export const Route = createFileRoute('/app/')({
|
||||
|
|
@ -69,6 +69,9 @@ function Banner() {
|
|||
<img
|
||||
alt="Default Banner"
|
||||
className="top-0 left-0 h-full rounded-xl w-full object-cover"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).src = DefaultBanner;
|
||||
}}
|
||||
src={cluster?.icon_url || DefaultBanner}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -174,12 +177,15 @@ function ClusterCard({
|
|||
const image = () => {
|
||||
const url = icon_url;
|
||||
|
||||
if (url === null)
|
||||
if (!url)
|
||||
return DefaultInstancePhoto;
|
||||
|
||||
return convertFileSrc(url);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-console -- ok
|
||||
console.log(image());
|
||||
|
||||
return (
|
||||
<>
|
||||
<Link
|
||||
|
|
@ -196,13 +202,18 @@ function ClusterCard({
|
|||
>
|
||||
<img
|
||||
className="h-full w-full object-cover"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).src = DefaultInstancePhoto;
|
||||
}}
|
||||
src={image()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="z-10 flex flex-row items-center justify-between gap-x-3 p-3">
|
||||
<div className="h-full flex flex-col gap-1.5 overflow-hidden">
|
||||
<p className="h-4 text-ellipsis whitespace-nowrap font-medium">{name}</p>
|
||||
<p className="h-4 text-ellipsis whitespace-nowrap font-medium">
|
||||
{name}
|
||||
</p>
|
||||
<p className="h-4 text-xs">
|
||||
{mc_loader}
|
||||
{' '}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import DiscordIcon from '@/assets/logos/discord.svg';
|
||||
import SettingsRow from '@/components/SettingsRow';
|
||||
import usePopState from '@/hooks/usePopState';
|
||||
import { bindings } from '@/main';
|
||||
import { useCommand } from '@onelauncher/common';
|
||||
import { Button, Switch } from '@onelauncher/common/components';
|
||||
|
|
@ -14,7 +15,16 @@ export const Route = createFileRoute('/app/settings/')({
|
|||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const _result = useCommand('getGlobalProfile', bindings.core.getGlobalProfile);
|
||||
const result = useCommand('readSettings', bindings.core.readSettings);
|
||||
|
||||
const save = useCommand('writeSettings', () => bindings.core.writeSettings(result.data!), {
|
||||
enabled: false,
|
||||
subscribed: false,
|
||||
});
|
||||
|
||||
usePopState(() => {
|
||||
save.refetch();
|
||||
});
|
||||
|
||||
const openLauncherDir = async () => {
|
||||
const dir = await dataDir();
|
||||
|
|
@ -32,7 +42,13 @@ function RouteComponent() {
|
|||
icon={<img className="w-6 invert-100" src={DiscordIcon} />}
|
||||
title="Discord RPC"
|
||||
>
|
||||
<Switch />
|
||||
<Switch
|
||||
defaultSelected={result.data?.discord_enabled}
|
||||
onChange={(val) => {
|
||||
if (result.data)
|
||||
result.data.discord_enabled = val;
|
||||
}}
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import type { SettingProfileModel } from '@/bindings.gen';
|
||||
import ScrollableContainer from '@/components/ScrollableContainer';
|
||||
import SettingsRow from '@/components/SettingsRow';
|
||||
import usePopState from '@/hooks/usePopState';
|
||||
import { bindings } from '@/main';
|
||||
import { useCommand } from '@onelauncher/common';
|
||||
import { Switch, TextField } from '@onelauncher/common/components';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { Database01Icon, FilePlus02Icon, FileX02Icon, LayoutTopIcon, Maximize01Icon, ParagraphWrapIcon, XIcon } from '@untitled-theme/icons-react';
|
||||
|
|
@ -10,13 +14,24 @@ export const Route = createFileRoute('/app/settings/minecraft')({
|
|||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const result = useCommand('readSettings', bindings.core.readSettings);
|
||||
|
||||
const save = useCommand('writeSettings', () => bindings.core.writeSettings(result.data!), {
|
||||
enabled: false,
|
||||
subscribed: false,
|
||||
});
|
||||
|
||||
usePopState(() => {
|
||||
save.refetch();
|
||||
});
|
||||
|
||||
return (
|
||||
<Sidebar.Page>
|
||||
<ScrollableContainer>
|
||||
<div className="h-full">
|
||||
<h1>Minecraft Settings</h1>
|
||||
|
||||
<GameSettings />
|
||||
<GameSettings settings={result.data!.global_game_settings} />
|
||||
|
||||
{/* <LauncherSettings /> */}
|
||||
|
||||
|
|
@ -27,7 +42,13 @@ function RouteComponent() {
|
|||
);
|
||||
}
|
||||
|
||||
export function GameSettings() {
|
||||
interface GameSettingProps {
|
||||
settings: SettingProfileModel;
|
||||
}
|
||||
|
||||
export function GameSettings(props: GameSettingProps) {
|
||||
const { settings } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsRow.Header>Game</SettingsRow.Header>
|
||||
|
|
@ -37,7 +58,12 @@ export function GameSettings() {
|
|||
icon={<Maximize01Icon />}
|
||||
title="Force Fullscreen"
|
||||
>
|
||||
<Switch />
|
||||
<Switch
|
||||
defaultSelected={settings.force_fullscreen ?? false}
|
||||
onChange={(val) => {
|
||||
settings.force_fullscreen = val;
|
||||
}}
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
|
|
@ -48,11 +74,19 @@ export function GameSettings() {
|
|||
<div className="grid grid-cols-[70px_16px_70px] gap-2 grid-justify-center grid-items-center">
|
||||
<TextField
|
||||
className="text-center"
|
||||
defaultValue={settings.res?.width.toString()}
|
||||
onChange={(e) => {
|
||||
settings.res!.width = Number(e.target.value);
|
||||
}}
|
||||
type="number"
|
||||
/>
|
||||
<XIcon className="size-4 self-center" />
|
||||
<TextField
|
||||
className="text-center"
|
||||
defaultValue={settings.res?.height.toString()}
|
||||
onChange={(e) => {
|
||||
settings.res!.height = Number(e.target.value);
|
||||
}}
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -65,21 +99,12 @@ export function GameSettings() {
|
|||
>
|
||||
<div className="flex items-center gap-x-4 flex-justify-center">
|
||||
<div className="flex flex-row items-center gap-x-2">
|
||||
<span>Min:</span>
|
||||
<TextField
|
||||
className="text-center"
|
||||
max={2024}
|
||||
min={1}
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center gap-x-2">
|
||||
<span>Max:</span>
|
||||
<TextField
|
||||
className="text-center"
|
||||
max={2024}
|
||||
min={1}
|
||||
defaultValue={settings.mem_max?.toString()}
|
||||
onChange={(e) => {
|
||||
settings.mem_max = Number(e.target.value);
|
||||
}}
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use interpulse::api::minecraft::Version;
|
|||
use onelauncher_entity::{clusters, icon::Icon, loader::GameLoader, prelude::entity};
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use serde::Serialize;
|
||||
use crate::store::{Settings, State};
|
||||
use tauri::{AppHandle, Runtime};
|
||||
|
||||
use crate::{api::{self, cluster::dao::ClusterId}, error::{LauncherError, LauncherResult}, store::{credentials::MinecraftCredentials, Core}};
|
||||
|
|
@ -64,6 +65,13 @@ pub trait TauriLauncherApi {
|
|||
|
||||
#[taurpc(alias = "openMsaLogin")]
|
||||
async fn open_msa_login<R: Runtime>(app_handle: AppHandle<R>) -> LauncherResult<Option<MinecraftCredentials>>;
|
||||
|
||||
// Settings
|
||||
#[taurpc(alias = "readSettings")]
|
||||
async fn read_settings() -> LauncherResult<Settings>;
|
||||
|
||||
#[taurpc(alias = "writeSettings")]
|
||||
async fn write_settings(setting: Settings) -> LauncherResult<()>;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -262,4 +270,22 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
|
|||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn read_settings(self) -> LauncherResult<Settings> {
|
||||
let state = State::get().await?;
|
||||
let settings = state.settings.read().await;
|
||||
|
||||
Ok(settings.clone())
|
||||
}
|
||||
|
||||
async fn write_settings(self, setting: Settings) -> LauncherResult<()> {
|
||||
let state = State::get().await?;
|
||||
let mut settings = state.settings.write().await;
|
||||
|
||||
*settings = setting;
|
||||
|
||||
settings.save().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@ const dropdownStyles = tv({
|
|||
triggerCustomIcon: 'w-4 h-4',
|
||||
|
||||
popover: [
|
||||
// width is not full instead it behaves like w-screen for some fucking reason
|
||||
'mt-1 rounded-lg shadow-md',
|
||||
// i'll fix this if i dont forget
|
||||
'mt-1 w-full rounded-lg shadow-md',
|
||||
'bg-component-bg border border-component-border',
|
||||
'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',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue