feat: styling stuff

This commit is contained in:
emirsassan 2025-07-15 21:03:22 +03:00
parent a02ca5c085
commit 54eb4fd645
8 changed files with 228 additions and 102 deletions

View file

@ -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;

View file

@ -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>
);
}

View file

@ -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>

View file

@ -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);

View 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: [],
};

View file

@ -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
}

View file

@ -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>

View file

@ -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;
}