feat: styling stuff
This commit is contained in:
parent
a02ca5c085
commit
54eb4fd645
8 changed files with 228 additions and 102 deletions
|
|
@ -0,0 +1,38 @@
|
|||
import type { ImgHTMLAttributes, RefAttributes } from 'react';
|
||||
import CurseforgeIcon from '@/assets/logos/curseforge.svg';
|
||||
import VanillaImage from '@/assets/logos/minecraft.png';
|
||||
import ModrinthIcon from '@/assets/logos/modrinth.svg';
|
||||
import SkyClientImage from '@/assets/logos/skyclient.png';
|
||||
|
||||
export function getProviderLogoSrc(provider: string): string {
|
||||
const providerName = provider.toLowerCase();
|
||||
|
||||
const mapping: Record<string, string> = {
|
||||
modrinth: ModrinthIcon,
|
||||
curseforge: CurseforgeIcon,
|
||||
skyclient: SkyClientImage,
|
||||
};
|
||||
|
||||
return mapping[providerName];
|
||||
}
|
||||
|
||||
type ProviderIconProp = ImgHTMLAttributes<HTMLImageElement> & RefAttributes<HTMLImageElement> & {
|
||||
provider: string | undefined;
|
||||
};
|
||||
|
||||
function ProviderIcon({
|
||||
provider,
|
||||
src: _src,
|
||||
alt: _alt,
|
||||
...rest
|
||||
}: ProviderIconProp) {
|
||||
return (
|
||||
<img
|
||||
{...rest}
|
||||
alt={`${provider}'s logo`}
|
||||
src={provider ? getProviderLogoSrc(provider) : VanillaImage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProviderIcon;
|
||||
|
|
@ -1,58 +1,11 @@
|
|||
import type { NotificationData } from '@/hooks/useNotification';
|
||||
import useNotifications from '@/hooks/useNotification';
|
||||
import { Button, Popup } from '@onelauncher/common/components';
|
||||
import { XIcon } from '@untitled-theme/icons-react';
|
||||
import { NotificationToast } from '../NotificationToasts';
|
||||
|
||||
interface NotificationItemProps {
|
||||
id: string;
|
||||
data: NotificationData;
|
||||
onRemove: (id: string) => void;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars -- deprecated
|
||||
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>
|
||||
);
|
||||
}
|
||||
import { Button } from '@onelauncher/common/components';
|
||||
import { InfoCircleIcon, Trash01Icon } from '@untitled-theme/icons-react';
|
||||
|
||||
function NotificationPopup() {
|
||||
const { list, clear, remove } = useNotifications();
|
||||
const { list, clear } = useNotifications();
|
||||
const notificationEntries = Object.entries(list);
|
||||
|
||||
const removeNotification = (id: string) => {
|
||||
remove(id);
|
||||
};
|
||||
|
||||
if (notificationEntries.length === 0)
|
||||
return (
|
||||
<div className="p-3 w-80 text-center">
|
||||
|
|
@ -62,27 +15,57 @@ function NotificationPopup() {
|
|||
|
||||
return (
|
||||
<div className="min-w-80 w-80 max-w-96 max-h-96 overflow-y-auto">
|
||||
<div className="flex items-center justify-between border-b border-component-border">
|
||||
<h3 className="font-semibold text-fg-primary">Notifications</h3>
|
||||
<div className="flex flex-col bg-page-elevated">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h4 className="text-fg-primary ml-2">Notifications</h4>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 mb-3">
|
||||
{notificationEntries.map(([id, data]) => (
|
||||
<div className="flex items-start gap-3 p-2 rounded-lg" key={id}>
|
||||
<div className="flex-shrink-0 self-center">
|
||||
<InfoCircleIcon className="h-6 w-6 text-fg-primary" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-semibold text-fg-primary text-sm">
|
||||
{data.title}
|
||||
</h4>
|
||||
<p className="text-fg-secondary text-xs mt-1">
|
||||
{data.message}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex-shrink-0 mt-2 self-center">
|
||||
<div className="h-2 w-2 rounded-full bg-blue-500" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<hr className="mb-2 text-component-border-pressed" />
|
||||
|
||||
{notificationEntries.length > 0 && (
|
||||
<Button
|
||||
className="text-xs"
|
||||
color="ghost"
|
||||
onClick={clear}
|
||||
size="normal"
|
||||
>
|
||||
Clear All
|
||||
</Button>
|
||||
<div className="flex items-center justify-between">
|
||||
<Button
|
||||
className="text-left"
|
||||
color="ghost"
|
||||
onClick={clear}
|
||||
size="normal"
|
||||
>
|
||||
<Trash01Icon className="h-4 w-4" />
|
||||
Clear Notifications
|
||||
</Button>
|
||||
{/* <Button
|
||||
className="h-6 w-6 p-0"
|
||||
color="ghost"
|
||||
size="iconLarge"
|
||||
>
|
||||
<Settings01Icon />
|
||||
</Button> */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{notificationEntries.map(([id, data]) => (
|
||||
<div key={id}>
|
||||
<div className="p-2">
|
||||
<NotificationToast key={id} notification={{ ...data, timestamp: Date.now(), id }} onRemove={removeNotification} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import ProviderIcon from '@/components/content/ProviderIcon';
|
||||
import { PROVIDERS } from '@/utils';
|
||||
import { browserCategories } from '@/utils/browser';
|
||||
import { Button, Dropdown, Show, TextField } from '@onelauncher/common/components';
|
||||
import { createFileRoute, Outlet } from '@tanstack/react-router';
|
||||
import { SearchMdIcon } from '@untitled-theme/icons-react';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const Route = createFileRoute('/app/browser')({
|
||||
component: RouteComponent,
|
||||
|
|
@ -56,16 +59,13 @@ function BrowserSidebar() {
|
|||
<div className="flex flex-col gap-y-1">
|
||||
<h6 className="my-1">Provider</h6>
|
||||
<Dropdown>
|
||||
{/* <For each={PROVIDERS}>
|
||||
{provider => (
|
||||
<Dropdown.Row>
|
||||
<ProviderIcon class="h-4 w-4" provider={provider} />
|
||||
{provider}
|
||||
</Dropdown.Row>
|
||||
)}
|
||||
</For> */}
|
||||
{PROVIDERS.map(provider => (
|
||||
<Dropdown.Item key={provider}>{provider}</Dropdown.Item>
|
||||
<Dropdown.Item key={provider}>
|
||||
<div className="flex flex-row">
|
||||
<ProviderIcon className="size-4 mr-2 self-center" provider={provider} />
|
||||
{provider}
|
||||
</div>
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
|
@ -75,6 +75,8 @@ function BrowserSidebar() {
|
|||
}
|
||||
|
||||
function BrowserCategories() {
|
||||
const categories = browserCategories.byPackageType('mod', 'Modrinth');
|
||||
|
||||
return (
|
||||
<div className="top-0 grid grid-cols-[1fr_auto] h-fit min-w-50 gap-y-6">
|
||||
<div />
|
||||
|
|
@ -82,21 +84,14 @@ function BrowserCategories() {
|
|||
<Show when>
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<h6 className="my-1">Categories</h6>
|
||||
{/* <For each={categories()}>
|
||||
{category => (
|
||||
<p
|
||||
class={`text-md capitalize text-fg-primary hover:text-fg-primary-hover line-height-snug ${isEnabled(category.id) ? 'text-opacity-100! hover:text-opacity-90!' : 'text-opacity-60! hover:text-opacity-70!'}`}
|
||||
onClick={() => toggleCategory(category.id)}
|
||||
>
|
||||
{category.display}
|
||||
</p>
|
||||
)}
|
||||
</For> */}
|
||||
<p
|
||||
className="text-md capitalize opacity-100 hover:opacity-90 text-fg-primary hover:text-fg-primary-hover"
|
||||
>
|
||||
Jennie
|
||||
</p>
|
||||
{categories.map(category => (
|
||||
<p
|
||||
className="text-md capitalize opacity-100 hover:opacity-90 text-fg-primary hover:text-fg-primary-hover"
|
||||
key={category.id}
|
||||
>
|
||||
{category.display}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { NewClusterCreate } from '@/components/launcher/cluster/ClusterCreation'
|
|||
import useRecentCluster from '@/hooks/useCluster';
|
||||
import { bindings } from '@/main';
|
||||
import { formatAsDuration, upperFirst } from '@/utils';
|
||||
import { useCommand } from '@onelauncher/common';
|
||||
import { useCommand, useCommandMut, useCommandSuspense } from '@onelauncher/common';
|
||||
import { Button, Show } from '@onelauncher/common/components';
|
||||
import { createFileRoute, Link } from '@tanstack/react-router';
|
||||
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||
|
|
@ -167,13 +167,10 @@ function ClusterCard({
|
|||
icon_url,
|
||||
stage,
|
||||
}: ClusterModel) {
|
||||
const launch = useCommand('launchCluster', () => bindings.core.launchCluster(id, null), {
|
||||
enabled: false,
|
||||
subscribed: false,
|
||||
});
|
||||
const launch = useCommandMut(() => bindings.core.launchCluster(id, null));
|
||||
|
||||
const handleLaunch = () => {
|
||||
launch.refetch();
|
||||
launch.mutate();
|
||||
|
||||
if (launch.error)
|
||||
console.error(launch.error.message);
|
||||
|
|
|
|||
95
apps/onelauncher/frontend/src/utils/browser.ts
Normal file
95
apps/onelauncher/frontend/src/utils/browser.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
export const BROWSER_VIEWS: Array<string> = ['grid', 'list'] as const;
|
||||
|
||||
export interface CategoryItem {
|
||||
display: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export const browserCategories = {
|
||||
byPackageType(packageType: 'mod' | string, provider: string): Array<CategoryItem> {
|
||||
switch (packageType) {
|
||||
case 'mod':
|
||||
return this.mod(provider);
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
|
||||
mod(provider: string = 'Modrinth'): Array<CategoryItem> {
|
||||
return Object.values(modMapping[provider]);
|
||||
},
|
||||
};
|
||||
|
||||
const modMapping: Record<string, Array<CategoryItem>> = {
|
||||
Curseforge: [
|
||||
{ display: 'Adventure', id: '422' },
|
||||
{ display: 'API and Library', id: '421' },
|
||||
{ display: 'Armor and Tools', id: '434' },
|
||||
{ display: 'Biomes', id: '407' },
|
||||
{ display: 'Bug Fixes', id: '6821' },
|
||||
{ display: 'Cosmetic', id: '424' },
|
||||
{ display: 'Dimensions', id: '410' },
|
||||
{ display: 'Education', id: '5299' },
|
||||
{ display: 'Energy', id: '417' },
|
||||
{ display: 'Farming', id: '416' },
|
||||
{ display: 'Food', id: '436' },
|
||||
{ display: 'Forestry', id: '433' },
|
||||
{ display: 'Magic', id: '419' },
|
||||
{ display: 'Miscellaneous', id: '425' },
|
||||
{ display: 'Mobs', id: '411' },
|
||||
{ display: 'Ores and Resources', id: '408' },
|
||||
{ display: 'Performance', id: '6814' },
|
||||
{ display: 'Player Transport', id: '414' },
|
||||
{ display: 'Redstone', id: '4558' },
|
||||
{ display: 'Storage', id: '420' },
|
||||
{ display: 'Structures', id: '409' },
|
||||
{ display: 'Technology', id: '412' },
|
||||
{ display: 'Utility & QoL', id: '5191' },
|
||||
{ display: 'World Generation', id: '406' },
|
||||
|
||||
// { display: 'Map and Information', id: '423' },
|
||||
// { display: 'Server Utility', id: '435' },
|
||||
// { display: 'Twitch Integration', id: '4671' },
|
||||
// { display: 'Create', id: '6484' },
|
||||
// { display: 'Buildcraft', id: '432' },
|
||||
// { display: 'Industrial Craft', id: '429' },
|
||||
// { display: 'KubeJS', id: '5314' },
|
||||
// { display: 'Genetics', id: '418' },
|
||||
// { display: 'Blood Magic', id: '4485' },
|
||||
// { display: 'Automation', id: '4843' },
|
||||
// { display: 'CraftTweaker', id: '4773' },
|
||||
// { display: 'Addons', id: '426' },
|
||||
// { display: 'Energy, Fluid, and Item Transport', id: '415' },
|
||||
// { display: 'Thermal Expansion', id: '427' },
|
||||
// { display: 'Thaumcraft', id: '430' },
|
||||
// { display: 'Processing', id: '413' },
|
||||
// { display: 'Tinker\'s Construct', id: '428' },
|
||||
// { display: 'Skyblock', id: '6145' },
|
||||
// { display: 'Galacticraft', id: '5232' },
|
||||
// { display: 'Applied Energistics 2', id: '4545' },
|
||||
// { display: 'Integrated Dynamics', id: '6954' },
|
||||
// { display: 'MCreator', id: '4906' },
|
||||
],
|
||||
Modrinth: [
|
||||
{ display: 'Adventure', id: 'adventure' },
|
||||
{ display: 'Cursed', id: 'cursed' },
|
||||
{ display: 'Decoration', id: 'decoration' },
|
||||
{ display: 'Economy', id: 'economy' },
|
||||
{ display: 'Equipment', id: 'equipment' },
|
||||
{ display: 'Food', id: 'food' },
|
||||
{ display: 'Game Mechanics', id: 'game-mechanics' },
|
||||
{ display: 'Library', id: 'library' },
|
||||
{ display: 'Magic', id: 'magic' },
|
||||
{ display: 'Management', id: 'management' },
|
||||
{ display: 'Minigame', id: 'minigame' },
|
||||
{ display: 'Mobs', id: 'mobs' },
|
||||
{ display: 'Optimization', id: 'optimization' },
|
||||
{ display: 'Social', id: 'social' },
|
||||
{ display: 'Storage', id: 'storage' },
|
||||
{ display: 'Technology', id: 'technology' },
|
||||
{ display: 'Transportation', id: 'transportation' },
|
||||
{ display: 'Utility', id: 'utility' },
|
||||
{ display: 'World Generation', id: 'worldgen' },
|
||||
],
|
||||
SkyClient: [],
|
||||
};
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
use interpulse::api::minecraft::Version;
|
||||
use onelauncher_entity::{clusters, icon::Icon, loader::GameLoader, prelude::entity, resolution::Resolution};
|
||||
use onelauncher_entity::{clusters, icon::Icon, loader::GameLoader, package::Provider, prelude::entity, resolution::Resolution};
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use serde::Serialize;
|
||||
use crate::store::{Settings, State};
|
||||
use crate::{api::packages::data::{ManagedPackage, ManagedUser, ManagedVersion, SearchQuery}, store::{Settings, State}, utils::pagination::Paginated};
|
||||
use tauri::{AppHandle, Runtime};
|
||||
|
||||
use crate::{api::{self, cluster::dao::ClusterId}, error::{LauncherError, LauncherResult}, store::{credentials::MinecraftCredentials, Core}};
|
||||
|
|
@ -81,6 +81,22 @@ pub trait TauriLauncherApi {
|
|||
|
||||
#[taurpc(alias = "writeSettings")]
|
||||
async fn write_settings(setting: Settings) -> LauncherResult<()>;
|
||||
|
||||
// Packages
|
||||
#[taurpc(alias = "searchPackages")]
|
||||
async fn search_packages(provider: Provider, query: SearchQuery) -> LauncherResult<Paginated<ManagedPackage>>;
|
||||
|
||||
#[taurpc(alias = "getPackage")]
|
||||
async fn get_package(provider: Provider, slug: String) -> LauncherResult<ManagedPackage>;
|
||||
|
||||
#[taurpc(alias = "getMultiplePackages")]
|
||||
async fn get_multiple_packages(provider: Provider, slugs: Vec<String>) -> LauncherResult<Vec<ManagedPackage>>;
|
||||
|
||||
#[taurpc(alias = "getPackageVersions")]
|
||||
async fn get_package_versions(provider: Provider, slug: String, mc_versions: Option<Vec<String>>, loaders: Option<Vec<GameLoader>>, offset: usize, limit: usize) -> LauncherResult<Paginated<ManagedVersion>>;
|
||||
|
||||
#[taurpc(alias = "getPackageUser")]
|
||||
async fn get_package_user(provider: Provider, slug: String) -> LauncherResult<ManagedUser>;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -369,4 +385,6 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Packages
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ interface MyPopoverProps extends Omit<PopoverProps, 'children'> {
|
|||
|
||||
export function Popup({ children, ...props }: MyPopoverProps) {
|
||||
return (
|
||||
<Popover {...props} className="flex flex-col gap-0.5 rounded-lg border border-component-border">
|
||||
<Popover {...props} className="flex flex-col gap-0.5 rounded-lg border border-component-border-hover">
|
||||
<Dialog className="bg-page-elevated p-2 shadow-md rounded-lg">
|
||||
{children}
|
||||
</Dialog>
|
||||
|
|
|
|||
|
|
@ -118,5 +118,5 @@ h5 { @apply text-[0.875rem] text-fg-secondary font-bold uppercase; }
|
|||
h6 { @apply text-[0.75rem] text-fg-secondary font-medium uppercase; }
|
||||
|
||||
code {
|
||||
@apply rounded-md bg-component-bg p-2 text-left;
|
||||
@apply rounded-md p-2 text-left;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue