From bff5664ea2a834144b257607f72f18edb9860a38 Mon Sep 17 00:00:00 2001 From: Jacob Date: Sat, 27 Sep 2025 17:53:12 +0800 Subject: [PATCH 01/13] feat: oneclient account skin manager --- apps/oneclient/frontend/src/bindings.gen.ts | 29 +- .../frontend/src/components/SkinViewer.tsx | 70 +++- .../overlay/RemoveSkinCapeModal.tsx | 22 ++ .../frontend/src/components/overlay/index.ts | 1 + .../frontend/src/routes/app/accountSkin.tsx | 298 ++++++++++++++++++ .../frontend/src/routes/app/accounts.tsx | 15 +- 6 files changed, 421 insertions(+), 14 deletions(-) create mode 100644 apps/oneclient/frontend/src/components/overlay/RemoveSkinCapeModal.tsx create mode 100644 apps/oneclient/frontend/src/routes/app/accountSkin.tsx diff --git a/apps/oneclient/frontend/src/bindings.gen.ts b/apps/oneclient/frontend/src/bindings.gen.ts index 0faf9f6d..5dd7ae1e 100644 --- a/apps/oneclient/frontend/src/bindings.gen.ts +++ b/apps/oneclient/frontend/src/bindings.gen.ts @@ -106,8 +106,14 @@ export type ModpackFormat = "CurseForge" | "MrPack" | "PolyMrPack" export type ModpackManifest = { name: string; version: string; loader: GameLoader; loader_version: string; mc_version: string; files: ModpackFile[] } +export type MojangCape = { id: string; state: string; url: string; alias: string } + +export type MojangFullPlayerProfile = { uuid: string; username: string; skins: MojangSkin[]; capes: MojangCape[] } + export type MojangPlayerProfile = { uuid: string; username: string; is_slim: boolean; skin_url: string | null; cape_url: string | null } +export type MojangSkin = { id: string; state: string; url: string; variant: SkinVariant } + export type PackageAuthor = { Team: { team_id: string; org_id: string | null } } | { Users: ManagedUser[] } export type PackageCategories = { Mod: PackageModCategory[] } | { ResourcePack: PackageResourcePackCategory[] } | { Shader: PackageShaderCategory[] } | { DataPack: PackageModCategory[] } | { ModPack: PackageModPackCategory[] } @@ -181,7 +187,9 @@ export type SettingProfileModel = { name: string; java_id: number | null; res: R export type Settings = { global_game_settings: SettingProfileModel; allow_parallel_running_clusters: boolean; enable_gamemode: boolean; discord_enabled: boolean; max_concurrent_requests: number; settings_version: number; native_window_frame: boolean } -export type SettingsOsExtra = { enable_gamemode: boolean | null } +export type SettingsOsExtra = Record + +export type SkinVariant = "classic" | "slim" export type Sort = "Relevance" | "Downloads" | "Newest" | "Updated" @@ -244,15 +252,13 @@ export type VersionType = */ "old_beta" -const ARGS_MAP = { 'oneclient':'{"getBundlesFor":["cluster_id"],"openDevTools":[],"getClustersGroupedByMajor":[]}', 'events':'{"ingress":["event"],"message":["event"],"process":["event"]}', 'core':'{"openMsaLogin":[],"getRunningProcesses":[],"launchCluster":["id","uuid"],"getWorlds":["id"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"searchPackages":["provider","query"],"getPackage":["provider","slug"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getLoadersForVersion":["mc_version"],"removeCluster":["id"],"getUser":["uuid"],"setDefaultUser":["uuid"],"getUsersFromAuthor":["provider","author"],"getMultiplePackages":["provider","slugs"],"getLogs":["id"],"getProfileOrDefault":["name"],"fetchMinecraftProfile":["uuid"],"updateClusterProfile":["name","profile"],"getUsers":[],"killProcess":["pid"],"createCluster":["options"],"removeUser":["uuid"],"getLogByName":["id","name"],"getGlobalProfile":[],"getGameVersions":[],"getClusterById":["id"],"getScreenshots":["id"],"writeSettings":["setting"],"getClusters":[],"installModpack":["modpack","cluster_id"],"getDefaultUser":["fallback"],"open":["input"],"getRunningProcessesByClusterId":["cluster_id"],"isClusterRunning":["cluster_id"],"updateClusterById":["id","request"],"readSettings":[],"getPackageBody":["provider","body"]}', 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}' } -export type Router = { 'events': { ingress: (event: IngressPayload) => Promise, -message: (event: MessagePayload) => Promise, -process: (event: ProcessPayload) => Promise }, -'folders': { fromCluster: (folderName: string) => Promise, -openCluster: (folderName: string) => Promise }, -'oneclient': { openDevTools: () => Promise, +const ARGS_MAP = { 'core':'{"getMultiplePackages":["provider","slugs"],"isClusterRunning":["cluster_id"],"getScreenshots":["id"],"changeSkin":["access_token","skin_url","skin_variant"],"removeCluster":["id"],"searchPackages":["provider","query"],"getRunningProcessesByClusterId":["cluster_id"],"getUsers":[],"getLoadersForVersion":["mc_version"],"writeSettings":["setting"],"getPackage":["provider","slug"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"setDefaultUser":["uuid"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getClusterById":["id"],"getUsersFromAuthor":["provider","author"],"open":["input"],"installModpack":["modpack","cluster_id"],"getUser":["uuid"],"getPackageBody":["provider","body"],"createCluster":["options"],"launchCluster":["id","uuid"],"getProfileOrDefault":["name"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"removeUser":["uuid"],"getGameVersions":[],"getLogByName":["id","name"],"getClusters":[],"openMsaLogin":[],"getLogs":["id"],"updateClusterById":["id","request"],"fetchMinecraftProfile":["uuid"],"killProcess":["pid"],"updateClusterProfile":["name","profile"],"readSettings":[],"getRunningProcesses":[],"fetchLoggedInProfile":["access_token"],"getGlobalProfile":[],"getWorlds":["id"],"getDefaultUser":["fallback"]}', 'events':'{"ingress":["event"],"message":["event"],"process":["event"]}', 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}', 'oneclient':'{"getClustersGroupedByMajor":[],"getBundlesFor":["cluster_id"],"openDevTools":[]}' } +export type Router = { 'oneclient': { openDevTools: () => Promise, getClustersGroupedByMajor: () => Promise>, getBundlesFor: (clusterId: number) => Promise }, +'events': { ingress: (event: IngressPayload) => Promise, +message: (event: MessagePayload) => Promise, +process: (event: ProcessPayload) => Promise }, 'core': { getClusters: () => Promise, getClusterById: (id: number) => Promise, removeCluster: (id: number) => Promise, @@ -289,7 +295,12 @@ downloadPackage: (provider: Provider, packageId: string, versionId: string, clus getUsersFromAuthor: (provider: Provider, author: PackageAuthor) => Promise, installModpack: (modpack: ModpackArchive, clusterId: number) => Promise, fetchMinecraftProfile: (uuid: string) => Promise, -open: (input: string) => Promise } }; +fetchLoggedInProfile: (accessToken: string) => Promise, +uploadSkinBytes: (accessToken: string, skinData: number[], imageFormat: string, skinVariant: SkinVariant) => Promise, +changeSkin: (accessToken: string, skinUrl: string, skinVariant: SkinVariant) => Promise, +open: (input: string) => Promise }, +'folders': { fromCluster: (folderName: string) => Promise, +openCluster: (folderName: string) => Promise } }; export type { InferCommandOutput } diff --git a/apps/oneclient/frontend/src/components/SkinViewer.tsx b/apps/oneclient/frontend/src/components/SkinViewer.tsx index cebc77f7..070d67cf 100644 --- a/apps/oneclient/frontend/src/components/SkinViewer.tsx +++ b/apps/oneclient/frontend/src/components/SkinViewer.tsx @@ -9,14 +9,49 @@ export interface SkinViewerProps { width?: number; height?: number; className?: string | undefined; + autoRotate?: boolean; + autoRotateSpeed?: number; + showText?: boolean; + playerRotateX?: number; + playerRotateY?: number; + playerRotateZ?: number; + translateRotateX?: number; + translateRotateY?: number; + translateRotateZ?: number; + zoom?: number; + animate?: boolean; + animation?: skinviewer.PlayerAnimation; + enableDamping?: boolean; + enableZoom?: boolean; + enableRotate?: boolean; + enablePan?: boolean; } + +const defaultIdleAnimation = new skinviewer.IdleAnimation(); + export function SkinViewer({ skinUrl, capeUrl, width = 260, height = 300, className, + autoRotate = false, + autoRotateSpeed = 0.25, + showText = true, + playerRotateX = 0, + playerRotateY = 0, + playerRotateZ = 0, + translateRotateX = 0, + translateRotateY = 0, + translateRotateZ = 0, + zoom = 0.9, + animate = false, + animation = defaultIdleAnimation, + enableDamping = true, + enableZoom = true, + enableRotate = true, + enablePan = true, }: SkinViewerProps) { const canvasRef = useRef(null); const viewerRef = useRef(null); @@ -29,8 +64,23 @@ export function SkinViewer({ canvas: canvasRef.current, }); - viewer.autoRotate = true; - viewer.autoRotateSpeed = 0.25; + viewer.controls.enableDamping = enableDamping + viewer.controls.enableZoom = enableZoom + viewer.controls.enableRotate = enableRotate + viewer.controls.enablePan = enablePan + + viewer.zoom = zoom; + viewer.playerWrapper.rotateX(playerRotateX); + viewer.playerWrapper.rotateY(playerRotateY); + viewer.playerWrapper.rotateZ(playerRotateZ); + viewer.playerWrapper.translateX(translateRotateX); + viewer.playerWrapper.translateY(translateRotateY); + viewer.playerWrapper.translateZ(translateRotateZ); + + viewer.animation = animation; + + viewer.autoRotate = autoRotate; + viewer.autoRotateSpeed = autoRotateSpeed; viewerRef.current = viewer; @@ -67,6 +117,20 @@ export function SkinViewer({ viewerRef.current.setSize(width, height); }, [width, height]); + useEffect(() => { + if (!viewerRef.current) + return; + + viewerRef.current.animation = animation; + }, [animation]); + + useEffect(() => { + if (!viewerRef.current || !viewerRef.current.animation) + return; + + viewerRef.current.animation.paused = !animate; + }, [animate]); + return (
- Hold to drag. Scroll to zoom in/out. + {showText ? Hold to drag. Scroll to zoom in/out. : <>}
); } diff --git a/apps/oneclient/frontend/src/components/overlay/RemoveSkinCapeModal.tsx b/apps/oneclient/frontend/src/components/overlay/RemoveSkinCapeModal.tsx new file mode 100644 index 00000000..7221c5af --- /dev/null +++ b/apps/oneclient/frontend/src/components/overlay/RemoveSkinCapeModal.tsx @@ -0,0 +1,22 @@ +import { Button } from '@onelauncher/common/components'; +import { Overlay } from './Overlay'; + +export function RemoveSkinCapeModal({ onPress }: { onPress: () => void; }) { + return ( + + Are you sure? + +

This cannot be undone

+ + +
+ ); +} diff --git a/apps/oneclient/frontend/src/components/overlay/index.ts b/apps/oneclient/frontend/src/components/overlay/index.ts index 913737fa..a18ef0e1 100644 --- a/apps/oneclient/frontend/src/components/overlay/index.ts +++ b/apps/oneclient/frontend/src/components/overlay/index.ts @@ -3,4 +3,5 @@ export * from './AddAccountModal'; export * from './Overlay'; export * from './Popup'; export * from './RemoveAccountModal'; +export * from './RemoveSkinCapeModal'; export * from './Toasts'; diff --git a/apps/oneclient/frontend/src/routes/app/accountSkin.tsx b/apps/oneclient/frontend/src/routes/app/accountSkin.tsx new file mode 100644 index 00000000..a048e493 --- /dev/null +++ b/apps/oneclient/frontend/src/routes/app/accountSkin.tsx @@ -0,0 +1,298 @@ +import type { PlayerAnimation } from 'skinview3d'; +import { SheetPage, SkinViewer } from '@/components'; +import { Overlay } from '@/components/overlay/Overlay'; +import { usePlayerProfile } from '@/hooks/usePlayerProfile'; +import { bindings } from '@/main'; +import { useCommandSuspense } from '@onelauncher/common'; +import { Button } from '@onelauncher/common/components'; +import { createFileRoute } from '@tanstack/react-router'; +import { PlusIcon, Trash01Icon } from '@untitled-theme/icons-react'; +import { useEffect, useState } from 'react'; +import { DialogTrigger } from 'react-aria-components'; +import { IdleAnimation, WalkingAnimation } from 'skinview3d'; +import { RemoveSkinCapeModal } from '../../components/overlay'; + +interface Skin { + is_slim?: boolean; + skin_url?: string | null; + cape_url?: string | null; +} + +const skinsData: Array = [ + { + is_slim: false, + skin_url: 'http://textures.minecraft.net/texture/90b8789136facaa9f87b765140e1c8135e6652f513481bd84e6bd8c44844d7ce', + }, + { + is_slim: true, + skin_url: 'https://textures.minecraft.net/texture/69655f89a9fb19a7da292a757be65c52efeab337e3f9579ae090815cfe9cd6d5', + }, + { + is_slim: false, + skin_url: 'http://textures.minecraft.net/texture/c8ccb0647686d04135ac92f4c19b9961b409f8ae3ac5dbea4040e57cda2bcaba', + }, +]; + +const capesData: Array = [ + 'http://textures.minecraft.net/texture/2340c0e03dd24a11b15a8b33c2a7e9e32abb2051b2481d0ba7defd635ca7a933', + 'http://textures.minecraft.net/texture/f9a76537647989f9a0b6d001e320dac591c359e9e61a31f4ce11c88f207f0ad4', + 'http://textures.minecraft.net/texture/28de4a81688ad18b49e735a273e086c18f1e3966956123ccb574034c06f5d336', +]; + +export const Route = createFileRoute('/app/accountSkin')({ + component: RouteComponent, +}); + +const idleAnimation = new IdleAnimation(); +idleAnimation.speed = 0.15; +const walkingAnimation = new WalkingAnimation(); +walkingAnimation.speed = 0.15; +walkingAnimation.headBobbing = false; + +function RouteComponent() { + const [skins, setSkins] = useState>(skinsData); + const [capes, setCapes] = useState>(capesData); + const { data: currentAccount } = useCommandSuspense(['getDefaultUser'], () => bindings.core.getDefaultUser(true)); + const { data: profile } = usePlayerProfile(currentAccount?.id); + const [isAnimated, setAnimated] = useState(false); + const [animation, setAnimation] = useState(idleAnimation); + const skinData: Skin = { + is_slim: profile?.is_slim, + skin_url: profile?.skin_url, + cape_url: profile?.cape_url, + }; + const [selectedSkin, setSelectedSkin] = useState(skinData); + useEffect(() => { + if (!skinData.skin_url) + return; + setSkins((prev) => { + const filtered = prev.filter(skin => skin.skin_url !== skinData.skin_url); + return [skinData, ...filtered]; + }); + setSelectedSkin(skinData); + }, [skinData.skin_url, profile]); + + const [selectedCape, setSelectedCapeSTATE] = useState(profile?.cape_url || null); + const setSelectedCape = (cape: string | null) => { + setSelectedCapeSTATE(cape); + if (selectedSkin.skin_url !== null) + setSelectedSkin({ ...selectedSkin, cape_url: cape }); + }; + useEffect(() => { + if (!skinData.cape_url) + return; + setCapes((prev) => { + const filtered = prev.filter(cape => cape !== skinData.cape_url); + return ['', skinData.cape_url, ...filtered].filter(Boolean) as Array; + }); + setSelectedCape(skinData.cape_url); + + setCapes(['', ...capes]) + }, [skinData.cape_url, profile]); + + return ( + { + setAnimation(isAnimated ? idleAnimation : walkingAnimation); + setAnimated(!isAnimated); + }} username={profile?.username || 'UNKNOWN'} + /> + )} + headerSmall={} + > + +
+
+

Current Skin

+ +
+ +
+ +
+ + + +
+ + + +
+ +
+
+
+ ); +} + +function SkinHistoryRow({ selected, animation, setSelectedSkin, skins, setSkins }: { selected: Skin; animation: PlayerAnimation; setSelectedSkin: (skin: Skin) => void; skins: Array; setSkins: React.Dispatch>> }) { + return ( +
+
+

Skin History

+
+ +
+
+
+ +
+
+ {skins.map(skinData => ( + + ))} +
+
+ ); +} + +function RenderSkin({ skin, selected, animation, setSelectedSkin, setSkins }: { skin: Skin; selected: Skin; animation: PlayerAnimation; setSelectedSkin: (skin: Skin) => void; setSkins: React.Dispatch>> }) { + return ( +
setSelectedSkin(skin)} + > + + {selected.skin_url === skin.skin_url ? + <> + : + + + + + setSkins(prev => prev.filter(skinData => skinData.skin_url !== skin.skin_url))} /> + + + } +
+ ); +} + +function CapeRow({ selected, selectedSkin, animation, setSelectedCape, capes }: { selected: string | null; selectedSkin: Skin; animation: PlayerAnimation; setSelectedCape: (cape: string | null) => void; capes: Array }) { + return ( +
+
+ {capes.map(cape => ( + + ))} + +
+ +
+

Cape History

+
+
+ ); +} + +function RenderCape({ selected, selectedSkin, animation, setSelectedCape, cape }: { selected: string | null; selectedSkin: Skin; animation: PlayerAnimation; setSelectedCape: (cape: string | null) => void; cape: string }) { + return ( +
setSelectedCape(cape)} + > + +
+ ); +} + +function HeaderLarge({ username, animate, toggleAnimation }: { username: string; animate: boolean; toggleAnimation: () => void }) { + return ( +
+
+

{`${username}'s Skins`}

+ +
+ + +
+
+
+ ); +} + +function HeaderSmall() { + return ( +
+

Accounts

+
+ ); +} + +function Viewer({ skinData, height = 400, width = 250, showText = true, animate = false, animation = idleAnimation, enableControls = false, flip = false }: { skinData: Skin; height?: number; width?: number; showText?: boolean; animate?: boolean; animation?: PlayerAnimation; enableControls?: boolean; flip?: boolean }) { + return ( + + ); +} diff --git a/apps/oneclient/frontend/src/routes/app/accounts.tsx b/apps/oneclient/frontend/src/routes/app/accounts.tsx index 07e3114d..43731f60 100644 --- a/apps/oneclient/frontend/src/routes/app/accounts.tsx +++ b/apps/oneclient/frontend/src/routes/app/accounts.tsx @@ -9,8 +9,8 @@ import { bindings } from '@/main'; import { useCommandMut, useCommandSuspense } from '@onelauncher/common'; import { Button } from '@onelauncher/common/components'; import { useQueryClient } from '@tanstack/react-query'; -import { createFileRoute } from '@tanstack/react-router'; -import { Trash01Icon } from '@untitled-theme/icons-react'; +import { createFileRoute, Link } from '@tanstack/react-router'; +import { Pencil01Icon, Trash01Icon } from '@untitled-theme/icons-react'; import { Button as AriaButton, DialogTrigger } from 'react-aria-components'; import { twMerge } from 'tailwind-merge'; @@ -154,6 +154,17 @@ function AccountRow({
+ + {/* TODO: Find a better way to handle handle a user that isn't just changing the default user */} + + + +
+ + ); +} diff --git a/apps/oneclient/frontend/src/components/overlay/index.ts b/apps/oneclient/frontend/src/components/overlay/index.ts index a18ef0e1..c1f1c7e2 100644 --- a/apps/oneclient/frontend/src/components/overlay/index.ts +++ b/apps/oneclient/frontend/src/components/overlay/index.ts @@ -1,5 +1,6 @@ export * from './AccountPopup'; export * from './AddAccountModal'; +export * from './ImportSkinModal'; export * from './Overlay'; export * from './Popup'; export * from './RemoveAccountModal'; diff --git a/apps/oneclient/frontend/src/routes/app/accountSkin.tsx b/apps/oneclient/frontend/src/routes/app/accountSkin.tsx index a048e493..ac55244f 100644 --- a/apps/oneclient/frontend/src/routes/app/accountSkin.tsx +++ b/apps/oneclient/frontend/src/routes/app/accountSkin.tsx @@ -5,12 +5,13 @@ import { usePlayerProfile } from '@/hooks/usePlayerProfile'; import { bindings } from '@/main'; import { useCommandSuspense } from '@onelauncher/common'; import { Button } from '@onelauncher/common/components'; +import { useQueryClient } from '@tanstack/react-query'; import { createFileRoute } from '@tanstack/react-router'; import { PlusIcon, Trash01Icon } from '@untitled-theme/icons-react'; import { useEffect, useState } from 'react'; import { DialogTrigger } from 'react-aria-components'; import { IdleAnimation, WalkingAnimation } from 'skinview3d'; -import { RemoveSkinCapeModal } from '../../components/overlay'; +import { ImportSkinModal, RemoveSkinCapeModal } from '../../components/overlay'; interface Skin { is_slim?: boolean; @@ -87,9 +88,10 @@ function RouteComponent() { }); setSelectedCape(skinData.cape_url); - setCapes(['', ...capes]) + setCapes(['', ...capes]); }, [skinData.cape_url, profile]); + return ( - {selected.skin_url === skin.skin_url ? - <> - : - - + {selected.skin_url === skin.skin_url + ? <> + : ( + + - - setSkins(prev => prev.filter(skinData => skinData.skin_url !== skin.skin_url))} /> - - - } + + setSkins(prev => prev.filter(skinData => skinData.skin_url !== skin.skin_url))} /> + + + )} ); } @@ -259,6 +261,15 @@ function HeaderLarge({ username, animate, toggleAnimation }: { username: string; + + + + + + + From e358f607361e40a20e847fa0a46d0a568cecaf9b Mon Sep 17 00:00:00 2001 From: Jacob Date: Mon, 29 Sep 2025 13:33:16 +0800 Subject: [PATCH 03/13] feat: oneclient account skin manager improt from url --- apps/oneclient/frontend/src/bindings.gen.ts | 10 ++-- .../components/overlay/ImportSkinModal.tsx | 23 ++++++-- .../frontend/src/routes/app/accountSkin.tsx | 53 ++++++++++++++----- packages/core/src/utils/minecraft.rs | 2 + 4 files changed, 66 insertions(+), 22 deletions(-) diff --git a/apps/oneclient/frontend/src/bindings.gen.ts b/apps/oneclient/frontend/src/bindings.gen.ts index be679829..5774914e 100644 --- a/apps/oneclient/frontend/src/bindings.gen.ts +++ b/apps/oneclient/frontend/src/bindings.gen.ts @@ -252,15 +252,12 @@ export type VersionType = */ "old_beta" -const ARGS_MAP = { 'events':'{"process":["event"],"ingress":["event"],"message":["event"]}', 'core':'{"getGameVersions":[],"getUsers":[],"getWorlds":["id"],"getGlobalProfile":[],"removeCluster":["id"],"updateClusterById":["id","request"],"getScreenshots":["id"],"getLoadersForVersion":["mc_version"],"getPackageBody":["provider","body"],"installModpack":["modpack","cluster_id"],"fetchMinecraftProfile":["uuid"],"getProfileOrDefault":["name"],"changeSkin":["access_token","skin_url","skin_variant"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"getRunningProcessesByClusterId":["cluster_id"],"searchPackages":["provider","query"],"openMsaLogin":[],"updateClusterProfile":["name","profile"],"getClusterById":["id"],"createCluster":["options"],"removeUser":["uuid"],"getRunningProcesses":[],"getClusters":[],"getLogByName":["id","name"],"getUser":["uuid"],"writeSettings":["setting"],"fetchLoggedInProfile":["access_token"],"killProcess":["pid"],"getMultiplePackages":["provider","slugs"],"getLogs":["id"],"launchCluster":["id","uuid"],"getDefaultUser":["fallback"],"readSettings":[],"getPackage":["provider","slug"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getUsersFromAuthor":["provider","author"],"open":["input"],"setDefaultUser":["uuid"],"isClusterRunning":["cluster_id"]}', 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}', 'oneclient':'{"getClustersGroupedByMajor":[],"openDevTools":[],"getBundlesFor":["cluster_id"]}' } +const ARGS_MAP = { 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}', 'core':'{"getProfileOrDefault":["name"],"getLogs":["id"],"launchCluster":["id","uuid"],"getRunningProcessesByClusterId":["cluster_id"],"killProcess":["pid"],"getUsers":[],"setDefaultUser":["uuid"],"fetchLoggedInProfile":["access_token"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"getLogByName":["id","name"],"getScreenshots":["id"],"getGameVersions":[],"getClusterById":["id"],"getPackageBody":["provider","body"],"getMultiplePackages":["provider","slugs"],"installModpack":["modpack","cluster_id"],"getLoadersForVersion":["mc_version"],"readSettings":[],"createCluster":["options"],"isClusterRunning":["cluster_id"],"removeUser":["uuid"],"getUsersFromAuthor":["provider","author"],"getDefaultUser":["fallback"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getUser":["uuid"],"getPackage":["provider","slug"],"getRunningProcesses":[],"openMsaLogin":[],"getWorlds":["id"],"getGlobalProfile":[],"open":["input"],"removeCluster":["id"],"changeSkin":["access_token","skin_url","skin_variant"],"updateClusterById":["id","request"],"updateClusterProfile":["name","profile"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"fetchMinecraftProfile":["uuid"],"writeSettings":["setting"],"searchPackages":["provider","query"],"getClusters":[]}', 'events':'{"ingress":["event"],"message":["event"],"process":["event"]}', 'oneclient':'{"getClustersGroupedByMajor":[],"openDevTools":[],"getBundlesFor":["cluster_id"]}' } export type Router = { 'oneclient': { openDevTools: () => Promise, getClustersGroupedByMajor: () => Promise>, getBundlesFor: (clusterId: number) => Promise }, 'folders': { fromCluster: (folderName: string) => Promise, openCluster: (folderName: string) => Promise }, -'events': { ingress: (event: IngressPayload) => Promise, -message: (event: MessagePayload) => Promise, -process: (event: ProcessPayload) => Promise }, 'core': { getClusters: () => Promise, getClusterById: (id: number) => Promise, removeCluster: (id: number) => Promise, @@ -300,7 +297,10 @@ fetchMinecraftProfile: (uuid: string) => Promise, fetchLoggedInProfile: (accessToken: string) => Promise, uploadSkinBytes: (accessToken: string, skinData: number[], imageFormat: string, skinVariant: SkinVariant) => Promise, changeSkin: (accessToken: string, skinUrl: string, skinVariant: SkinVariant) => Promise, -open: (input: string) => Promise } }; +open: (input: string) => Promise }, +'events': { ingress: (event: IngressPayload) => Promise, +message: (event: MessagePayload) => Promise, +process: (event: ProcessPayload) => Promise } }; export type { InferCommandOutput } diff --git a/apps/oneclient/frontend/src/components/overlay/ImportSkinModal.tsx b/apps/oneclient/frontend/src/components/overlay/ImportSkinModal.tsx index e37b6e33..a4645757 100644 --- a/apps/oneclient/frontend/src/components/overlay/ImportSkinModal.tsx +++ b/apps/oneclient/frontend/src/components/overlay/ImportSkinModal.tsx @@ -1,17 +1,30 @@ import { Button, TextField } from '@onelauncher/common/components'; +import { useState } from 'react'; import { Overlay } from './Overlay'; -export function ImportSkinModal() { +export function ImportSkinModal({ importFromURL }: { importFromURL: (url: string) => void }) { + const [input, setInput] = useState(''); return ( Import - + setInput(e.target.value)} /> -
- -
diff --git a/apps/oneclient/frontend/src/routes/app/accountSkin.tsx b/apps/oneclient/frontend/src/routes/app/accountSkin.tsx index ac55244f..68172275 100644 --- a/apps/oneclient/frontend/src/routes/app/accountSkin.tsx +++ b/apps/oneclient/frontend/src/routes/app/accountSkin.tsx @@ -1,9 +1,10 @@ +import type { SkinVariant } from '@/bindings.gen'; import type { PlayerAnimation } from 'skinview3d'; import { SheetPage, SkinViewer } from '@/components'; import { Overlay } from '@/components/overlay/Overlay'; import { usePlayerProfile } from '@/hooks/usePlayerProfile'; import { bindings } from '@/main'; -import { useCommandSuspense } from '@onelauncher/common'; +import { useCommandMut, useCommandSuspense } from '@onelauncher/common'; import { Button } from '@onelauncher/common/components'; import { useQueryClient } from '@tanstack/react-query'; import { createFileRoute } from '@tanstack/react-router'; @@ -47,10 +48,11 @@ export const Route = createFileRoute('/app/accountSkin')({ const idleAnimation = new IdleAnimation(); idleAnimation.speed = 0.15; const walkingAnimation = new WalkingAnimation(); -walkingAnimation.speed = 0.15; +walkingAnimation.speed = 0.1; walkingAnimation.headBobbing = false; function RouteComponent() { + const queryClient = useQueryClient(); const [skins, setSkins] = useState>(skinsData); const [capes, setCapes] = useState>(capesData); const { data: currentAccount } = useCommandSuspense(['getDefaultUser'], () => bindings.core.getDefaultUser(true)); @@ -91,15 +93,37 @@ function RouteComponent() { setCapes(['', ...capes]); }, [skinData.cape_url, profile]); + const importFromURL = (url: string) => { + setSkins([...skins, { is_slim: false, skin_url: url }]); + }; + + const { mutate: getLoggedInUser } = useCommandMut(bindings.core.fetchLoggedInProfile, { + onSuccess() { + queryClient.invalidateQueries({ + queryKey: ['getDefaultUser'], + }); + }, + }); + + const saveSkin = () => { + if (!currentAccount) + return; + const token = getLoggedInUser(currentAccount.access_token); + console.log(currentAccount); + }; + + const toggleAnimation = () => { + setAnimation(isAnimated ? idleAnimation : walkingAnimation); + setAnimated(!isAnimated); + }; return ( { - setAnimation(isAnimated ? idleAnimation : walkingAnimation); - setAnimated(!isAnimated); - }} username={profile?.username || 'UNKNOWN'} + importFromURL={importFromURL} + saveSkin={saveSkin} + username={profile?.username || 'UNKNOWN'} /> )} headerSmall={} @@ -114,6 +138,14 @@ function RouteComponent() { enableControls skinData={selectedSkin} /> +
@@ -248,17 +280,14 @@ function RenderCape({ selected, selectedSkin, animation, setSelectedCape, cape } ); } -function HeaderLarge({ username, animate, toggleAnimation }: { username: string; animate: boolean; toggleAnimation: () => void }) { +function HeaderLarge({ username, importFromURL, saveSkin }: { username: string; importFromURL: (url: string) => void; saveSkin: () => void }) { return (

{`${username}'s Skins`}

- - @@ -267,7 +296,7 @@ function HeaderLarge({ username, animate, toggleAnimation }: { username: string; - +
diff --git a/packages/core/src/utils/minecraft.rs b/packages/core/src/utils/minecraft.rs index d106fc60..bcf80b2b 100644 --- a/packages/core/src/utils/minecraft.rs +++ b/packages/core/src/utils/minecraft.rs @@ -21,7 +21,9 @@ pub struct MojangPlayerProfile { #[derive(Debug, serde::Serialize, serde::Deserialize, Clone)] #[serde(rename_all = "lowercase")] pub enum SkinVariant { + #[serde(alias = "CLASSIC")] Classic, + #[serde(alias = "SLIM")] Slim, } From 5fd6677567efdacef402f65b64b6d4b483e7de42 Mon Sep 17 00:00:00 2001 From: Jacob Date: Tue, 30 Sep 2025 09:26:57 +0800 Subject: [PATCH 04/13] feat: oneclient accoutn skin manager animations --- apps/oneclient/frontend/src/bindings.gen.ts | 18 +- .../frontend/src/routes/app/accountSkin.tsx | 192 ++++++++---------- packages/core/src/utils/minecraft.rs | 3 +- 3 files changed, 99 insertions(+), 114 deletions(-) diff --git a/apps/oneclient/frontend/src/bindings.gen.ts b/apps/oneclient/frontend/src/bindings.gen.ts index 5774914e..0f4b3747 100644 --- a/apps/oneclient/frontend/src/bindings.gen.ts +++ b/apps/oneclient/frontend/src/bindings.gen.ts @@ -108,7 +108,7 @@ export type ModpackManifest = { name: string; version: string; loader: GameLoade export type MojangCape = { id: string; state: string; url: string; alias: string } -export type MojangFullPlayerProfile = { uuid: string; username: string; skins: MojangSkin[]; capes: MojangCape[] } +export type MojangFullPlayerProfile = { id: string; username: string; skins: MojangSkin[]; capes: MojangCape[] } export type MojangPlayerProfile = { uuid: string; username: string; is_slim: boolean; skin_url: string | null; cape_url: string | null } @@ -252,12 +252,15 @@ export type VersionType = */ "old_beta" -const ARGS_MAP = { 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}', 'core':'{"getProfileOrDefault":["name"],"getLogs":["id"],"launchCluster":["id","uuid"],"getRunningProcessesByClusterId":["cluster_id"],"killProcess":["pid"],"getUsers":[],"setDefaultUser":["uuid"],"fetchLoggedInProfile":["access_token"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"getLogByName":["id","name"],"getScreenshots":["id"],"getGameVersions":[],"getClusterById":["id"],"getPackageBody":["provider","body"],"getMultiplePackages":["provider","slugs"],"installModpack":["modpack","cluster_id"],"getLoadersForVersion":["mc_version"],"readSettings":[],"createCluster":["options"],"isClusterRunning":["cluster_id"],"removeUser":["uuid"],"getUsersFromAuthor":["provider","author"],"getDefaultUser":["fallback"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getUser":["uuid"],"getPackage":["provider","slug"],"getRunningProcesses":[],"openMsaLogin":[],"getWorlds":["id"],"getGlobalProfile":[],"open":["input"],"removeCluster":["id"],"changeSkin":["access_token","skin_url","skin_variant"],"updateClusterById":["id","request"],"updateClusterProfile":["name","profile"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"fetchMinecraftProfile":["uuid"],"writeSettings":["setting"],"searchPackages":["provider","query"],"getClusters":[]}', 'events':'{"ingress":["event"],"message":["event"],"process":["event"]}', 'oneclient':'{"getClustersGroupedByMajor":[],"openDevTools":[],"getBundlesFor":["cluster_id"]}' } -export type Router = { 'oneclient': { openDevTools: () => Promise, -getClustersGroupedByMajor: () => Promise>, -getBundlesFor: (clusterId: number) => Promise }, +const ARGS_MAP = { 'core':'{"killProcess":["pid"],"getUser":["uuid"],"getClusterById":["id"],"getPackage":["provider","slug"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getClusters":[],"getRunningProcesses":[],"isClusterRunning":["cluster_id"],"getUsers":[],"getGameVersions":[],"fetchMinecraftProfile":["uuid"],"launchCluster":["id","uuid"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"createCluster":["options"],"getScreenshots":["id"],"removeUser":["uuid"],"getWorlds":["id"],"writeSettings":["setting"],"openMsaLogin":[],"searchPackages":["provider","query"],"readSettings":[],"getLogs":["id"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"installModpack":["modpack","cluster_id"],"getLoadersForVersion":["mc_version"],"getRunningProcessesByClusterId":["cluster_id"],"getMultiplePackages":["provider","slugs"],"setDefaultUser":["uuid"],"changeSkin":["access_token","skin_url","skin_variant"],"getProfileOrDefault":["name"],"getPackageBody":["provider","body"],"updateClusterById":["id","request"],"updateClusterProfile":["name","profile"],"getLogByName":["id","name"],"getGlobalProfile":[],"removeCluster":["id"],"getDefaultUser":["fallback"],"open":["input"],"getUsersFromAuthor":["provider","author"],"fetchLoggedInProfile":["access_token"]}', 'oneclient':'{"openDevTools":[],"getClustersGroupedByMajor":[],"getBundlesFor":["cluster_id"]}', 'events':'{"ingress":["event"],"message":["event"],"process":["event"]}', 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}' } +export type Router = { 'events': { ingress: (event: IngressPayload) => Promise, +message: (event: MessagePayload) => Promise, +process: (event: ProcessPayload) => Promise }, 'folders': { fromCluster: (folderName: string) => Promise, openCluster: (folderName: string) => Promise }, +'oneclient': { openDevTools: () => Promise, +getClustersGroupedByMajor: () => Promise>, +getBundlesFor: (clusterId: number) => Promise }, 'core': { getClusters: () => Promise, getClusterById: (id: number) => Promise, removeCluster: (id: number) => Promise, @@ -297,10 +300,7 @@ fetchMinecraftProfile: (uuid: string) => Promise, fetchLoggedInProfile: (accessToken: string) => Promise, uploadSkinBytes: (accessToken: string, skinData: number[], imageFormat: string, skinVariant: SkinVariant) => Promise, changeSkin: (accessToken: string, skinUrl: string, skinVariant: SkinVariant) => Promise, -open: (input: string) => Promise }, -'events': { ingress: (event: IngressPayload) => Promise, -message: (event: MessagePayload) => Promise, -process: (event: ProcessPayload) => Promise } }; +open: (input: string) => Promise } }; export type { InferCommandOutput } diff --git a/apps/oneclient/frontend/src/routes/app/accountSkin.tsx b/apps/oneclient/frontend/src/routes/app/accountSkin.tsx index 68172275..db66dc12 100644 --- a/apps/oneclient/frontend/src/routes/app/accountSkin.tsx +++ b/apps/oneclient/frontend/src/routes/app/accountSkin.tsx @@ -1,17 +1,16 @@ -import type { SkinVariant } from '@/bindings.gen'; +import type { MinecraftCredentials } from '@/bindings.gen'; import type { PlayerAnimation } from 'skinview3d'; import { SheetPage, SkinViewer } from '@/components'; import { Overlay } from '@/components/overlay/Overlay'; import { usePlayerProfile } from '@/hooks/usePlayerProfile'; import { bindings } from '@/main'; -import { useCommandMut, useCommandSuspense } from '@onelauncher/common'; +import { useCommandSuspense } from '@onelauncher/common'; import { Button } from '@onelauncher/common/components'; -import { useQueryClient } from '@tanstack/react-query'; import { createFileRoute } from '@tanstack/react-router'; -import { PlusIcon, Trash01Icon } from '@untitled-theme/icons-react'; +import { Download01Icon, PlusIcon, Trash01Icon } from '@untitled-theme/icons-react'; import { useEffect, useState } from 'react'; import { DialogTrigger } from 'react-aria-components'; -import { IdleAnimation, WalkingAnimation } from 'skinview3d'; +import { CrouchAnimation, FlyingAnimation, HitAnimation, IdleAnimation, WalkingAnimation } from 'skinview3d'; import { ImportSkinModal, RemoveSkinCapeModal } from '../../components/overlay'; interface Skin { @@ -20,45 +19,36 @@ interface Skin { cape_url?: string | null; } -const skinsData: Array = [ - { - is_slim: false, - skin_url: 'http://textures.minecraft.net/texture/90b8789136facaa9f87b765140e1c8135e6652f513481bd84e6bd8c44844d7ce', - }, - { - is_slim: true, - skin_url: 'https://textures.minecraft.net/texture/69655f89a9fb19a7da292a757be65c52efeab337e3f9579ae090815cfe9cd6d5', - }, - { - is_slim: false, - skin_url: 'http://textures.minecraft.net/texture/c8ccb0647686d04135ac92f4c19b9961b409f8ae3ac5dbea4040e57cda2bcaba', - }, -]; - -const capesData: Array = [ - 'http://textures.minecraft.net/texture/2340c0e03dd24a11b15a8b33c2a7e9e32abb2051b2481d0ba7defd635ca7a933', - 'http://textures.minecraft.net/texture/f9a76537647989f9a0b6d001e320dac591c359e9e61a31f4ce11c88f207f0ad4', - 'http://textures.minecraft.net/texture/28de4a81688ad18b49e735a273e086c18f1e3966956123ccb574034c06f5d336', -]; - export const Route = createFileRoute('/app/accountSkin')({ component: RouteComponent, }); -const idleAnimation = new IdleAnimation(); -idleAnimation.speed = 0.15; -const walkingAnimation = new WalkingAnimation(); -walkingAnimation.speed = 0.1; -walkingAnimation.headBobbing = false; +const animations = [ + { name: 'Idle', animation: new IdleAnimation(), speed: 0.1 }, + { name: 'Walking', animation: new WalkingAnimation(), speed: 0.1 }, + { name: 'Flying', animation: new FlyingAnimation(), speed: 0.2 }, + { name: 'Crouch', animation: new CrouchAnimation(), speed: 0.025 }, + { name: 'Hit', animation: new HitAnimation(), speed: 0.1 }, +]; function RouteComponent() { - const queryClient = useQueryClient(); - const [skins, setSkins] = useState>(skinsData); - const [capes, setCapes] = useState>(capesData); + const [skins, setSkins] = useState>([]); + const [capes, setCapes] = useState>([]); const { data: currentAccount } = useCommandSuspense(['getDefaultUser'], () => bindings.core.getDefaultUser(true)); + const { data: loggedInUser } = useCommandSuspense(['fetchLoggedInProfile'], () => bindings.core.fetchLoggedInProfile((currentAccount as MinecraftCredentials).access_token)); + useEffect(() => { + setSkins( + loggedInUser.skins.map(skin => ({ + is_slim: skin.variant === 'slim', + skin_url: skin.url, + })), + ); + + setCapes(loggedInUser.capes.map(cape => cape.url)); + }, [loggedInUser]); const { data: profile } = usePlayerProfile(currentAccount?.id); - const [isAnimated, setAnimated] = useState(false); - const [animation, setAnimation] = useState(idleAnimation); + const [animation, setAnimation] = useState(animations[0].animation); + const [animationName, setAnimationName] = useState(animations[0].name); const skinData: Skin = { is_slim: profile?.is_slim, skin_url: profile?.skin_url, @@ -75,56 +65,58 @@ function RouteComponent() { setSelectedSkin(skinData); }, [skinData.skin_url, profile]); - const [selectedCape, setSelectedCapeSTATE] = useState(profile?.cape_url || null); - const setSelectedCape = (cape: string | null) => { + const [selectedCape, setSelectedCapeSTATE] = useState(profile?.cape_url || ''); + const setSelectedCape = (cape: string) => { setSelectedCapeSTATE(cape); if (selectedSkin.skin_url !== null) setSelectedSkin({ ...selectedSkin, cape_url: cape }); }; useEffect(() => { - if (!skinData.cape_url) - return; setCapes((prev) => { - const filtered = prev.filter(cape => cape !== skinData.cape_url); - return ['', skinData.cape_url, ...filtered].filter(Boolean) as Array; + const filtered = prev.filter(cape => cape !== ''); + return ['', ...filtered]; }); - setSelectedCape(skinData.cape_url); - - setCapes(['', ...capes]); + setSelectedCape(skinData.cape_url || ''); }, [skinData.cape_url, profile]); const importFromURL = (url: string) => { setSkins([...skins, { is_slim: false, skin_url: url }]); }; - const { mutate: getLoggedInUser } = useCommandMut(bindings.core.fetchLoggedInProfile, { - onSuccess() { - queryClient.invalidateQueries({ - queryKey: ['getDefaultUser'], - }); - }, - }); - - const saveSkin = () => { - if (!currentAccount) - return; - const token = getLoggedInUser(currentAccount.access_token); - console.log(currentAccount); + useEffect(() => { + importFromURL('http://textures.minecraft.net/texture/90b8789136facaa9f87b765140e1c8135e6652f513481bd84e6bd8c44844d7ce'); + }, []); + + if (currentAccount === null) + return ( + } headerSmall={<>}> + +

No accounts added

+
+
+ ); + + const getNextAnimationData = () => { + const animationIndex = animations.findIndex(animationData => animationData.name === animationName); + if (animationIndex === -1 || animationIndex === animations.length - 1) + return animations[0]; + else + return animations[animationIndex + 1]; }; - const toggleAnimation = () => { - setAnimation(isAnimated ? idleAnimation : walkingAnimation); - setAnimated(!isAnimated); + const changeSelectedAnimation = () => { + const data = getNextAnimationData(); + data.animation.speed = data.speed; + if (data.name === 'Walking') + (data.animation as WalkingAnimation).headBobbing = false; + setAnimation(data.animation); + setAnimationName(data.name); }; return ( + )} headerSmall={} > @@ -132,20 +124,15 @@ function RouteComponent() {

Current Skin

+ + -
@@ -154,6 +141,7 @@ function RouteComponent() { void; skins: Array; setSkins: React.Dispatch>> }) { +function SkinHistoryRow({ selected, animation, setSelectedSkin, skins, setSkins, importFromURL }: { selected: Skin; animation: PlayerAnimation; setSelectedSkin: (skin: Skin) => void; skins: Array; setSkins: React.Dispatch>>; importFromURL: (url: string) => void }) { return (
@@ -186,11 +174,16 @@ function SkinHistoryRow({ selected, animation, setSelectedSkin, skins, setSkins
-
-
- -
-
+ + + + + + {skins.map(skinData => ( void; setSkins: React.Dispatch>> }) { return ( -
setSelectedSkin(skin)} > )} -
+ ); } -function CapeRow({ selected, selectedSkin, animation, setSelectedCape, capes }: { selected: string | null; selectedSkin: Skin; animation: PlayerAnimation; setSelectedCape: (cape: string | null) => void; capes: Array }) { +function CapeRow({ selected, selectedSkin, animation, setSelectedCape, capes }: { selected: string | null; selectedSkin: Skin; animation: PlayerAnimation; setSelectedCape: (cape: string) => void; capes: Array }) { return (
@@ -261,14 +254,14 @@ function CapeRow({ selected, selectedSkin, animation, setSelectedCape, capes }: ); } -function RenderCape({ selected, selectedSkin, animation, setSelectedCape, cape }: { selected: string | null; selectedSkin: Skin; animation: PlayerAnimation; setSelectedCape: (cape: string | null) => void; cape: string }) { +function RenderCape({ selected, selectedSkin, animation, setSelectedCape, cape }: { selected: string | null; selectedSkin: Skin; animation: PlayerAnimation; setSelectedCape: (cape: string) => void; cape: string }) { return ( -
setSelectedCape(cape)} > -
+ ); } -function HeaderLarge({ username, importFromURL, saveSkin }: { username: string; importFromURL: (url: string) => void; saveSkin: () => void }) { +function HeaderLarge({ username }: { username: string }) { return (

{`${username}'s Skins`}

- - - - - - - -
@@ -313,10 +297,10 @@ function HeaderSmall() { ); } -function Viewer({ skinData, height = 400, width = 250, showText = true, animate = false, animation = idleAnimation, enableControls = false, flip = false }: { skinData: Skin; height?: number; width?: number; showText?: boolean; animate?: boolean; animation?: PlayerAnimation; enableControls?: boolean; flip?: boolean }) { +function Viewer({ skinData, height = 400, width = 250, showText = true, animation, enableControls = false, flip = false }: { skinData: Skin; height?: number; width?: number; showText?: boolean; animation?: PlayerAnimation; enableControls?: boolean; flip?: boolean }) { return ( , pub capes: Vec, From e66cd104ddd46926f9b50cf55c56cab4dfc99fb3 Mon Sep 17 00:00:00 2001 From: Jacob Date: Tue, 30 Sep 2025 10:31:19 +0800 Subject: [PATCH 05/13] feat: oneclient account skin manager export skin --- Cargo.lock | 6 ++- Cargo.toml | 1 + apps/oneclient/desktop/Cargo.toml | 1 + .../desktop/capabilities/default.json | 5 ++- apps/oneclient/desktop/src/lib.rs | 1 + apps/oneclient/frontend/package.json | 1 + apps/oneclient/frontend/src/bindings.gen.ts | 22 +++++------ .../frontend/src/routes/app/accountSkin.tsx | 37 +++++++++++++++++++ pnpm-lock.yaml | 18 +++++++++ pnpm-workspace.yaml | 1 + 10 files changed, 79 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b2ab04fd..af19ed27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3411,6 +3411,7 @@ dependencies = [ "tauri-plugin-clipboard-manager", "tauri-plugin-deep-link", "tauri-plugin-dialog", + "tauri-plugin-fs", "tauri-plugin-single-instance", "tauri-plugin-updater", "tauri-plugin-window-state", @@ -6061,9 +6062,9 @@ dependencies = [ [[package]] name = "tauri-plugin-fs" -version = "2.3.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ead0daec5d305adcefe05af9d970fc437bcc7996052d564e7393eb291252da" +checksum = "88371e340ad2f07409a3b68294abe73f20bc9c1bc1b631a31dc37a3d0161f682" dependencies = [ "anyhow", "dunce", @@ -6079,6 +6080,7 @@ dependencies = [ "thiserror 2.0.12", "toml", "url", + "uuid 1.16.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 81dc2a18..fc08dc38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ tauri-build = { version = "=2.2.0" } tauri-plugin-clipboard-manager = { version = "=2.2.2" } tauri-plugin-deep-link = { version = "=2.2.1" } tauri-plugin-dialog = { version = "=2.2.1" } +tauri-plugin-fs = { version = "=2.2.1" } tauri-plugin-single-instance = { version = "=2.2.3" } tauri-plugin-updater = { version = "=2.7.1" } tauri-plugin-window-state = { version = "=2.2.2" } diff --git a/apps/oneclient/desktop/Cargo.toml b/apps/oneclient/desktop/Cargo.toml index 8e2ce0c1..572627e8 100644 --- a/apps/oneclient/desktop/Cargo.toml +++ b/apps/oneclient/desktop/Cargo.toml @@ -36,6 +36,7 @@ tauri-plugin-single-instance = { workspace = true } tauri-plugin-updater = { workspace = true } tauri-plugin-clipboard-manager = { workspace = true } tauri-plugin-dialog = { workspace = true } +tauri-plugin-fs = { workspace = true } tauri-plugin-deep-link = { workspace = true } # code gen diff --git a/apps/oneclient/desktop/capabilities/default.json b/apps/oneclient/desktop/capabilities/default.json index 40d2165e..208960d7 100644 --- a/apps/oneclient/desktop/capabilities/default.json +++ b/apps/oneclient/desktop/capabilities/default.json @@ -31,6 +31,9 @@ "dialog:allow-save", "dialog:allow-confirm", "deep-link:default", - "updater:default" + "updater:default", + + "fs:allow-download-read", + "fs:allow-download-write" ] } diff --git a/apps/oneclient/desktop/src/lib.rs b/apps/oneclient/desktop/src/lib.rs index 2bc41045..bbda4a49 100644 --- a/apps/oneclient/desktop/src/lib.rs +++ b/apps/oneclient/desktop/src/lib.rs @@ -71,6 +71,7 @@ async fn initialize_tauri(builder: tauri::Builder) -> LauncherResult // .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_clipboard_manager::init()) .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_deep_link::init()) .menu(tauri::menu::Menu::new) .invoke_handler(router.into_handler()) diff --git a/apps/oneclient/frontend/package.json b/apps/oneclient/frontend/package.json index e4e50039..ed92b5b4 100644 --- a/apps/oneclient/frontend/package.json +++ b/apps/oneclient/frontend/package.json @@ -28,6 +28,7 @@ "@tauri-apps/api": "catalog:", "@tauri-apps/plugin-clipboard-manager": "catalog:", "@tauri-apps/plugin-dialog": "catalog:", + "@tauri-apps/plugin-fs": "catalog:", "@untitled-theme/icons-react": "catalog:", "motion": "catalog:", "overlayscrollbars": "catalog:", diff --git a/apps/oneclient/frontend/src/bindings.gen.ts b/apps/oneclient/frontend/src/bindings.gen.ts index 0f4b3747..3d73a331 100644 --- a/apps/oneclient/frontend/src/bindings.gen.ts +++ b/apps/oneclient/frontend/src/bindings.gen.ts @@ -252,16 +252,8 @@ export type VersionType = */ "old_beta" -const ARGS_MAP = { 'core':'{"killProcess":["pid"],"getUser":["uuid"],"getClusterById":["id"],"getPackage":["provider","slug"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getClusters":[],"getRunningProcesses":[],"isClusterRunning":["cluster_id"],"getUsers":[],"getGameVersions":[],"fetchMinecraftProfile":["uuid"],"launchCluster":["id","uuid"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"createCluster":["options"],"getScreenshots":["id"],"removeUser":["uuid"],"getWorlds":["id"],"writeSettings":["setting"],"openMsaLogin":[],"searchPackages":["provider","query"],"readSettings":[],"getLogs":["id"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"installModpack":["modpack","cluster_id"],"getLoadersForVersion":["mc_version"],"getRunningProcessesByClusterId":["cluster_id"],"getMultiplePackages":["provider","slugs"],"setDefaultUser":["uuid"],"changeSkin":["access_token","skin_url","skin_variant"],"getProfileOrDefault":["name"],"getPackageBody":["provider","body"],"updateClusterById":["id","request"],"updateClusterProfile":["name","profile"],"getLogByName":["id","name"],"getGlobalProfile":[],"removeCluster":["id"],"getDefaultUser":["fallback"],"open":["input"],"getUsersFromAuthor":["provider","author"],"fetchLoggedInProfile":["access_token"]}', 'oneclient':'{"openDevTools":[],"getClustersGroupedByMajor":[],"getBundlesFor":["cluster_id"]}', 'events':'{"ingress":["event"],"message":["event"],"process":["event"]}', 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}' } -export type Router = { 'events': { ingress: (event: IngressPayload) => Promise, -message: (event: MessagePayload) => Promise, -process: (event: ProcessPayload) => Promise }, -'folders': { fromCluster: (folderName: string) => Promise, -openCluster: (folderName: string) => Promise }, -'oneclient': { openDevTools: () => Promise, -getClustersGroupedByMajor: () => Promise>, -getBundlesFor: (clusterId: number) => Promise }, -'core': { getClusters: () => Promise, +const ARGS_MAP = { 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}', 'core':'{"changeSkin":["access_token","skin_url","skin_variant"],"open":["input"],"killProcess":["pid"],"writeSettings":["setting"],"getProfileOrDefault":["name"],"removeUser":["uuid"],"launchCluster":["id","uuid"],"getPackage":["provider","slug"],"updateClusterProfile":["name","profile"],"installModpack":["modpack","cluster_id"],"isClusterRunning":["cluster_id"],"getPackageBody":["provider","body"],"getDefaultUser":["fallback"],"updateClusterById":["id","request"],"fetchMinecraftProfile":["uuid"],"removeCluster":["id"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getRunningProcesses":[],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getLoadersForVersion":["mc_version"],"getWorlds":["id"],"getGameVersions":[],"getGlobalProfile":[],"openMsaLogin":[],"getMultiplePackages":["provider","slugs"],"getLogByName":["id","name"],"readSettings":[],"getScreenshots":["id"],"getClusters":[],"getUsers":[],"setDefaultUser":["uuid"],"searchPackages":["provider","query"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"getRunningProcessesByClusterId":["cluster_id"],"createCluster":["options"],"getLogs":["id"],"getUser":["uuid"],"getClusterById":["id"],"getUsersFromAuthor":["provider","author"],"fetchLoggedInProfile":["access_token"]}', 'events':'{"process":["event"],"message":["event"],"ingress":["event"]}', 'oneclient':'{"openDevTools":[],"getClustersGroupedByMajor":[],"getBundlesFor":["cluster_id"]}' } +export type Router = { 'core': { getClusters: () => Promise, getClusterById: (id: number) => Promise, removeCluster: (id: number) => Promise, createCluster: (options: CreateCluster) => Promise, @@ -300,7 +292,15 @@ fetchMinecraftProfile: (uuid: string) => Promise, fetchLoggedInProfile: (accessToken: string) => Promise, uploadSkinBytes: (accessToken: string, skinData: number[], imageFormat: string, skinVariant: SkinVariant) => Promise, changeSkin: (accessToken: string, skinUrl: string, skinVariant: SkinVariant) => Promise, -open: (input: string) => Promise } }; +open: (input: string) => Promise }, +'events': { ingress: (event: IngressPayload) => Promise, +message: (event: MessagePayload) => Promise, +process: (event: ProcessPayload) => Promise }, +'folders': { fromCluster: (folderName: string) => Promise, +openCluster: (folderName: string) => Promise }, +'oneclient': { openDevTools: () => Promise, +getClustersGroupedByMajor: () => Promise>, +getBundlesFor: (clusterId: number) => Promise } }; export type { InferCommandOutput } diff --git a/apps/oneclient/frontend/src/routes/app/accountSkin.tsx b/apps/oneclient/frontend/src/routes/app/accountSkin.tsx index db66dc12..065b3e05 100644 --- a/apps/oneclient/frontend/src/routes/app/accountSkin.tsx +++ b/apps/oneclient/frontend/src/routes/app/accountSkin.tsx @@ -7,6 +7,9 @@ import { bindings } from '@/main'; import { useCommandSuspense } from '@onelauncher/common'; import { Button } from '@onelauncher/common/components'; import { createFileRoute } from '@tanstack/react-router'; +import { downloadDir, join } from '@tauri-apps/api/path'; +import { save } from '@tauri-apps/plugin-dialog'; +import { writeFile } from '@tauri-apps/plugin-fs'; import { Download01Icon, PlusIcon, Trash01Icon } from '@untitled-theme/icons-react'; import { useEffect, useState } from 'react'; import { DialogTrigger } from 'react-aria-components'; @@ -200,6 +203,32 @@ function SkinHistoryRow({ selected, animation, setSelectedSkin, skins, setSkins, } function RenderSkin({ skin, selected, animation, setSelectedSkin, setSkins }: { skin: Skin; selected: Skin; animation: PlayerAnimation; setSelectedSkin: (skin: Skin) => void; setSkins: React.Dispatch>> }) { + const handleSave = async () => { + try { + if (!skin.skin_url) + return; + const filePath = await save({ + title: 'Skin Export Location', + filters: [ + { + name: 'Images', + extensions: ['png'], + }, + ], + defaultPath: await join(await downloadDir(), `${skin.skin_url.split('/').reverse()[0]}.png`), + }); + + if (!filePath) + return; + + const response = await fetch(skin.skin_url); + const buffer = await response.arrayBuffer(); + + await writeFile(filePath, new Uint8Array(buffer)); + } catch (error) { + console.error(error); + } + }; return ( ); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c243cacb..6da2ef47 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,6 +60,9 @@ catalogs: '@tauri-apps/plugin-dialog': specifier: ^2.2.1 version: 2.2.1 + '@tauri-apps/plugin-fs': + specifier: ^2.2.1 + version: 2.4.2 '@tauri-apps/plugin-opener': specifier: 2.3.0 version: 2.3.0 @@ -277,6 +280,9 @@ importers: '@tauri-apps/plugin-dialog': specifier: 'catalog:' version: 2.2.1 + '@tauri-apps/plugin-fs': + specifier: 'catalog:' + version: 2.4.2 '@untitled-theme/icons-react': specifier: 'catalog:' version: 0.14.0(@types/react@19.1.2)(react@19.1.0) @@ -2868,6 +2874,9 @@ packages: '@tauri-apps/api@2.5.0': resolution: {integrity: sha512-Ldux4ip+HGAcPUmuLT8EIkk6yafl5vK0P0c0byzAKzxJh7vxelVtdPONjfgTm96PbN24yjZNESY8CKo8qniluA==} + '@tauri-apps/api@2.8.0': + resolution: {integrity: sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw==} + '@tauri-apps/cli-darwin-arm64@2.5.0': resolution: {integrity: sha512-VuVAeTFq86dfpoBDNYAdtQVLbP0+2EKCHIIhkaxjeoPARR0sLpFHz2zs0PcFU76e+KAaxtEtAJAXGNUc8E1PzQ==} engines: {node: '>= 10'} @@ -2945,6 +2954,9 @@ packages: '@tauri-apps/plugin-dialog@2.2.1': resolution: {integrity: sha512-wZmCouo4PgTosh/UoejPw9DPs6RllS5Pp3fuOV2JobCu36mR5AXU2MzU9NZiVaFi/5Zfc8RN0IhcZHnksJ1o8A==} + '@tauri-apps/plugin-fs@2.4.2': + resolution: {integrity: sha512-YGhmYuTgXGsi6AjoV+5mh2NvicgWBfVJHHheuck6oHD+HC9bVWPaHvCP0/Aw4pHDejwrvT8hE3+zZAaWf+hrig==} + '@tauri-apps/plugin-opener@2.3.0': resolution: {integrity: sha512-yAbauwp8BCHIhhA48NN8rEf6OtfZBPCgTOCa10gmtoVCpmic5Bq+1Ba7C+NZOjogedkSiV7hAotjYnnbUVmYrw==} @@ -8828,6 +8840,8 @@ snapshots: '@tauri-apps/api@2.5.0': {} + '@tauri-apps/api@2.8.0': {} + '@tauri-apps/cli-darwin-arm64@2.5.0': optional: true @@ -8883,6 +8897,10 @@ snapshots: dependencies: '@tauri-apps/api': 2.5.0 + '@tauri-apps/plugin-fs@2.4.2': + dependencies: + '@tauri-apps/api': 2.8.0 + '@tauri-apps/plugin-opener@2.3.0': dependencies: '@tauri-apps/api': 2.5.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 57eaf623..7f903d5b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -46,6 +46,7 @@ catalog: '@tauri-apps/api': ^2.5.0 '@tauri-apps/plugin-clipboard-manager': ^2.2.2 '@tauri-apps/plugin-dialog': ^2.2.1 + '@tauri-apps/plugin-fs': ^2.2.1 '@tauri-apps/plugin-shell': ^2.2.1 '@tauri-apps/plugin-opener': 2.3.0 overlayscrollbars: ^2.11.2 From c3c159bd46bd4ef82c43464c8d58b99e50b2599d Mon Sep 17 00:00:00 2001 From: Jacob Date: Tue, 30 Sep 2025 11:15:47 +0800 Subject: [PATCH 06/13] feat: oneclient account skin manager skin history --- .../desktop/capabilities/default.json | 4 +- apps/oneclient/frontend/src/bindings.gen.ts | 16 +-- .../frontend/src/routes/app/accountSkin.tsx | 99 ++++++++++++++----- 3 files changed, 88 insertions(+), 31 deletions(-) diff --git a/apps/oneclient/desktop/capabilities/default.json b/apps/oneclient/desktop/capabilities/default.json index 208960d7..341ba87a 100644 --- a/apps/oneclient/desktop/capabilities/default.json +++ b/apps/oneclient/desktop/capabilities/default.json @@ -34,6 +34,8 @@ "updater:default", "fs:allow-download-read", - "fs:allow-download-write" + "fs:allow-download-write", + "fs:allow-data-read-recursive", + "fs:allow-data-write-recursive" ] } diff --git a/apps/oneclient/frontend/src/bindings.gen.ts b/apps/oneclient/frontend/src/bindings.gen.ts index 3d73a331..c2356463 100644 --- a/apps/oneclient/frontend/src/bindings.gen.ts +++ b/apps/oneclient/frontend/src/bindings.gen.ts @@ -252,8 +252,10 @@ export type VersionType = */ "old_beta" -const ARGS_MAP = { 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}', 'core':'{"changeSkin":["access_token","skin_url","skin_variant"],"open":["input"],"killProcess":["pid"],"writeSettings":["setting"],"getProfileOrDefault":["name"],"removeUser":["uuid"],"launchCluster":["id","uuid"],"getPackage":["provider","slug"],"updateClusterProfile":["name","profile"],"installModpack":["modpack","cluster_id"],"isClusterRunning":["cluster_id"],"getPackageBody":["provider","body"],"getDefaultUser":["fallback"],"updateClusterById":["id","request"],"fetchMinecraftProfile":["uuid"],"removeCluster":["id"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getRunningProcesses":[],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getLoadersForVersion":["mc_version"],"getWorlds":["id"],"getGameVersions":[],"getGlobalProfile":[],"openMsaLogin":[],"getMultiplePackages":["provider","slugs"],"getLogByName":["id","name"],"readSettings":[],"getScreenshots":["id"],"getClusters":[],"getUsers":[],"setDefaultUser":["uuid"],"searchPackages":["provider","query"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"getRunningProcessesByClusterId":["cluster_id"],"createCluster":["options"],"getLogs":["id"],"getUser":["uuid"],"getClusterById":["id"],"getUsersFromAuthor":["provider","author"],"fetchLoggedInProfile":["access_token"]}', 'events':'{"process":["event"],"message":["event"],"ingress":["event"]}', 'oneclient':'{"openDevTools":[],"getClustersGroupedByMajor":[],"getBundlesFor":["cluster_id"]}' } -export type Router = { 'core': { getClusters: () => Promise, +const ARGS_MAP = { 'oneclient':'{"getBundlesFor":["cluster_id"],"openDevTools":[],"getClustersGroupedByMajor":[]}', 'events':'{"process":["event"],"message":["event"],"ingress":["event"]}', 'core':'{"changeSkin":["access_token","skin_url","skin_variant"],"open":["input"],"killProcess":["pid"],"writeSettings":["setting"],"getProfileOrDefault":["name"],"removeUser":["uuid"],"launchCluster":["id","uuid"],"getPackage":["provider","slug"],"updateClusterProfile":["name","profile"],"installModpack":["modpack","cluster_id"],"isClusterRunning":["cluster_id"],"getPackageBody":["provider","body"],"getDefaultUser":["fallback"],"updateClusterById":["id","request"],"fetchMinecraftProfile":["uuid"],"removeCluster":["id"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getRunningProcesses":[],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getLoadersForVersion":["mc_version"],"getWorlds":["id"],"getGameVersions":[],"getGlobalProfile":[],"openMsaLogin":[],"getMultiplePackages":["provider","slugs"],"getLogByName":["id","name"],"readSettings":[],"getScreenshots":["id"],"getClusters":[],"getUsers":[],"setDefaultUser":["uuid"],"searchPackages":["provider","query"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"getRunningProcessesByClusterId":["cluster_id"],"createCluster":["options"],"getLogs":["id"],"getUser":["uuid"],"getClusterById":["id"],"getUsersFromAuthor":["provider","author"],"fetchLoggedInProfile":["access_token"]}', 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}' } +export type Router = { 'folders': { fromCluster: (folderName: string) => Promise, +openCluster: (folderName: string) => Promise }, +'core': { getClusters: () => Promise, getClusterById: (id: number) => Promise, removeCluster: (id: number) => Promise, createCluster: (options: CreateCluster) => Promise, @@ -293,14 +295,12 @@ fetchLoggedInProfile: (accessToken: string) => Promise, uploadSkinBytes: (accessToken: string, skinData: number[], imageFormat: string, skinVariant: SkinVariant) => Promise, changeSkin: (accessToken: string, skinUrl: string, skinVariant: SkinVariant) => Promise, open: (input: string) => Promise }, -'events': { ingress: (event: IngressPayload) => Promise, -message: (event: MessagePayload) => Promise, -process: (event: ProcessPayload) => Promise }, -'folders': { fromCluster: (folderName: string) => Promise, -openCluster: (folderName: string) => Promise }, 'oneclient': { openDevTools: () => Promise, getClustersGroupedByMajor: () => Promise>, -getBundlesFor: (clusterId: number) => Promise } }; +getBundlesFor: (clusterId: number) => Promise }, +'events': { ingress: (event: IngressPayload) => Promise, +message: (event: MessagePayload) => Promise, +process: (event: ProcessPayload) => Promise } }; export type { InferCommandOutput } diff --git a/apps/oneclient/frontend/src/routes/app/accountSkin.tsx b/apps/oneclient/frontend/src/routes/app/accountSkin.tsx index 065b3e05..bdd247c8 100644 --- a/apps/oneclient/frontend/src/routes/app/accountSkin.tsx +++ b/apps/oneclient/frontend/src/routes/app/accountSkin.tsx @@ -7,9 +7,9 @@ import { bindings } from '@/main'; import { useCommandSuspense } from '@onelauncher/common'; import { Button } from '@onelauncher/common/components'; import { createFileRoute } from '@tanstack/react-router'; -import { downloadDir, join } from '@tauri-apps/api/path'; +import { dataDir, downloadDir, join } from '@tauri-apps/api/path'; import { save } from '@tauri-apps/plugin-dialog'; -import { writeFile } from '@tauri-apps/plugin-fs'; +import { exists, mkdir, readTextFile, writeFile, writeTextFile } from '@tauri-apps/plugin-fs'; import { Download01Icon, PlusIcon, Trash01Icon } from '@untitled-theme/icons-react'; import { useEffect, useState } from 'react'; import { DialogTrigger } from 'react-aria-components'; @@ -34,18 +34,76 @@ const animations = [ { name: 'Hit', animation: new HitAnimation(), speed: 0.1 }, ]; +async function getSkinHistory(): Promise> { + const parentDir = await join(await dataDir(), 'OneClient', 'metadata', 'history'); + const skinsPath = await join(parentDir, 'skins.json'); + try { + const dirExists = await exists(parentDir); + if (!dirExists) + await mkdir(parentDir, { recursive: true }); + + const fileExists = await exists(skinsPath); + if (!fileExists) { + await writeTextFile(skinsPath, JSON.stringify([])); + return []; + } + + const contents = await readTextFile(skinsPath); + return JSON.parse(contents) as Array; + } + catch (error) { + console.error(error); + await writeTextFile(skinsPath, JSON.stringify([])); + return []; + } +} + +async function saveSkinHistory(skins: Array): Promise { + const parentDir = await join(await dataDir(), 'OneClient', 'metadata', 'history'); + const skinsPath = await join(parentDir, 'skins.json'); + try { + const dirExists = await exists(parentDir); + if (!dirExists) + await mkdir(parentDir, { recursive: true }); + + await writeTextFile(skinsPath, JSON.stringify(skins)); + } + catch (error) { + console.error(error); + } +} + function RouteComponent() { - const [skins, setSkins] = useState>([]); + const [skins, setSkinsSTATE] = useState>([]); + const setSkins = (updater: Array | ((prev: Array) => Array)) => { + const newSkins = typeof updater === 'function' ? updater(skins) : updater; + + const seen: Set = new Set(); + const filteredDupes = newSkins.filter((skin) => { + const key = skin.skin_url || ''; + if (seen.has(key)) + return false; + seen.add(key); + return true; + }); + + saveSkinHistory(filteredDupes); + setSkinsSTATE(filteredDupes); + }; const [capes, setCapes] = useState>([]); const { data: currentAccount } = useCommandSuspense(['getDefaultUser'], () => bindings.core.getDefaultUser(true)); const { data: loggedInUser } = useCommandSuspense(['fetchLoggedInProfile'], () => bindings.core.fetchLoggedInProfile((currentAccount as MinecraftCredentials).access_token)); useEffect(() => { - setSkins( - loggedInUser.skins.map(skin => ({ + async function fetchSkins() { + const skins: Array = loggedInUser.skins.map(skin => ({ is_slim: skin.variant === 'slim', skin_url: skin.url, - })), - ); + })); + (await getSkinHistory()).forEach(skin => skins.push(skin)); + setSkins(skins); + } + + fetchSkins(); setCapes(loggedInUser.capes.map(cape => cape.url)); }, [loggedInUser]); @@ -86,10 +144,6 @@ function RouteComponent() { setSkins([...skins, { is_slim: false, skin_url: url }]); }; - useEffect(() => { - importFromURL('http://textures.minecraft.net/texture/90b8789136facaa9f87b765140e1c8135e6652f513481bd84e6bd8c44844d7ce'); - }, []); - if (currentAccount === null) return ( } headerSmall={<>}> @@ -225,7 +279,8 @@ function RenderSkin({ skin, selected, animation, setSelectedSkin, setSkins }: { const buffer = await response.arrayBuffer(); await writeFile(filePath, new Uint8Array(buffer)); - } catch (error) { + } + catch (error) { console.error(error); } }; @@ -245,16 +300,16 @@ function RenderSkin({ skin, selected, animation, setSelectedSkin, setSkins }: { {selected.skin_url === skin.skin_url ? <> : ( - - - - - setSkins(prev => prev.filter(skinData => skinData.skin_url !== skin.skin_url))} /> - - - )} + + + + + setSkins(prev => prev.filter(skinData => skinData.skin_url !== skin.skin_url))} /> + + + )} +
+ + - - - - + + + + + + + + +
-
+ ); } From 23f05e4d5a335ec6007d4f2a24539b0736a70d94 Mon Sep 17 00:00:00 2001 From: Jacob Date: Tue, 30 Sep 2025 12:17:08 +0800 Subject: [PATCH 08/13] feat: oneclient account skin manager saving skins --- apps/oneclient/frontend/src/bindings.gen.ts | 8 +-- .../frontend/src/routes/app/accountSkin.tsx | 52 ++++++++++++++----- 2 files changed, 42 insertions(+), 18 deletions(-) diff --git a/apps/oneclient/frontend/src/bindings.gen.ts b/apps/oneclient/frontend/src/bindings.gen.ts index 0fe7a20e..0e5517f5 100644 --- a/apps/oneclient/frontend/src/bindings.gen.ts +++ b/apps/oneclient/frontend/src/bindings.gen.ts @@ -252,12 +252,10 @@ export type VersionType = */ "old_beta" -const ARGS_MAP = { 'oneclient':'{"openDevTools":[],"getClustersGroupedByMajor":[],"getBundlesFor":["cluster_id"]}', 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}', 'events':'{"message":["event"],"process":["event"],"ingress":["event"]}', 'core':'{"getUser":["uuid"],"getLoadersForVersion":["mc_version"],"killProcess":["pid"],"removeUser":["uuid"],"createCluster":["options"],"installModpack":["modpack","cluster_id"],"isClusterRunning":["cluster_id"],"searchPackages":["provider","query"],"getDefaultUser":["fallback"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"launchCluster":["id","uuid"],"getWorlds":["id"],"getPackage":["provider","slug"],"open":["input"],"changeSkin":["access_token","skin_url","skin_variant"],"removeCluster":["id"],"getRunningProcessesByClusterId":["cluster_id"],"readSettings":[],"writeSettings":["setting"],"getProfileOrDefault":["name"],"getUsers":[],"getScreenshots":["id"],"getLogs":["id"],"updateClusterProfile":["name","profile"],"getGameVersions":[],"getRunningProcesses":[],"openMsaLogin":[],"setDefaultUser":["uuid"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getClusters":[],"getClusterById":["id"],"getMultiplePackages":["provider","slugs"],"getUsersFromAuthor":["provider","author"],"updateClusterById":["id","request"],"getLogByName":["id","name"],"getPackageBody":["provider","body"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getGlobalProfile":[],"fetchMinecraftProfile":["uuid"],"fetchLoggedInProfile":["access_token"]}' } +const ARGS_MAP = { 'events':'{"message":["event"],"process":["event"],"ingress":["event"]}', 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}', 'oneclient':'{"openDevTools":[],"getClustersGroupedByMajor":[],"getBundlesFor":["cluster_id"]}', 'core':'{"getUser":["uuid"],"getLoadersForVersion":["mc_version"],"killProcess":["pid"],"removeUser":["uuid"],"createCluster":["options"],"installModpack":["modpack","cluster_id"],"isClusterRunning":["cluster_id"],"searchPackages":["provider","query"],"getDefaultUser":["fallback"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"launchCluster":["id","uuid"],"getWorlds":["id"],"getPackage":["provider","slug"],"open":["input"],"changeSkin":["access_token","skin_url","skin_variant"],"removeCluster":["id"],"getRunningProcessesByClusterId":["cluster_id"],"readSettings":[],"writeSettings":["setting"],"getProfileOrDefault":["name"],"getUsers":[],"getScreenshots":["id"],"getLogs":["id"],"updateClusterProfile":["name","profile"],"getGameVersions":[],"getRunningProcesses":[],"openMsaLogin":[],"setDefaultUser":["uuid"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getClusters":[],"getClusterById":["id"],"getMultiplePackages":["provider","slugs"],"getUsersFromAuthor":["provider","author"],"updateClusterById":["id","request"],"getLogByName":["id","name"],"getPackageBody":["provider","body"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getGlobalProfile":[],"fetchMinecraftProfile":["uuid"],"fetchLoggedInProfile":["access_token"]}' } export type Router = { 'events': { ingress: (event: IngressPayload) => Promise, message: (event: MessagePayload) => Promise, process: (event: ProcessPayload) => Promise }, -'folders': { fromCluster: (folderName: string) => Promise, -openCluster: (folderName: string) => Promise }, 'oneclient': { openDevTools: () => Promise, getClustersGroupedByMajor: () => Promise>, getBundlesFor: (clusterId: number) => Promise }, @@ -300,7 +298,9 @@ fetchMinecraftProfile: (uuid: string) => Promise, fetchLoggedInProfile: (accessToken: string) => Promise, uploadSkinBytes: (accessToken: string, skinData: number[], imageFormat: string, skinVariant: SkinVariant) => Promise, changeSkin: (accessToken: string, skinUrl: string, skinVariant: SkinVariant) => Promise, -open: (input: string) => Promise } }; +open: (input: string) => Promise }, +'folders': { fromCluster: (folderName: string) => Promise, +openCluster: (folderName: string) => Promise } }; export type { InferCommandOutput } diff --git a/apps/oneclient/frontend/src/routes/app/accountSkin.tsx b/apps/oneclient/frontend/src/routes/app/accountSkin.tsx index bdd247c8..4dd85c7c 100644 --- a/apps/oneclient/frontend/src/routes/app/accountSkin.tsx +++ b/apps/oneclient/frontend/src/routes/app/accountSkin.tsx @@ -1,11 +1,13 @@ import type { MinecraftCredentials } from '@/bindings.gen'; import type { PlayerAnimation } from 'skinview3d'; import { SheetPage, SkinViewer } from '@/components'; +import { ImportSkinModal, RemoveSkinCapeModal } from '@/components/overlay'; import { Overlay } from '@/components/overlay/Overlay'; import { usePlayerProfile } from '@/hooks/usePlayerProfile'; import { bindings } from '@/main'; import { useCommandSuspense } from '@onelauncher/common'; import { Button } from '@onelauncher/common/components'; +import { useQueryClient } from '@tanstack/react-query'; import { createFileRoute } from '@tanstack/react-router'; import { dataDir, downloadDir, join } from '@tauri-apps/api/path'; import { save } from '@tauri-apps/plugin-dialog'; @@ -14,7 +16,6 @@ import { Download01Icon, PlusIcon, Trash01Icon } from '@untitled-theme/icons-rea import { useEffect, useState } from 'react'; import { DialogTrigger } from 'react-aria-components'; import { CrouchAnimation, FlyingAnimation, HitAnimation, IdleAnimation, WalkingAnimation } from 'skinview3d'; -import { ImportSkinModal, RemoveSkinCapeModal } from '../../components/overlay'; interface Skin { is_slim?: boolean; @@ -74,6 +75,7 @@ async function saveSkinHistory(skins: Array): Promise { } function RouteComponent() { + const queryClient = useQueryClient(); const [skins, setSkinsSTATE] = useState>([]); const setSkins = (updater: Array | ((prev: Array) => Array)) => { const newSkins = typeof updater === 'function' ? updater(skins) : updater; @@ -144,6 +146,28 @@ function RouteComponent() { setSkins([...skins, { is_slim: false, skin_url: url }]); }; + const saveSkin = async () => { + try { + if (!currentAccount) + return; + if (!selectedSkin.skin_url || selectedSkin.is_slim === undefined) + return; + await bindings.core.changeSkin(currentAccount.access_token, selectedSkin.skin_url, selectedSkin.is_slim ? 'slim' : 'classic'); + queryClient.invalidateQueries({ + queryKey: ['getDefaultUser'], + }); + queryClient.invalidateQueries({ + queryKey: ['fetchLoggedInProfile'], + }); + queryClient.invalidateQueries({ + queryKey: ['fetchMinecraftProfile'], + }); + } + catch (error) { + console.error(error); + } + }; + if (currentAccount === null) return ( } headerSmall={<>}> @@ -173,7 +197,7 @@ function RouteComponent() { return ( + )} headerSmall={} > @@ -300,16 +324,16 @@ function RenderSkin({ skin, selected, animation, setSelectedSkin, setSkins }: { {selected.skin_url === skin.skin_url ? <> : ( - - - - - setSkins(prev => prev.filter(skinData => skinData.skin_url !== skin.skin_url))} /> - - - )} + + + + + setSkins(prev => prev.filter(skinData => skinData.skin_url !== skin.skin_url))} /> + + + )}
From 2067c2bcdccb339ddf2fa0ea4ab09eef8e3be14d Mon Sep 17 00:00:00 2001 From: Jacob Date: Tue, 30 Sep 2025 13:15:04 +0800 Subject: [PATCH 09/13] change order of mini account buttons --- apps/oneclient/frontend/src/bindings.gen.ts | 8 ++++---- .../src/components/overlay/AccountPopup.tsx | 20 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/oneclient/frontend/src/bindings.gen.ts b/apps/oneclient/frontend/src/bindings.gen.ts index 0e5517f5..ff26ca44 100644 --- a/apps/oneclient/frontend/src/bindings.gen.ts +++ b/apps/oneclient/frontend/src/bindings.gen.ts @@ -252,10 +252,12 @@ export type VersionType = */ "old_beta" -const ARGS_MAP = { 'events':'{"message":["event"],"process":["event"],"ingress":["event"]}', 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}', 'oneclient':'{"openDevTools":[],"getClustersGroupedByMajor":[],"getBundlesFor":["cluster_id"]}', 'core':'{"getUser":["uuid"],"getLoadersForVersion":["mc_version"],"killProcess":["pid"],"removeUser":["uuid"],"createCluster":["options"],"installModpack":["modpack","cluster_id"],"isClusterRunning":["cluster_id"],"searchPackages":["provider","query"],"getDefaultUser":["fallback"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"launchCluster":["id","uuid"],"getWorlds":["id"],"getPackage":["provider","slug"],"open":["input"],"changeSkin":["access_token","skin_url","skin_variant"],"removeCluster":["id"],"getRunningProcessesByClusterId":["cluster_id"],"readSettings":[],"writeSettings":["setting"],"getProfileOrDefault":["name"],"getUsers":[],"getScreenshots":["id"],"getLogs":["id"],"updateClusterProfile":["name","profile"],"getGameVersions":[],"getRunningProcesses":[],"openMsaLogin":[],"setDefaultUser":["uuid"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getClusters":[],"getClusterById":["id"],"getMultiplePackages":["provider","slugs"],"getUsersFromAuthor":["provider","author"],"updateClusterById":["id","request"],"getLogByName":["id","name"],"getPackageBody":["provider","body"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getGlobalProfile":[],"fetchMinecraftProfile":["uuid"],"fetchLoggedInProfile":["access_token"]}' } +const ARGS_MAP = { 'oneclient':'{"openDevTools":[],"getClustersGroupedByMajor":[],"getBundlesFor":["cluster_id"]}', 'core':'{"getUser":["uuid"],"getLoadersForVersion":["mc_version"],"killProcess":["pid"],"removeUser":["uuid"],"createCluster":["options"],"installModpack":["modpack","cluster_id"],"isClusterRunning":["cluster_id"],"searchPackages":["provider","query"],"getDefaultUser":["fallback"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"launchCluster":["id","uuid"],"getWorlds":["id"],"getPackage":["provider","slug"],"open":["input"],"changeSkin":["access_token","skin_url","skin_variant"],"removeCluster":["id"],"getRunningProcessesByClusterId":["cluster_id"],"readSettings":[],"writeSettings":["setting"],"getProfileOrDefault":["name"],"getUsers":[],"getScreenshots":["id"],"getLogs":["id"],"updateClusterProfile":["name","profile"],"getGameVersions":[],"getRunningProcesses":[],"openMsaLogin":[],"setDefaultUser":["uuid"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getClusters":[],"getClusterById":["id"],"getMultiplePackages":["provider","slugs"],"getUsersFromAuthor":["provider","author"],"updateClusterById":["id","request"],"getLogByName":["id","name"],"getPackageBody":["provider","body"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getGlobalProfile":[],"fetchMinecraftProfile":["uuid"],"fetchLoggedInProfile":["access_token"]}', 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}', 'events':'{"message":["event"],"process":["event"],"ingress":["event"]}' } export type Router = { 'events': { ingress: (event: IngressPayload) => Promise, message: (event: MessagePayload) => Promise, process: (event: ProcessPayload) => Promise }, +'folders': { fromCluster: (folderName: string) => Promise, +openCluster: (folderName: string) => Promise }, 'oneclient': { openDevTools: () => Promise, getClustersGroupedByMajor: () => Promise>, getBundlesFor: (clusterId: number) => Promise }, @@ -298,9 +300,7 @@ fetchMinecraftProfile: (uuid: string) => Promise, fetchLoggedInProfile: (accessToken: string) => Promise, uploadSkinBytes: (accessToken: string, skinData: number[], imageFormat: string, skinVariant: SkinVariant) => Promise, changeSkin: (accessToken: string, skinUrl: string, skinVariant: SkinVariant) => Promise, -open: (input: string) => Promise }, -'folders': { fromCluster: (folderName: string) => Promise, -openCluster: (folderName: string) => Promise } }; +open: (input: string) => Promise } }; export type { InferCommandOutput } diff --git a/apps/oneclient/frontend/src/components/overlay/AccountPopup.tsx b/apps/oneclient/frontend/src/components/overlay/AccountPopup.tsx index 704bfc9f..f02f2f0b 100644 --- a/apps/oneclient/frontend/src/components/overlay/AccountPopup.tsx +++ b/apps/oneclient/frontend/src/components/overlay/AccountPopup.tsx @@ -117,16 +117,6 @@ function AccountEntry({
- - - - - - - - + + + + + + + +
From fedde0a309f4ee594f9e23630511ba312db1b1c0 Mon Sep 17 00:00:00 2001 From: Jacob Date: Tue, 30 Sep 2025 16:10:27 +0800 Subject: [PATCH 10/13] feat: oneclient account skin manager elytra support --- apps/oneclient/frontend/src/bindings.gen.ts | 6 ++-- .../frontend/src/components/SkinViewer.tsx | 10 +++--- .../frontend/src/routes/app/accountSkin.tsx | 33 ++++++++++++++----- 3 files changed, 32 insertions(+), 17 deletions(-) diff --git a/apps/oneclient/frontend/src/bindings.gen.ts b/apps/oneclient/frontend/src/bindings.gen.ts index ff26ca44..fde3b505 100644 --- a/apps/oneclient/frontend/src/bindings.gen.ts +++ b/apps/oneclient/frontend/src/bindings.gen.ts @@ -252,15 +252,15 @@ export type VersionType = */ "old_beta" -const ARGS_MAP = { 'oneclient':'{"openDevTools":[],"getClustersGroupedByMajor":[],"getBundlesFor":["cluster_id"]}', 'core':'{"getUser":["uuid"],"getLoadersForVersion":["mc_version"],"killProcess":["pid"],"removeUser":["uuid"],"createCluster":["options"],"installModpack":["modpack","cluster_id"],"isClusterRunning":["cluster_id"],"searchPackages":["provider","query"],"getDefaultUser":["fallback"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"launchCluster":["id","uuid"],"getWorlds":["id"],"getPackage":["provider","slug"],"open":["input"],"changeSkin":["access_token","skin_url","skin_variant"],"removeCluster":["id"],"getRunningProcessesByClusterId":["cluster_id"],"readSettings":[],"writeSettings":["setting"],"getProfileOrDefault":["name"],"getUsers":[],"getScreenshots":["id"],"getLogs":["id"],"updateClusterProfile":["name","profile"],"getGameVersions":[],"getRunningProcesses":[],"openMsaLogin":[],"setDefaultUser":["uuid"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getClusters":[],"getClusterById":["id"],"getMultiplePackages":["provider","slugs"],"getUsersFromAuthor":["provider","author"],"updateClusterById":["id","request"],"getLogByName":["id","name"],"getPackageBody":["provider","body"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getGlobalProfile":[],"fetchMinecraftProfile":["uuid"],"fetchLoggedInProfile":["access_token"]}', 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}', 'events':'{"message":["event"],"process":["event"],"ingress":["event"]}' } +const ARGS_MAP = { 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}', 'events':'{"message":["event"],"process":["event"],"ingress":["event"]}', 'core':'{"getUser":["uuid"],"getLoadersForVersion":["mc_version"],"killProcess":["pid"],"removeUser":["uuid"],"createCluster":["options"],"installModpack":["modpack","cluster_id"],"isClusterRunning":["cluster_id"],"searchPackages":["provider","query"],"getDefaultUser":["fallback"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"launchCluster":["id","uuid"],"getWorlds":["id"],"getPackage":["provider","slug"],"open":["input"],"changeSkin":["access_token","skin_url","skin_variant"],"removeCluster":["id"],"getRunningProcessesByClusterId":["cluster_id"],"readSettings":[],"writeSettings":["setting"],"getProfileOrDefault":["name"],"getUsers":[],"getScreenshots":["id"],"getLogs":["id"],"updateClusterProfile":["name","profile"],"getGameVersions":[],"getRunningProcesses":[],"openMsaLogin":[],"setDefaultUser":["uuid"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getClusters":[],"getClusterById":["id"],"getMultiplePackages":["provider","slugs"],"getUsersFromAuthor":["provider","author"],"updateClusterById":["id","request"],"getLogByName":["id","name"],"getPackageBody":["provider","body"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getGlobalProfile":[],"fetchMinecraftProfile":["uuid"],"fetchLoggedInProfile":["access_token"]}', 'oneclient':'{"openDevTools":[],"getClustersGroupedByMajor":[],"getBundlesFor":["cluster_id"]}' } export type Router = { 'events': { ingress: (event: IngressPayload) => Promise, message: (event: MessagePayload) => Promise, process: (event: ProcessPayload) => Promise }, -'folders': { fromCluster: (folderName: string) => Promise, -openCluster: (folderName: string) => Promise }, 'oneclient': { openDevTools: () => Promise, getClustersGroupedByMajor: () => Promise>, getBundlesFor: (clusterId: number) => Promise }, +'folders': { fromCluster: (folderName: string) => Promise, +openCluster: (folderName: string) => Promise }, 'core': { getClusters: () => Promise, getClusterById: (id: number) => Promise, removeCluster: (id: number) => Promise, diff --git a/apps/oneclient/frontend/src/components/SkinViewer.tsx b/apps/oneclient/frontend/src/components/SkinViewer.tsx index b2984787..d48d80bd 100644 --- a/apps/oneclient/frontend/src/components/SkinViewer.tsx +++ b/apps/oneclient/frontend/src/components/SkinViewer.tsx @@ -25,9 +25,9 @@ export interface SkinViewerProps { enableZoom?: boolean; enableRotate?: boolean; enablePan?: boolean; + elytra?: boolean; } - const defaultIdleAnimation = new skinviewer.IdleAnimation(); export function SkinViewer({ @@ -52,6 +52,7 @@ export function SkinViewer({ enableZoom = true, enableRotate = true, enablePan = true, + elytra = false }: SkinViewerProps) { const canvasRef = useRef(null); const viewerRef = useRef(null); @@ -96,19 +97,16 @@ export function SkinViewer({ viewerRef.current.loadSkin(getSkinUrl(skinUrl)); }, [skinUrl]); - useEffect(() => { - - }, [capeUrl]); useEffect(() => { if (!viewerRef.current) return; if (capeUrl) - viewerRef.current.loadCape(capeUrl); + viewerRef.current.loadCape(capeUrl, { backEquipment: elytra ? "elytra" : "cape" }); else viewerRef.current.resetCape(); - }, [capeUrl]); + }, [capeUrl, elytra]); useEffect(() => { if (!viewerRef.current) diff --git a/apps/oneclient/frontend/src/routes/app/accountSkin.tsx b/apps/oneclient/frontend/src/routes/app/accountSkin.tsx index 4dd85c7c..7d7c178a 100644 --- a/apps/oneclient/frontend/src/routes/app/accountSkin.tsx +++ b/apps/oneclient/frontend/src/routes/app/accountSkin.tsx @@ -145,6 +145,7 @@ function RouteComponent() { const importFromURL = (url: string) => { setSkins([...skins, { is_slim: false, skin_url: url }]); }; + const [shouldShowElytra, setShouldShowElytra] = useState(false); const saveSkin = async () => { try { @@ -211,7 +212,9 @@ function RouteComponent() {
@@ -222,10 +225,12 @@ function RouteComponent() { @@ -237,6 +242,8 @@ function RouteComponent() { selected={selectedCape} selectedSkin={selectedSkin} setSelectedCape={setSelectedCape} + setShouldShowElytra={() => setShouldShowElytra(!shouldShowElytra)} + shouldShowElytra={shouldShowElytra} />
@@ -247,7 +254,7 @@ function RouteComponent() { ); } -function SkinHistoryRow({ selected, animation, setSelectedSkin, skins, setSkins, importFromURL }: { selected: Skin; animation: PlayerAnimation; setSelectedSkin: (skin: Skin) => void; skins: Array; setSkins: React.Dispatch>>; importFromURL: (url: string) => void }) { +function SkinHistoryRow({ selected, animation, setSelectedSkin, skins, setSkins, importFromURL, capeURL, shouldShowElytra }: { selected: Skin; animation: PlayerAnimation; setSelectedSkin: (skin: Skin) => void; skins: Array; setSkins: React.Dispatch>>; importFromURL: (url: string) => void; capeURL: string; shouldShowElytra: boolean }) { return (
@@ -268,10 +275,12 @@ function SkinHistoryRow({ selected, animation, setSelectedSkin, skins, setSkins, {skins.map(skinData => ( ))} @@ -280,7 +289,7 @@ function SkinHistoryRow({ selected, animation, setSelectedSkin, skins, setSkins, ); } -function RenderSkin({ skin, selected, animation, setSelectedSkin, setSkins }: { skin: Skin; selected: Skin; animation: PlayerAnimation; setSelectedSkin: (skin: Skin) => void; setSkins: React.Dispatch>> }) { +function RenderSkin({ skin, selected, animation, setSelectedSkin, setSkins, capeURL, shouldShowElytra }: { skin: Skin; selected: Skin; animation: PlayerAnimation; setSelectedSkin: (skin: Skin) => void; setSkins: React.Dispatch>>; capeURL: string; shouldShowElytra: boolean }) { const handleSave = async () => { try { if (!skin.skin_url) @@ -316,7 +325,9 @@ function RenderSkin({ skin, selected, animation, setSelectedSkin, setSkins }: { > void; capes: Array }) { +function CapeRow({ selected, selectedSkin, animation, setSelectedCape, capes, shouldShowElytra, setShouldShowElytra }: { selected: string | null; selectedSkin: Skin; animation: PlayerAnimation; setSelectedCape: (cape: string) => void; capes: Array; shouldShowElytra: boolean; setShouldShowElytra: () => void }) { return (
@@ -358,6 +369,7 @@ function CapeRow({ selected, selectedSkin, animation, setSelectedCape, capes }: selected={selected} selectedSkin={selectedSkin} setSelectedCape={setSelectedCape} + shouldShowElytra={shouldShowElytra} /> ))} @@ -365,12 +377,15 @@ function CapeRow({ selected, selectedSkin, animation, setSelectedCape, capes }:

Cape History

+
); } -function RenderCape({ selected, selectedSkin, animation, setSelectedCape, cape }: { selected: string | null; selectedSkin: Skin; animation: PlayerAnimation; setSelectedCape: (cape: string) => void; cape: string }) { +function RenderCape({ selected, selectedSkin, animation, setSelectedCape, cape, shouldShowElytra }: { selected: string | null; selectedSkin: Skin; animation: PlayerAnimation; setSelectedCape: (cape: string) => void; cape: string; shouldShowElytra: boolean }) { return ( - - - setSkins(prev => prev.filter(skinData => skinData.skin_url !== skin.skin_url))} /> - - - )} + + + + + setSkins(prev => prev.filter(skinData => skinData.skin_url !== skin.skin_url))} /> + + + )}
@@ -240,10 +240,10 @@ function RouteComponent() { animation={animation} capes={capes} selected={selectedCape} - selectedSkin={selectedSkin} setSelectedCape={setSelectedCape} setShouldShowElytra={() => setShouldShowElytra(!shouldShowElytra)} shouldShowElytra={shouldShowElytra} + skinURL={selectedSkin.skin_url} />
@@ -290,7 +290,7 @@ function SkinHistoryRow({ selected, animation, setSelectedSkin, skins, setSkins, } function RenderSkin({ skin, selected, animation, setSelectedSkin, setSkins, capeURL, shouldShowElytra }: { skin: Skin; selected: Skin; animation: PlayerAnimation; setSelectedSkin: (skin: Skin) => void; setSkins: React.Dispatch>>; capeURL: string; shouldShowElytra: boolean }) { - const handleSave = async () => { + const exportSkin = async () => { try { if (!skin.skin_url) return; @@ -317,6 +317,8 @@ function RenderSkin({ skin, selected, animation, setSelectedSkin, setSkins, cape console.error(error); } }; + if (!skin.skin_url) + return <>; return ( - - - setSkins(prev => prev.filter(skinData => skinData.skin_url !== skin.skin_url))} /> - - - )} + + + + + setSkins(prev => prev.filter(skinData => skinData.skin_url !== skin.skin_url))} /> + + + )} @@ -430,7 +432,7 @@ function HeaderSmall() { ); } -function Viewer({ skinData, capeURL, height = 400, width = 250, showText = true, animation, enableControls = false, flip = false, shouldShowElytra }: { skinData: Skin; capeURL: string; height?: number; width?: number; showText?: boolean; animation?: PlayerAnimation; enableControls?: boolean; flip?: boolean; shouldShowElytra: boolean }) { +function Viewer({ skinURL, capeURL, height = 400, width = 250, showText = true, animation, enableControls = false, flip = false, shouldShowElytra }: { skinURL: string; capeURL: string; height?: number; width?: number; showText?: boolean; animation?: PlayerAnimation; enableControls?: boolean; flip?: boolean; shouldShowElytra: boolean }) { return ( LauncherResult; + + #[taurpc(alias = "changeCape")] + async fn change_cape( + access_token: String, + cape_uuid: String, + ) -> LauncherResult; + + #[taurpc(alias = "removeCape")] + async fn remove_cape( + access_token: String, + ) -> LauncherResult; // endregion: minecraft // MARK: API: Other @@ -744,6 +755,21 @@ impl TauriLauncherApi for TauriLauncherApiImpl { ) -> LauncherResult { crate::utils::minecraft::change_skin(&access_token, &skin_url, skin_variant).await } + + async fn change_cape( + self, + access_token: String, + cape_uuid: String, + ) -> LauncherResult { + crate::utils::minecraft::change_cape(&access_token, &cape_uuid).await + } + + async fn remove_cape( + self, + access_token: String, + ) -> LauncherResult { + crate::utils::minecraft::remove_cape(&access_token).await + } // endregion: minecraft // MARK: Impl: Other diff --git a/packages/core/src/utils/minecraft.rs b/packages/core/src/utils/minecraft.rs index 9a371bde..e8f7fdda 100644 --- a/packages/core/src/utils/minecraft.rs +++ b/packages/core/src/utils/minecraft.rs @@ -238,3 +238,42 @@ pub async fn change_skin( .cloned() .ok_or_else(|| anyhow::anyhow!("no skins found in response").into()) } + +pub async fn change_cape( + access_token: &str, + cape_uuid: &str, +) -> LauncherResult { + let mut headers: HashMap<&str, &str> = HashMap::with_capacity(1); + + let bearer = &format!("Bearer {access_token}"); + headers.insert("Authorization", bearer); + + http::fetch_json_advanced::( + Method::PUT, + "https://api.minecraftservices.com/minecraft/profile/capes/active", + Some(json!({ + "capeId": cape_uuid, + })), + Some(headers), + None, + None, + ) + .await +} + +pub async fn remove_cape(access_token: &str) -> LauncherResult { + let mut headers: HashMap<&str, &str> = HashMap::with_capacity(1); + + let bearer = &format!("Bearer {access_token}"); + headers.insert("Authorization", bearer); + + http::fetch_json_advanced::( + Method::DELETE, + "https://api.minecraftservices.com/minecraft/profile/capes/active", + None, + Some(headers), + None, + None, + ) + .await +} From c251f17cc96d958e1f572e35041c3acaa69aafe0 Mon Sep 17 00:00:00 2001 From: Jacob Date: Wed, 1 Oct 2025 17:47:45 +0800 Subject: [PATCH 13/13] feat: oneclient account skin manager import from username --- apps/oneclient/frontend/src/bindings.gen.ts | 21 +++++++------ .../components/overlay/ImportSkinModal.tsx | 3 +- .../frontend/src/routes/app/accountSkin.tsx | 30 +++++++++++++++++-- packages/core/src/api/tauri/commands.rs | 12 ++++++++ packages/core/src/utils/minecraft.rs | 20 +++++++++++++ 5 files changed, 74 insertions(+), 12 deletions(-) diff --git a/apps/oneclient/frontend/src/bindings.gen.ts b/apps/oneclient/frontend/src/bindings.gen.ts index 8240a474..1f362a5c 100644 --- a/apps/oneclient/frontend/src/bindings.gen.ts +++ b/apps/oneclient/frontend/src/bindings.gen.ts @@ -114,6 +114,8 @@ export type MojangPlayerProfile = { uuid: string; username: string; is_slim: boo export type MojangSkin = { id: string; state: string; url: string; variant: SkinVariant } +export type MowojangProfile = { id: string; username: string } + export type PackageAuthor = { Team: { team_id: string; org_id: string | null } } | { Users: ManagedUser[] } export type PackageCategories = { Mod: PackageModCategory[] } | { ResourcePack: PackageResourcePackCategory[] } | { Shader: PackageShaderCategory[] } | { DataPack: PackageModCategory[] } | { ModPack: PackageModPackCategory[] } @@ -252,8 +254,14 @@ export type VersionType = */ "old_beta" -const ARGS_MAP = { 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}', 'oneclient':'{"getClustersGroupedByMajor":[],"openDevTools":[],"getBundlesFor":["cluster_id"]}', 'core':'{"changeSkin":["access_token","skin_url","skin_variant"],"getLoadersForVersion":["mc_version"],"getPackageBody":["provider","body"],"getClusters":[],"removeCape":["access_token"],"updateClusterById":["id","request"],"getRunningProcesses":[],"getLogs":["id"],"setDefaultUser":["uuid"],"fetchLoggedInProfile":["access_token"],"getGameVersions":[],"launchCluster":["id","uuid"],"isClusterRunning":["cluster_id"],"removeCluster":["id"],"getWorlds":["id"],"getUsersFromAuthor":["provider","author"],"getRunningProcessesByClusterId":["cluster_id"],"getGlobalProfile":[],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"installModpack":["modpack","cluster_id"],"updateClusterProfile":["name","profile"],"getUsers":[],"writeSettings":["setting"],"getUser":["uuid"],"getLogByName":["id","name"],"createSettingsProfile":["name"],"getScreenshots":["id"],"getMultiplePackages":["provider","slugs"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"getProfileOrDefault":["name"],"killProcess":["pid"],"readSettings":[],"getClusterById":["id"],"removeUser":["uuid"],"getPackage":["provider","slug"],"changeCape":["access_token","cape_uuid"],"createCluster":["options"],"searchPackages":["provider","query"],"openMsaLogin":[],"fetchMinecraftProfile":["uuid"],"getDefaultUser":["fallback"],"open":["input"]}', 'events':'{"message":["event"],"ingress":["event"],"process":["event"]}' } -export type Router = { 'folders': { fromCluster: (folderName: string) => Promise, +const ARGS_MAP = { 'events':'{"ingress":["event"],"message":["event"],"process":["event"]}', 'oneclient':'{"getClustersGroupedByMajor":[],"openDevTools":[],"getBundlesFor":["cluster_id"]}', 'core':'{"updateClusterProfile":["name","profile"],"updateClusterById":["id","request"],"getLogByName":["id","name"],"killProcess":["pid"],"getUsers":[],"getPackage":["provider","slug"],"removeCluster":["id"],"launchCluster":["id","uuid"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getProfileOrDefault":["name"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"changeCape":["access_token","cape_uuid"],"changeSkin":["access_token","skin_url","skin_variant"],"getPackageBody":["provider","body"],"installModpack":["modpack","cluster_id"],"getWorlds":["id"],"removeUser":["uuid"],"convertUsernameUUID":["username_uuid"],"writeSettings":["setting"],"getGameVersions":[],"getClusterById":["id"],"getLoadersForVersion":["mc_version"],"getScreenshots":["id"],"getMultiplePackages":["provider","slugs"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getUsersFromAuthor":["provider","author"],"openMsaLogin":[],"getDefaultUser":["fallback"],"getUser":["uuid"],"getRunningProcessesByClusterId":["cluster_id"],"getGlobalProfile":[],"createSettingsProfile":["name"],"setDefaultUser":["uuid"],"getClusters":[],"removeCape":["access_token"],"searchPackages":["provider","query"],"isClusterRunning":["cluster_id"],"getLogs":["id"],"fetchLoggedInProfile":["access_token"],"open":["input"],"fetchMinecraftProfile":["uuid"],"readSettings":[],"getRunningProcesses":[],"createCluster":["options"]}', 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}' } +export type Router = { 'oneclient': { openDevTools: () => Promise, +getClustersGroupedByMajor: () => Promise>, +getBundlesFor: (clusterId: number) => Promise }, +'events': { ingress: (event: IngressPayload) => Promise, +message: (event: MessagePayload) => Promise, +process: (event: ProcessPayload) => Promise }, +'folders': { fromCluster: (folderName: string) => Promise, openCluster: (folderName: string) => Promise }, 'core': { getClusters: () => Promise, getClusterById: (id: number) => Promise, @@ -297,13 +305,8 @@ uploadSkinBytes: (accessToken: string, skinData: number[], imageFormat: string, changeSkin: (accessToken: string, skinUrl: string, skinVariant: SkinVariant) => Promise, changeCape: (accessToken: string, capeUuid: string) => Promise, removeCape: (accessToken: string) => Promise, -open: (input: string) => Promise }, -'oneclient': { openDevTools: () => Promise, -getClustersGroupedByMajor: () => Promise>, -getBundlesFor: (clusterId: number) => Promise }, -'events': { ingress: (event: IngressPayload) => Promise, -message: (event: MessagePayload) => Promise, -process: (event: ProcessPayload) => Promise } }; +convertUsernameUUID: (usernameUuid: string) => Promise, +open: (input: string) => Promise } }; export type { InferCommandOutput } diff --git a/apps/oneclient/frontend/src/components/overlay/ImportSkinModal.tsx b/apps/oneclient/frontend/src/components/overlay/ImportSkinModal.tsx index a4645757..aa55a0dd 100644 --- a/apps/oneclient/frontend/src/components/overlay/ImportSkinModal.tsx +++ b/apps/oneclient/frontend/src/components/overlay/ImportSkinModal.tsx @@ -2,7 +2,7 @@ import { Button, TextField } from '@onelauncher/common/components'; import { useState } from 'react'; import { Overlay } from './Overlay'; -export function ImportSkinModal({ importFromURL }: { importFromURL: (url: string) => void }) { +export function ImportSkinModal({ importFromURL, importFromUsername }: { importFromURL: (url: string) => void; importFromUsername: (username: string) => void }) { const [input, setInput] = useState(''); return ( @@ -13,6 +13,7 @@ export function ImportSkinModal({ importFromURL }: { importFromURL: (url: string - + {skins.map(skinData => ( diff --git a/packages/core/src/api/tauri/commands.rs b/packages/core/src/api/tauri/commands.rs index 31766464..71877a8f 100644 --- a/packages/core/src/api/tauri/commands.rs +++ b/packages/core/src/api/tauri/commands.rs @@ -233,6 +233,11 @@ pub trait TauriLauncherApi { async fn remove_cape( access_token: String, ) -> LauncherResult; + + #[taurpc(alias = "convertUsernameUUID")] + async fn convert_username_uuid( + username_uuid: String, + ) -> LauncherResult; // endregion: minecraft // MARK: API: Other @@ -770,6 +775,13 @@ impl TauriLauncherApi for TauriLauncherApiImpl { ) -> LauncherResult { crate::utils::minecraft::remove_cape(&access_token).await } + + async fn convert_username_uuid( + self, + username_uuid: String, + ) -> LauncherResult { + crate::utils::minecraft::convert_username_uuid(&username_uuid).await + } // endregion: minecraft // MARK: Impl: Other diff --git a/packages/core/src/utils/minecraft.rs b/packages/core/src/utils/minecraft.rs index e8f7fdda..7bfb27e1 100644 --- a/packages/core/src/utils/minecraft.rs +++ b/packages/core/src/utils/minecraft.rs @@ -277,3 +277,23 @@ pub async fn remove_cape(access_token: &str) -> LauncherResult LauncherResult { + http::fetch_json_advanced::( + Method::GET, + &format!("https://mowojang.matdoes.dev/{username_uuid}"), + None, + None, + None, + None, + ) + .await +}