merge(notifications): Merged notification work
This commit is contained in:
parent
3c0c402410
commit
8e5d7e77fc
20 changed files with 533 additions and 46 deletions
|
|
@ -20,6 +20,7 @@
|
|||
"dependencies": {
|
||||
"@fontsource/poppins": "^5.0.14",
|
||||
"@iconify-json/logos": "^1.1.42",
|
||||
"@solid-primitives/refs": "^1.0.8",
|
||||
"@solidjs/router": "^0.13.3",
|
||||
"@tauri-apps/api": "^2.0.0-beta",
|
||||
"@tauri-apps/plugin-shell": "github:tauri-apps/tauri-plugin-shell#v2",
|
||||
|
|
|
|||
|
|
@ -44,8 +44,8 @@
|
|||
"hiddenTitle": true,
|
||||
"width": 1280,
|
||||
"height": 802,
|
||||
"minWidth": 600,
|
||||
"minHeight": 400,
|
||||
"minWidth": 1020,
|
||||
"minHeight": 600,
|
||||
"resizable": true,
|
||||
"center": true,
|
||||
"decorations": false,
|
||||
|
|
|
|||
46
desktop/src/bridge/notifications.ts
Normal file
46
desktop/src/bridge/notifications.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { invoke } from '@tauri-apps/api/core';
|
||||
import type { UnlistenFn } from '@tauri-apps/api/event';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
|
||||
export enum NotificationType {
|
||||
Info = 'Info',
|
||||
Alert = 'Alert',
|
||||
Success = 'Success',
|
||||
Download = 'Download',
|
||||
DownloadSuccess = 'DownloadSuccess',
|
||||
DownloadError = 'DownloadError',
|
||||
Refresh = 'Refresh',
|
||||
}
|
||||
|
||||
export async function addNotification(notification: MakeOptional<Core.Notification, 'created_at'>): Promise<void> {
|
||||
await invoke('plugin:onelauncher|notifications_add', {
|
||||
notification: {
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
...notification,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function removeNotification(index: number): Promise<void> {
|
||||
await invoke('plugin:onelauncher|notifications_remove_by_index', { index });
|
||||
}
|
||||
|
||||
export async function clearNotifications(): Promise<void> {
|
||||
await invoke('plugin:onelauncher|notifications_clear');
|
||||
}
|
||||
|
||||
export async function getNotifications(): Promise<Core.Notification[]> {
|
||||
return await invoke('plugin:onelauncher|notifications_get');
|
||||
}
|
||||
|
||||
export async function on<
|
||||
T extends Core.NotificationEvents,
|
||||
>(
|
||||
event: T,
|
||||
callback: () => void,
|
||||
): Promise<UnlistenFn> {
|
||||
return await listen<string>(`notifications_event`, (e) => {
|
||||
if (e.payload.toLowerCase() === event)
|
||||
callback();
|
||||
});
|
||||
};
|
||||
1
desktop/src/constants.ts
Normal file
1
desktop/src/constants.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const WEBSITE = 'https://polyfrost.org/';
|
||||
11
desktop/src/types/core.d.ts
vendored
Normal file
11
desktop/src/types/core.d.ts
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
declare namespace Core {
|
||||
export interface Notification {
|
||||
created_at: number;
|
||||
title: string;
|
||||
message: string;
|
||||
notification_type: NotificationType;
|
||||
progress?: number;
|
||||
}
|
||||
|
||||
export type NotificationEvents = 'added' | 'removed' | 'cleared';
|
||||
}
|
||||
1
desktop/src/types/global.d.ts
vendored
1
desktop/src/types/global.d.ts
vendored
|
|
@ -1,3 +1,4 @@
|
|||
declare namespace globalThis {
|
||||
export type WithIndex<T> = T & { index: number }
|
||||
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
|
||||
}
|
||||
|
|
@ -2,8 +2,9 @@ import type { ParentProps } from 'solid-js';
|
|||
import { Transition } from 'solid-transition-group';
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-solid';
|
||||
import environment from '../utils/environment';
|
||||
import WindowFrame from './components/native/WindowFrame';
|
||||
import WindowFrame from './components/WindowFrame';
|
||||
import Navbar from './components/Navbar';
|
||||
import ErrorBoundary from './components/ErrorBoundary';
|
||||
|
||||
function App(props: ParentProps) {
|
||||
if (!environment.isDev()) {
|
||||
|
|
@ -14,21 +15,21 @@ function App(props: ParentProps) {
|
|||
|
||||
return (
|
||||
<main class="flex flex-col bg-primary w-full min-h-screen overflow-hidden h-screen max-h-screen text-white">
|
||||
|
||||
<WindowFrame />
|
||||
<div class="flex flex-col px-8">
|
||||
<Navbar />
|
||||
</div>
|
||||
|
||||
{/* This was a pain to do */}
|
||||
<div class="relative h-full w-full overflow-hidden">
|
||||
<div class="absolute top-0 left-0 flex flex-col h-full w-full overflow-x-hidden">
|
||||
<OverlayScrollbarsComponent class="absolute top-0 left-0 flex flex-col h-full w-full overflow-x-hidden overflow-y-auto px-8">
|
||||
<AnimatedRoutes>
|
||||
{props.children}
|
||||
</AnimatedRoutes>
|
||||
<div class="pb-8" />
|
||||
</OverlayScrollbarsComponent>
|
||||
<ErrorBoundary>
|
||||
<OverlayScrollbarsComponent class="absolute top-0 left-0 flex flex-col h-full w-full overflow-x-hidden overflow-y-auto px-8">
|
||||
<AnimatedRoutes>
|
||||
{props.children}
|
||||
</AnimatedRoutes>
|
||||
<div class="pb-8" />
|
||||
</OverlayScrollbarsComponent>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
21
desktop/src/ui/components/ErrorBoundary.tsx
Normal file
21
desktop/src/ui/components/ErrorBoundary.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import type { ParentProps } from 'solid-js';
|
||||
import { ErrorBoundary as Capturer } from 'solid-js';
|
||||
|
||||
// TODO: Uncomment code
|
||||
function ErrorBoundary(props: ParentProps) {
|
||||
return (
|
||||
<>{props.children}</>
|
||||
// <Capturer
|
||||
// fallback={err => (
|
||||
// <div class="p-2 bg-danger/20 rounded-md border-danger border">
|
||||
// <h2>An unexpected error has occurred</h2>
|
||||
// <p>{err.toString()}</p>
|
||||
// </div>
|
||||
// )}
|
||||
// >
|
||||
// {props.children}
|
||||
// </Capturer>
|
||||
);
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
import { A } from '@solidjs/router';
|
||||
import { createSignal } from 'solid-js';
|
||||
import { Bell01Icon, Cloud01Icon, Settings01Icon, TerminalBrowserIcon } from '@untitled-theme/icons-solid';
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import useAccount from '../hooks/useAccount';
|
||||
import PolyfrostFull from './logos/PolyfrostFull';
|
||||
import AccountPopup from './overlay/AccountPopup';
|
||||
import PlayerHead from './game/PlayerHead';
|
||||
import Button from './base/Button';
|
||||
import NotificationPopup from './overlay/notifications/NotificationPopup';
|
||||
import Popup from './overlay/Popup';
|
||||
import { WEBSITE } from '~constants';
|
||||
|
||||
interface NavbarLinkProps {
|
||||
path: string;
|
||||
|
|
@ -28,38 +32,54 @@ function NavbarLink(props: NavbarLinkProps) {
|
|||
|
||||
function Navbar() {
|
||||
const [profileMenuOpen, setProfileMenuOpen] = createSignal(false);
|
||||
const [notificationMenuOpen, setNotificationMenuOpen] = createSignal(false);
|
||||
|
||||
const account = useAccount();
|
||||
|
||||
let profileButton!: HTMLButtonElement;
|
||||
let notificationButton!: HTMLButtonElement;
|
||||
|
||||
return (
|
||||
<div class="flex flex-row justify-center items-center min-h-[60px] h-15 mx-2">
|
||||
<div class="flex flex-row *:flex-1 items-center min-h-[60px] h-15 mx-2">
|
||||
<div>
|
||||
<PolyfrostFull />
|
||||
<div onClick={() => open(WEBSITE)} class="flex items-center justify-center active:scale-90 transition-transform w-min">
|
||||
<PolyfrostFull />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-1 flex-row items-center justify-center gap-x-10 py-1">
|
||||
<div class="flex flex-row items-center justify-center gap-x-10 py-1">
|
||||
<NavbarLink path="/" label="Home" />
|
||||
<NavbarLink path="/browser" label="Browser" />
|
||||
<NavbarLink path="/updates" label="Updates" />
|
||||
</div>
|
||||
<div class="flex flex-row justify-end items-center gap-x-2 relative">
|
||||
<Button styleType="icon" class="w-8 h-8">
|
||||
<TerminalBrowserIcon class="w-7 h-7" />
|
||||
<Button buttonStyle="icon">
|
||||
<TerminalBrowserIcon />
|
||||
</Button>
|
||||
|
||||
<Button styleType="icon" class="w-8 h-8">
|
||||
<Cloud01Icon class="w-7 h-7" />
|
||||
<Button buttonStyle="icon">
|
||||
<Cloud01Icon />
|
||||
</Button>
|
||||
|
||||
<Button styleType="icon" class="w-8 h-8">
|
||||
<Bell01Icon class="w-7 h-7" />
|
||||
{/* Notification Manager Button */}
|
||||
<Button
|
||||
buttonStyle="icon"
|
||||
ref={notificationButton}
|
||||
class="[&>div]:absolute relative"
|
||||
onClick={() => setNotificationMenuOpen(!notificationMenuOpen())}
|
||||
>
|
||||
<Bell01Icon />
|
||||
</Button>
|
||||
|
||||
<Button styleType="icon" class="w-8 h-8">
|
||||
<Settings01Icon class="w-7 h-7" />
|
||||
{/* Launcher Settings Button */}
|
||||
<Button buttonStyle="icon">
|
||||
<Settings01Icon />
|
||||
</Button>
|
||||
|
||||
<button ref={profileButton} onClick={() => setProfileMenuOpen(!profileMenuOpen())}>
|
||||
{/* Account Menu Button */}
|
||||
<button
|
||||
ref={profileButton}
|
||||
onClick={() => setProfileMenuOpen(!profileMenuOpen())}
|
||||
>
|
||||
<PlayerHead class="w-[30px] h-[30px] rounded-md hover:opacity-70" uuid={account.uuid} />
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -67,8 +87,18 @@ function Navbar() {
|
|||
<AccountPopup
|
||||
visible={profileMenuOpen}
|
||||
setVisible={setProfileMenuOpen}
|
||||
mount={profileButton}
|
||||
class="right-0 top-full mt-2"
|
||||
ref={(el) => {
|
||||
el.style.left = `${profileButton.getBoundingClientRect().right - el.getBoundingClientRect().width}px`;
|
||||
el.style.top = `${profileButton.getBoundingClientRect().bottom}px`;
|
||||
}}
|
||||
class="mt-2"
|
||||
/>
|
||||
|
||||
<NotificationPopup
|
||||
visible={notificationMenuOpen}
|
||||
setVisible={setNotificationMenuOpen}
|
||||
ref={el => Popup.setPos(notificationButton, el)}
|
||||
class="mt-2"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
48
desktop/src/ui/components/TimeAgo.tsx
Normal file
48
desktop/src/ui/components/TimeAgo.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { type ParentProps, createSignal, onCleanup, onMount } from 'solid-js';
|
||||
|
||||
type TimeAgoProps = ParentProps & {
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
function TimeAgo(props: TimeAgoProps) {
|
||||
const [time, setTime] = createSignal('');
|
||||
const [intervalId, setIntervalId] = createSignal<NodeJS.Timeout | undefined>(undefined);
|
||||
|
||||
onMount(() => {
|
||||
const formatter = new Intl.RelativeTimeFormat(navigator.language, { numeric: 'auto' });
|
||||
const units = {
|
||||
year: 24 * 60 * 60 * 1000 * 365,
|
||||
month: 24 * 60 * 60 * 1000 * 365 / 12,
|
||||
day: 24 * 60 * 60 * 1000,
|
||||
hour: 60 * 60 * 1000,
|
||||
minute: 60 * 1000,
|
||||
second: 1000,
|
||||
};
|
||||
|
||||
const getRelativeTime = (timestamp: number): string => {
|
||||
const elapsed = timestamp - Date.now();
|
||||
|
||||
for (const [unit, ms] of Object.entries(units)) {
|
||||
if (Math.abs(elapsed) > ms || unit === 'second')
|
||||
return formatter.format(Math.round(elapsed / ms), unit as Intl.RelativeTimeFormatUnit);
|
||||
}
|
||||
|
||||
return 'now';
|
||||
};
|
||||
|
||||
const loop = () => {
|
||||
setTime(getRelativeTime(props.timestamp));
|
||||
};
|
||||
|
||||
loop();
|
||||
setIntervalId(setInterval(loop, 1000));
|
||||
});
|
||||
|
||||
onCleanup(() => {
|
||||
clearInterval(intervalId()!);
|
||||
});
|
||||
|
||||
return <span>{time()}</span>;
|
||||
}
|
||||
|
||||
export default TimeAgo;
|
||||
|
|
@ -2,8 +2,8 @@ import { Window } from '@tauri-apps/api/window';
|
|||
import { MinusIcon, XCloseIcon } from '@untitled-theme/icons-solid';
|
||||
import type { JSX } from 'solid-js';
|
||||
import { createSignal, onMount } from 'solid-js';
|
||||
import Modal from '../overlay/Modal';
|
||||
import Button from '../base/Button';
|
||||
import Modal from './overlay/Modal';
|
||||
import Button from './base/Button';
|
||||
|
||||
interface TitlebarButtonProps {
|
||||
icon: (any: any) => JSX.Element;
|
||||
|
|
@ -50,8 +50,8 @@ function WindowFrame() {
|
|||
visible={isModalVisible}
|
||||
setVisible={setModalVisible}
|
||||
buttons={[
|
||||
<Button styleType="secondary" onClick={() => setModalVisible(false)}>No</Button>,
|
||||
<Button styleType="danger" onClick={() => quit()}>Yes</Button>,
|
||||
<Button buttonStyle="secondary" onClick={() => setModalVisible(false)}>No</Button>,
|
||||
<Button buttonStyle="danger" onClick={() => quit()}>Yes</Button>,
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -21,7 +21,14 @@
|
|||
}
|
||||
|
||||
&__icon {
|
||||
@apply p-1.5 hover:bg-gray-0.05 active:bg-gray-0.10 aspect-square box-border;
|
||||
@apply p-1.5 hover:bg-gray-0.05 active:bg-gray-0.10 aspect-square box-border w-8 h-8;
|
||||
|
||||
&.large {
|
||||
@apply w-9 h-9;
|
||||
& > * {
|
||||
@apply p-0.5;
|
||||
}
|
||||
}
|
||||
|
||||
& > * {
|
||||
@apply w-full h-full stroke-fg-primary;
|
||||
|
|
@ -43,4 +50,4 @@
|
|||
|
||||
.icon > * {
|
||||
@apply w-4;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,15 +5,16 @@ import styles from './Button.module.scss';
|
|||
type ButtonProps = JSX.ButtonHTMLAttributes<HTMLButtonElement> & ParentProps & {
|
||||
iconLeft?: JSX.Element;
|
||||
iconRight?: JSX.Element;
|
||||
styleType?: 'primary' | 'secondary' | 'danger' | 'icon' | 'ghost';
|
||||
buttonStyle?: 'primary' | 'secondary' | 'danger' | 'icon' | 'ghost';
|
||||
large?: boolean;
|
||||
};
|
||||
|
||||
function Button(props: ButtonProps) {
|
||||
const [buttonProps, rest] = splitProps(props, ['iconLeft', 'iconRight', 'styleType', 'class']);
|
||||
const merged = mergeProps({ styleType: 'primary' }, buttonProps);
|
||||
const [buttonProps, rest] = splitProps(props, ['iconLeft', 'iconRight', 'buttonStyle', 'class', 'large']);
|
||||
const merged = mergeProps({ buttonStyle: 'primary', large: false }, buttonProps);
|
||||
|
||||
return (
|
||||
<button class={`${styles.button} ${styles[`button__${merged.styleType}`]} ${buttonProps.class || ''}`} {...rest}>
|
||||
<button class={`${styles.button} ${styles[`button__${merged.buttonStyle}`]} ${merged.large ? styles.large : ''} ${buttonProps.class || ''}`} {...rest}>
|
||||
{merged.iconLeft && <span class={styles.icon}>{merged.iconLeft}</span>}
|
||||
{props.children}
|
||||
{merged.iconRight && <span class={styles.icon}>{merged.iconRight}</span>}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ function AccountComponent(props: AccountComponentProps) {
|
|||
</div>
|
||||
</div>
|
||||
{props.loggedIn && (
|
||||
<Button styleType="icon" class="w-8 h-8">
|
||||
<Button buttonStyle="icon">
|
||||
<LogOut01Icon class=" stroke-danger" />
|
||||
</Button>
|
||||
)}
|
||||
|
|
@ -50,11 +50,11 @@ function AccountPopup(props: Popup.PopupProps) {
|
|||
|
||||
<div class="flex flex-row justify-between">
|
||||
<div>
|
||||
<Button styleType="ghost" iconLeft={<PlusIcon />}>Add Account</Button>
|
||||
<Button buttonStyle="ghost" iconLeft={<PlusIcon />}>Add Account</Button>
|
||||
</div>
|
||||
<div class="flex flex-row">
|
||||
<Button styleType="icon" class="w-9 h-9">
|
||||
<Settings01Icon class="p-0.5 stroke-fg-primary" />
|
||||
<Button buttonStyle="icon" large>
|
||||
<Settings01Icon class="stroke-fg-primary" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { mergeRefs } from '@solid-primitives/refs';
|
||||
import {
|
||||
Show,
|
||||
createEffect,
|
||||
|
|
@ -11,13 +12,13 @@ import { Portal } from 'solid-js/web';
|
|||
|
||||
// eslint-disable-next-line ts/no-namespace
|
||||
declare namespace Popup {
|
||||
interface PopupProps extends ParentProps {
|
||||
type PopupProps = Omit<Parameters<typeof Portal>[0], 'children'> & ParentProps & {
|
||||
visible: Accessor<boolean>;
|
||||
setVisible: Setter<boolean>;
|
||||
class?: string;
|
||||
style?: JSX.CSSProperties | string;
|
||||
mount?: Node;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function Popup(props: Popup.PopupProps) {
|
||||
|
|
@ -53,8 +54,14 @@ function Popup(props: Popup.PopupProps) {
|
|||
|
||||
return (
|
||||
<Show when={localVisible()}>
|
||||
<Portal mount={props.mount || document.body}>
|
||||
<div ref={popupRef} style={props.style || ''} class={`absolute z-[1000] animate-duration-150 ${animate()} ${props.class || ''}`}>
|
||||
<Portal
|
||||
mount={props.mount || document.body}
|
||||
ref={mergeRefs((el) => {
|
||||
el.classList.add('absolute', 'z-[1000]');
|
||||
return el;
|
||||
}, props.ref)}
|
||||
>
|
||||
<div ref={popupRef} style={props.style || ''} class={`${animate()} ${props.class || ''}`}>
|
||||
{props.children}
|
||||
</div>
|
||||
</Portal>
|
||||
|
|
@ -62,4 +69,14 @@ function Popup(props: Popup.PopupProps) {
|
|||
);
|
||||
}
|
||||
|
||||
Popup.setPos = (parent: HTMLElement, ref: (SVGGElement | HTMLDivElement)) => {
|
||||
const parentRect = parent.getBoundingClientRect();
|
||||
|
||||
const top = parentRect.bottom;
|
||||
const right = document.body.clientWidth - parentRect.right;
|
||||
|
||||
ref.style.top = `${top}px`;
|
||||
ref.style.right = `${right}px`;
|
||||
};
|
||||
|
||||
export default Popup;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
import { type JSX, Show } from 'solid-js';
|
||||
import { AlertTriangleIcon, CheckCircleIcon, FolderCheckIcon, FolderDownloadIcon, FolderXIcon, InfoCircleIcon, RefreshCcw02Icon } from '@untitled-theme/icons-solid';
|
||||
import TimeAgo from '../../TimeAgo';
|
||||
import { NotificationType } from '../../../../bridge/notifications';
|
||||
|
||||
interface NotificationComponentProps {
|
||||
overlay: boolean;
|
||||
data: Core.Notification;
|
||||
};
|
||||
|
||||
function IconFromNotificationType(type: NotificationType): (props: JSX.HTMLAttributes<HTMLDivElement>) => JSX.Element {
|
||||
switch (type) {
|
||||
case NotificationType.Alert:
|
||||
return AlertTriangleIcon as any;
|
||||
case NotificationType.Download:
|
||||
return FolderDownloadIcon as any;
|
||||
case NotificationType.DownloadSuccess:
|
||||
return FolderCheckIcon as any;
|
||||
case NotificationType.DownloadError:
|
||||
return FolderXIcon as any;
|
||||
case NotificationType.Refresh:
|
||||
return RefreshCcw02Icon as any;
|
||||
case NotificationType.Success:
|
||||
return CheckCircleIcon as any;
|
||||
case NotificationType.Info:
|
||||
default:
|
||||
return InfoCircleIcon as any;
|
||||
}
|
||||
}
|
||||
|
||||
function ColorFromNotificationType(type: NotificationType): string {
|
||||
switch (type) {
|
||||
case NotificationType.Info:
|
||||
case NotificationType.Refresh:
|
||||
return 'text-brand';
|
||||
|
||||
case NotificationType.Alert:
|
||||
case NotificationType.DownloadError:
|
||||
return 'text-danger';
|
||||
|
||||
case NotificationType.DownloadSuccess:
|
||||
case NotificationType.Success:
|
||||
return 'text-success';
|
||||
|
||||
case NotificationType.Download:
|
||||
default:
|
||||
return 'text-fg-primary';
|
||||
}
|
||||
}
|
||||
|
||||
function NotificationOverlayComponent(props: NotificationComponentProps) {
|
||||
return (
|
||||
<div>
|
||||
<span>{props.data.title}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NotificationPopupComponent(props: NotificationComponentProps) {
|
||||
return (
|
||||
<div class="p-2 flex flex-col gap-y-1">
|
||||
<div class="min-h-10 grid place-items-center grid-cols-[24px_1fr_auto] gap-3">
|
||||
{IconFromNotificationType(props.data.notification_type)({
|
||||
class: `w-6 h-6 ${ColorFromNotificationType(props.data.notification_type)}`,
|
||||
})}
|
||||
|
||||
<div class="flex flex-col w-full">
|
||||
<span class={`font-medium ${ColorFromNotificationType(props.data.notification_type)}`}>{props.data.title}</span>
|
||||
<span class="text-sm text-white/60">{props.data.message}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row justify-end items-center gap-1">
|
||||
<span class="text-sm text-white/40">
|
||||
<TimeAgo timestamp={props.data.created_at * 1000} />
|
||||
</span>
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-brand" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={props.data.progress}>
|
||||
<div class="rounded-full overflow-hidden h-1.5 bg-brand-disabled w-full">
|
||||
<div
|
||||
class="rounded-full h-full min-w-0 max-w-full bg-brand transition-[width]"
|
||||
style={{
|
||||
width: `${Math.floor(props.data.progress! * 100)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NotificationComponent(props: NotificationComponentProps) {
|
||||
return (
|
||||
<>
|
||||
{(props.overlay ? NotificationOverlayComponent : NotificationPopupComponent)(props)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default NotificationComponent;
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import { For, Match, Switch, createSignal, onMount } from 'solid-js';
|
||||
import { Settings01Icon, Trash01Icon } from '@untitled-theme/icons-solid';
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-solid';
|
||||
import Popup from '../Popup';
|
||||
import Button from '../../base/Button';
|
||||
import * as manager from '../../../../bridge/notifications';
|
||||
import NotificationComponent from './NotificationComponent';
|
||||
|
||||
function NotificationPopup(props: Popup.PopupProps) {
|
||||
const [notifications, setNotifications] = createSignal<Core.Notification[]>([]);
|
||||
|
||||
let inner!: HTMLDivElement;
|
||||
let parent!: HTMLDivElement;
|
||||
|
||||
function updateSize() {
|
||||
if (inner && parent) {
|
||||
const rect = inner.getBoundingClientRect();
|
||||
parent.style.height = `${rect.height}px`;
|
||||
}
|
||||
}
|
||||
|
||||
async function refetchNotifications() {
|
||||
const notis = await manager.getNotifications();
|
||||
setNotifications(notis);
|
||||
updateSize();
|
||||
console.log(notifications());
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
document.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'n') {
|
||||
manager.addNotification({
|
||||
title: 'Test Notification',
|
||||
message: 'This is a test notification',
|
||||
notification_type: manager.NotificationType.Download,
|
||||
progress: 0.39,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
refetchNotifications();
|
||||
manager.on('added', refetchNotifications);
|
||||
manager.on('removed', refetchNotifications);
|
||||
manager.on('cleared', refetchNotifications);
|
||||
});
|
||||
|
||||
return (
|
||||
<Popup {...props}>
|
||||
<div class="bg-secondary rounded-xl border border-gray-0.10 w-96 p-2 shadow-lg shadow-black/50">
|
||||
<div class="overflow-hidden transition-[height]" ref={parent}>
|
||||
<div class="flex flex-col justify-start items-stretch text-start gap-2" ref={inner}>
|
||||
<p class="text-2lg px-2 pt-1">Notifications</p>
|
||||
|
||||
<Switch>
|
||||
<Match when={notifications().length !== 0}>
|
||||
<OverlayScrollbarsComponent class="max-h-[min(500px,60vh)] overflow-auto">
|
||||
<div class="flex flex-col-reverse justify-center items-stretch ">
|
||||
<For each={notifications()}>
|
||||
{noti => (
|
||||
<div class="flex flex-col w-full">
|
||||
<NotificationComponent data={noti} overlay={false} />
|
||||
<span class="bg-gray-0.05 h-px w-full" />
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</OverlayScrollbarsComponent>
|
||||
</Match>
|
||||
<Match when={notifications().length === 0}>
|
||||
<span class="px-2">You have no notifications</span>
|
||||
<span class="bg-gray-0.05 h-px" />
|
||||
</Match>
|
||||
</Switch>
|
||||
|
||||
<div class="flex flex-row justify-between items-end">
|
||||
<Button onClick={() => manager.clearNotifications()} buttonStyle="ghost" iconLeft={<Trash01Icon />}>
|
||||
Clear Notifications
|
||||
</Button>
|
||||
|
||||
<Button buttonStyle="icon" large>
|
||||
<Settings01Icon />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
);
|
||||
}
|
||||
|
||||
export default NotificationPopup;
|
||||
|
|
@ -47,7 +47,7 @@ function Banner() {
|
|||
<div class="flex w-full flex-row justify-between items-end">
|
||||
<div class="flex flex-row items-center gap-x-4">
|
||||
<Button iconLeft={<PlayIcon />}>Launch 1.19.2</Button>
|
||||
<Button styleType="icon" class="w-8 h-8">
|
||||
<Button buttonStyle="icon" class="w-8 h-8">
|
||||
<DotsVerticalIcon />
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -159,7 +159,7 @@ function ClusterCard(props: Core.ClusterWithManifest) {
|
|||
{props.instance.mods && `• ${props.mods} mods`} */}
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={e => openContextMenu(e)} styleType="icon" class="w-8 h-8">
|
||||
<Button onClick={e => openContextMenu(e)} buttonStyle="icon" class="w-8 h-8">
|
||||
<DotsVerticalIcon />
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -218,7 +218,7 @@ function HomePage() {
|
|||
</div>
|
||||
<div class="flex flex-row gap-x-4">
|
||||
<Button
|
||||
styleType="primary"
|
||||
buttonStyle="primary"
|
||||
iconLeft={<PlusIcon class="!w-5 stroke-[2.2]" />}
|
||||
children="New Cluster"
|
||||
onClick={() => newInstance()}
|
||||
|
|
|
|||
|
|
@ -30,9 +30,12 @@ export default defineConfig({
|
|||
'sm': '0.75rem',
|
||||
'md': '0.875rem',
|
||||
'lg': '1rem',
|
||||
'2lg': '1.125rem',
|
||||
'xl': '1.25rem',
|
||||
'xxl': '1.5rem',
|
||||
'2xl': '2rem',
|
||||
'3xl': '2.5rem',
|
||||
'4xl': '3rem',
|
||||
},
|
||||
|
||||
colors: {
|
||||
|
|
|
|||
106
pnpm-lock.yaml
generated
106
pnpm-lock.yaml
generated
|
|
@ -35,6 +35,9 @@ importers:
|
|||
'@iconify-json/logos':
|
||||
specifier: ^1.1.42
|
||||
version: 1.1.42
|
||||
'@solid-primitives/refs':
|
||||
specifier: ^1.0.8
|
||||
version: 1.0.8(solid-js@1.8.17)
|
||||
'@solidjs/router':
|
||||
specifier: ^0.13.3
|
||||
version: 0.13.3(solid-js@1.8.17)
|
||||
|
|
@ -90,6 +93,9 @@ importers:
|
|||
sass:
|
||||
specifier: ^1.77.0
|
||||
version: 1.77.0
|
||||
ts-node:
|
||||
specifier: ^10.9.2
|
||||
version: 10.9.2(@types/node@20.12.11)(typescript@5.4.5)
|
||||
typescript:
|
||||
specifier: ^5.4.5
|
||||
version: 5.4.5
|
||||
|
|
@ -330,6 +336,10 @@ packages:
|
|||
resolution: {integrity: sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@cspotcode/source-map-support@0.8.1':
|
||||
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@es-joy/jsdoccomment@0.42.0':
|
||||
resolution: {integrity: sha512-R1w57YlVA6+YE01wch3GPYn6bCsrOV3YW/5oGGE2tmX6JcL9Nr+b5IikrjMPF+v9CV3ay+obImEdsDhovhJrzw==}
|
||||
engines: {node: '>=16'}
|
||||
|
|
@ -578,6 +588,9 @@ packages:
|
|||
'@jridgewell/trace-mapping@0.3.25':
|
||||
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.9':
|
||||
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
||||
|
||||
'@jsdevtools/ez-spawn@3.0.4':
|
||||
resolution: {integrity: sha512-f5DRIOZf7wxogefH03RjMPMdBF7ADTWUMoOs9kaJo06EfwF+aFhMZMDZxHg/Xe12hptN9xoZjGso2fdjapBRIA==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -851,6 +864,18 @@ packages:
|
|||
peerDependencies:
|
||||
solid-js: ^1.5
|
||||
|
||||
'@tsconfig/node10@1.0.11':
|
||||
resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==}
|
||||
|
||||
'@tsconfig/node12@1.0.11':
|
||||
resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==}
|
||||
|
||||
'@tsconfig/node14@1.0.3':
|
||||
resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==}
|
||||
|
||||
'@tsconfig/node16@1.0.4':
|
||||
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
|
||||
|
||||
'@types/aria-query@5.0.4':
|
||||
resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
|
||||
|
||||
|
|
@ -1178,6 +1203,9 @@ packages:
|
|||
resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
arg@4.1.3:
|
||||
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
|
||||
|
||||
argparse@2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
|
||||
|
|
@ -1356,6 +1384,9 @@ packages:
|
|||
core-js-compat@3.37.0:
|
||||
resolution: {integrity: sha512-vYq4L+T8aS5UuFg4UwDhc7YNRWVeVZwltad9C/jV3R2LgVOpS9BDr7l/WL6BN0dbV3k1XejPTHqqEzJgsa0frA==}
|
||||
|
||||
create-require@1.1.1:
|
||||
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
|
||||
|
||||
cross-spawn@7.0.3:
|
||||
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
|
||||
engines: {node: '>= 8'}
|
||||
|
|
@ -1440,6 +1471,10 @@ packages:
|
|||
resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
||||
diff@4.0.2:
|
||||
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
|
||||
engines: {node: '>=0.3.1'}
|
||||
|
||||
dir-glob@3.0.1:
|
||||
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -2190,6 +2225,9 @@ packages:
|
|||
magic-string@0.30.10:
|
||||
resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==}
|
||||
|
||||
make-error@1.3.6:
|
||||
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
|
||||
|
||||
mdast-util-from-markdown@0.8.5:
|
||||
resolution: {integrity: sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==}
|
||||
|
||||
|
|
@ -2805,6 +2843,20 @@ packages:
|
|||
peerDependencies:
|
||||
typescript: '>=4.2.0'
|
||||
|
||||
ts-node@10.9.2:
|
||||
resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@swc/core': '>=1.2.50'
|
||||
'@swc/wasm': '>=1.2.50'
|
||||
'@types/node': '*'
|
||||
typescript: '>=2.7'
|
||||
peerDependenciesMeta:
|
||||
'@swc/core':
|
||||
optional: true
|
||||
'@swc/wasm':
|
||||
optional: true
|
||||
|
||||
tsconfck@3.0.3:
|
||||
resolution: {integrity: sha512-4t0noZX9t6GcPTfBAbIbbIU4pfpCwh0ueq3S4O/5qXI1VwK1outmxhe9dOiEWqMz3MW2LKgDTpqWV+37IWuVbA==}
|
||||
engines: {node: ^18 || >=20}
|
||||
|
|
@ -2892,6 +2944,9 @@ packages:
|
|||
uuid@9.0.1:
|
||||
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
||||
|
||||
v8-compile-cache-lib@3.0.1:
|
||||
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
|
||||
|
||||
validate-html-nesting@1.2.2:
|
||||
resolution: {integrity: sha512-hGdgQozCsQJMyfK5urgFcWEqsSSrK63Awe0t/IMR0bZ0QMtnuaiHzThW81guu3qx9abLi99NEuiaN6P9gVYsNg==}
|
||||
|
||||
|
|
@ -3073,6 +3128,10 @@ packages:
|
|||
engines: {node: '>= 14'}
|
||||
hasBin: true
|
||||
|
||||
yn@3.1.1:
|
||||
resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
yocto-queue@0.1.0:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -3393,6 +3452,10 @@ snapshots:
|
|||
'@babel/helper-validator-identifier': 7.24.5
|
||||
to-fast-properties: 2.0.0
|
||||
|
||||
'@cspotcode/source-map-support@0.8.1':
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.9
|
||||
|
||||
'@es-joy/jsdoccomment@0.42.0':
|
||||
dependencies:
|
||||
comment-parser: 1.4.1
|
||||
|
|
@ -3587,6 +3650,11 @@ snapshots:
|
|||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.4.15
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.9':
|
||||
dependencies:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.4.15
|
||||
|
||||
'@jsdevtools/ez-spawn@3.0.4':
|
||||
dependencies:
|
||||
call-me-maybe: 1.0.2
|
||||
|
|
@ -3815,6 +3883,14 @@ snapshots:
|
|||
dependencies:
|
||||
solid-js: 1.8.17
|
||||
|
||||
'@tsconfig/node10@1.0.11': {}
|
||||
|
||||
'@tsconfig/node12@1.0.11': {}
|
||||
|
||||
'@tsconfig/node14@1.0.3': {}
|
||||
|
||||
'@tsconfig/node16@1.0.4': {}
|
||||
|
||||
'@types/aria-query@5.0.4': {}
|
||||
|
||||
'@types/babel__core@7.20.5':
|
||||
|
|
@ -4302,6 +4378,8 @@ snapshots:
|
|||
|
||||
are-docs-informative@0.0.2: {}
|
||||
|
||||
arg@4.1.3: {}
|
||||
|
||||
argparse@2.0.1: {}
|
||||
|
||||
aria-query@5.1.3:
|
||||
|
|
@ -4503,6 +4581,8 @@ snapshots:
|
|||
dependencies:
|
||||
browserslist: 4.23.0
|
||||
|
||||
create-require@1.1.1: {}
|
||||
|
||||
cross-spawn@7.0.3:
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
|
|
@ -4588,6 +4668,8 @@ snapshots:
|
|||
|
||||
diff-sequences@29.6.3: {}
|
||||
|
||||
diff@4.0.2: {}
|
||||
|
||||
dir-glob@3.0.1:
|
||||
dependencies:
|
||||
path-type: 4.0.0
|
||||
|
|
@ -5447,6 +5529,8 @@ snapshots:
|
|||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.4.15
|
||||
|
||||
make-error@1.3.6: {}
|
||||
|
||||
mdast-util-from-markdown@0.8.5:
|
||||
dependencies:
|
||||
'@types/mdast': 3.0.15
|
||||
|
|
@ -6058,6 +6142,24 @@ snapshots:
|
|||
dependencies:
|
||||
typescript: 5.4.5
|
||||
|
||||
ts-node@10.9.2(@types/node@20.12.11)(typescript@5.4.5):
|
||||
dependencies:
|
||||
'@cspotcode/source-map-support': 0.8.1
|
||||
'@tsconfig/node10': 1.0.11
|
||||
'@tsconfig/node12': 1.0.11
|
||||
'@tsconfig/node14': 1.0.3
|
||||
'@tsconfig/node16': 1.0.4
|
||||
'@types/node': 20.12.11
|
||||
acorn: 8.11.3
|
||||
acorn-walk: 8.3.2
|
||||
arg: 4.1.3
|
||||
create-require: 1.1.1
|
||||
diff: 4.0.2
|
||||
make-error: 1.3.6
|
||||
typescript: 5.4.5
|
||||
v8-compile-cache-lib: 3.0.1
|
||||
yn: 3.1.1
|
||||
|
||||
tsconfck@3.0.3(typescript@5.4.5):
|
||||
optionalDependencies:
|
||||
typescript: 5.4.5
|
||||
|
|
@ -6144,6 +6246,8 @@ snapshots:
|
|||
|
||||
uuid@9.0.1: {}
|
||||
|
||||
v8-compile-cache-lib@3.0.1: {}
|
||||
|
||||
validate-html-nesting@1.2.2: {}
|
||||
|
||||
validate-npm-package-license@3.0.4:
|
||||
|
|
@ -6329,6 +6433,8 @@ snapshots:
|
|||
|
||||
yaml@2.4.2: {}
|
||||
|
||||
yn@3.1.1: {}
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
||||
yocto-queue@1.0.0: {}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue