fix: oneclient account skin manager force reloading (#382)
* fix: oneclient account skin manager reloading * fix: formatting
This commit is contained in:
parent
59e7cde8df
commit
4643994cdf
4 changed files with 138 additions and 111 deletions
|
|
@ -254,8 +254,10 @@ export type VersionType =
|
|||
*/
|
||||
"old_beta"
|
||||
|
||||
const ARGS_MAP = { 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}', 'events':'{"process":["event"],"ingress":["event"],"message":["event"]}', 'oneclient':'{"openDevTools":[],"getClustersGroupedByMajor":[],"getBundlesFor":["cluster_id"]}', 'core':'{"updateClusterById":["id","request"],"getRunningProcesses":[],"readSettings":[],"changeSkin":["access_token","skin_url","skin_variant"],"fetchMinecraftProfile":["uuid"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"searchPackages":["provider","query"],"getClusters":[],"getPackageBody":["provider","body"],"killProcess":["pid"],"createSettingsProfile":["name"],"installModpack":["modpack","cluster_id"],"getMultiplePackages":["provider","slugs"],"getUsers":[],"open":["input"],"getUsersFromAuthor":["provider","author"],"getUser":["uuid"],"launchCluster":["id","uuid"],"removeUser":["uuid"],"getLogByName":["id","name"],"getPackage":["provider","slug"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"removeCape":["access_token"],"writeSettings":["setting"],"getClusterById":["id"],"getLoadersForVersion":["mc_version"],"openMsaLogin":[],"updateClusterProfile":["name","profile"],"isClusterRunning":["cluster_id"],"getScreenshots":["id"],"getGlobalProfile":[],"createCluster":["options"],"getLogs":["id"],"getRunningProcessesByClusterId":["cluster_id"],"getProfileOrDefault":["name"],"getGameVersions":[],"getWorlds":["id"],"getDefaultUser":["fallback"],"setDefaultUser":["uuid"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"fetchLoggedInProfile":["access_token"],"changeCape":["access_token","cape_uuid"],"removeCluster":["id"],"convertUsernameUUID":["username_uuid"]}' }
|
||||
export type Router = { 'core': { getClusters: () => Promise<ClusterModel[]>,
|
||||
const ARGS_MAP = { 'oneclient':'{"openDevTools":[],"getClustersGroupedByMajor":[],"getBundlesFor":["cluster_id"]}', 'events':'{"process":["event"],"ingress":["event"],"message":["event"]}', 'core':'{"updateClusterById":["id","request"],"getRunningProcesses":[],"readSettings":[],"changeSkin":["access_token","skin_url","skin_variant"],"fetchMinecraftProfile":["uuid"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"searchPackages":["provider","query"],"getClusters":[],"getPackageBody":["provider","body"],"killProcess":["pid"],"createSettingsProfile":["name"],"installModpack":["modpack","cluster_id"],"getMultiplePackages":["provider","slugs"],"getUsers":[],"open":["input"],"getUsersFromAuthor":["provider","author"],"getUser":["uuid"],"launchCluster":["id","uuid"],"removeUser":["uuid"],"getLogByName":["id","name"],"getPackage":["provider","slug"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"removeCape":["access_token"],"writeSettings":["setting"],"getClusterById":["id"],"getLoadersForVersion":["mc_version"],"openMsaLogin":[],"updateClusterProfile":["name","profile"],"isClusterRunning":["cluster_id"],"getScreenshots":["id"],"getGlobalProfile":[],"createCluster":["options"],"getLogs":["id"],"getRunningProcessesByClusterId":["cluster_id"],"getProfileOrDefault":["name"],"getGameVersions":[],"getWorlds":["id"],"getDefaultUser":["fallback"],"setDefaultUser":["uuid"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"fetchLoggedInProfile":["access_token"],"changeCape":["access_token","cape_uuid"],"removeCluster":["id"],"convertUsernameUUID":["username_uuid"]}', 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}' }
|
||||
export type Router = { 'folders': { fromCluster: (folderName: string) => Promise<string>,
|
||||
openCluster: (folderName: string) => Promise<null> },
|
||||
'core': { getClusters: () => Promise<ClusterModel[]>,
|
||||
getClusterById: (id: number) => Promise<ClusterModel | null>,
|
||||
removeCluster: (id: number) => Promise<null>,
|
||||
createCluster: (options: CreateCluster) => Promise<ClusterModel>,
|
||||
|
|
@ -304,9 +306,7 @@ getClustersGroupedByMajor: () => Promise<Partial<{ [key in number]: ClusterModel
|
|||
getBundlesFor: (clusterId: number) => Promise<ModpackArchive[]> },
|
||||
'events': { ingress: (event: IngressPayload) => Promise<void>,
|
||||
message: (event: MessagePayload) => Promise<void>,
|
||||
process: (event: ProcessPayload) => Promise<void> },
|
||||
'folders': { fromCluster: (folderName: string) => Promise<string>,
|
||||
openCluster: (folderName: string) => Promise<null> } };
|
||||
process: (event: ProcessPayload) => Promise<void> } };
|
||||
|
||||
|
||||
export type { InferCommandOutput }
|
||||
|
|
|
|||
40
apps/oneclient/frontend/src/routes/app/account/route.tsx
Normal file
40
apps/oneclient/frontend/src/routes/app/account/route.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import type { MinecraftCredentials } from '@/bindings.gen';
|
||||
import { LoaderSuspense } from '@/components';
|
||||
import { bindings } from '@/main';
|
||||
import { createFileRoute, Outlet } from '@tanstack/react-router';
|
||||
|
||||
export interface AccountsRouteSearchParams {
|
||||
profile: MinecraftCredentials;
|
||||
}
|
||||
|
||||
export const Route = createFileRoute('/app/account')({
|
||||
component: RouteComponent,
|
||||
validateSearch: (search): AccountsRouteSearchParams => {
|
||||
return {
|
||||
profile: search.profile as MinecraftCredentials,
|
||||
};
|
||||
},
|
||||
async beforeLoad({ context, search }) {
|
||||
const query = context.queryClient.ensureQueryData({
|
||||
queryKey: ['fetchLoggedInProfile', search.profile.access_token],
|
||||
queryFn: () => bindings.core.fetchLoggedInProfile(search.profile.access_token),
|
||||
});
|
||||
|
||||
const profileData = await query;
|
||||
return {
|
||||
profileData,
|
||||
profile: search.profile,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return (
|
||||
<>
|
||||
<LoaderSuspense spinner={{ size: 'large' }}>
|
||||
<Outlet />
|
||||
</LoaderSuspense>
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,15 +1,11 @@
|
|||
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 { getSkinUrl } from '@/utils/minecraft';
|
||||
import { toast } from '@/utils/toast';
|
||||
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';
|
||||
|
|
@ -29,18 +25,10 @@ interface Cape {
|
|||
id: string;
|
||||
}
|
||||
|
||||
export const Route = createFileRoute('/app/accountSkin')({
|
||||
export const Route = createFileRoute('/app/account/skins')({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
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 },
|
||||
];
|
||||
|
||||
async function getSkinHistory(): Promise<Array<Skin>> {
|
||||
const parentDir = await join(await dataDir(), 'OneClient', 'metadata', 'history');
|
||||
const skinsPath = await join(parentDir, 'skins.json');
|
||||
|
|
@ -48,13 +36,11 @@ async function getSkinHistory(): Promise<Array<Skin>> {
|
|||
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<Skin>;
|
||||
}
|
||||
|
|
@ -72,7 +58,6 @@ async function saveSkinHistory(skins: Array<Skin>): Promise<void> {
|
|||
const dirExists = await exists(parentDir);
|
||||
if (!dirExists)
|
||||
await mkdir(parentDir, { recursive: true });
|
||||
|
||||
await writeTextFile(skinsPath, JSON.stringify(skins));
|
||||
}
|
||||
catch (error) {
|
||||
|
|
@ -80,63 +65,68 @@ async function saveSkinHistory(skins: Array<Skin>): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
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 useSkinHistory() {
|
||||
const [skins, setSkinsState] = useState<Array<Skin>>([]);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const history = await getSkinHistory();
|
||||
setSkinsState(history);
|
||||
setLoaded(true);
|
||||
})();
|
||||
}, []);
|
||||
const setSkins = (updater: (prev: Array<Skin>) => Array<Skin>) => {
|
||||
setSkinsState((prev) => {
|
||||
const newSkins = updater(prev);
|
||||
const dedupedSkins: Array<Skin> = [];
|
||||
const seen: Set<string> = new Set();
|
||||
for (const skin of newSkins)
|
||||
if (!seen.has(skin.skin_url)) {
|
||||
seen.add(skin.skin_url);
|
||||
dedupedSkins.push(skin);
|
||||
}
|
||||
|
||||
saveSkinHistory(dedupedSkins);
|
||||
return dedupedSkins;
|
||||
});
|
||||
};
|
||||
|
||||
return [skins, setSkins, loaded] as const;
|
||||
}
|
||||
|
||||
function RouteComponent() {
|
||||
const queryClient = useQueryClient();
|
||||
const [skins, setSkinsSTATE] = useState<Array<Skin>>([]);
|
||||
const setSkins = (updater: Array<Skin> | ((prev: Array<Skin>) => Array<Skin>)) => {
|
||||
const newSkins = typeof updater === 'function' ? updater(skins) : updater;
|
||||
const { profileData, profile, queryClient } = Route.useRouteContext();
|
||||
|
||||
const seen: Set<string> = 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<Array<Cape>>([]);
|
||||
const { data: currentAccount } = useCommandSuspense(['getDefaultUser'], () => bindings.core.getDefaultUser(true));
|
||||
const { data: loggedInUser } = useCommandSuspense(['fetchLoggedInProfile'], () => bindings.core.fetchLoggedInProfile((currentAccount as MinecraftCredentials).access_token));
|
||||
useEffect(() => {
|
||||
async function fetchSkins() {
|
||||
const skins: Array<Skin> = loggedInUser.skins.map(skin => ({
|
||||
is_slim: skin.variant === 'slim',
|
||||
skin_url: skin.url,
|
||||
}));
|
||||
(await getSkinHistory()).forEach(skin => skins.push(skin));
|
||||
setSkins(skins);
|
||||
}
|
||||
|
||||
fetchSkins();
|
||||
|
||||
setCapes([{ url: '', id: '' }, ...loggedInUser.capes.map(cape => ({ url: cape.url, id: cape.id }))]);
|
||||
}, [loggedInUser]);
|
||||
const { data: profile } = usePlayerProfile(currentAccount?.id);
|
||||
const [animation, setAnimation] = useState<PlayerAnimation>(animations[0].animation);
|
||||
const [animationName, setAnimationName] = useState<string>(animations[0].name);
|
||||
const skinData: Skin = {
|
||||
is_slim: profile?.is_slim ?? false,
|
||||
skin_url: getSkinUrl(profile?.skin_url),
|
||||
};
|
||||
const [selectedSkin, setSelectedSkin] = useState<Skin>(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, setSelectedCape] = useState<string>('');
|
||||
const [shouldShowElytra, setShouldShowElytra] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
setCapes([{ url: '', id: '' }, ...profileData.capes.map(cape => ({ url: cape.url, id: cape.id }))]);
|
||||
}, []);
|
||||
|
||||
const [skins, setSkins, loaded] = useSkinHistory();
|
||||
const [selectedSkin, setSelectedSkin] = useState<Skin>({ skin_url: getSkinUrl(null), is_slim: false });
|
||||
const skinData: Skin = { is_slim: profileData.skins[0].variant === 'slim', skin_url: getSkinUrl(profileData.skins[0].url) };
|
||||
|
||||
useEffect(() => {
|
||||
if (!loaded)
|
||||
return;
|
||||
|
||||
setSkins(prev => [...prev, skinData]); // ✅ always merges with latest
|
||||
setSelectedSkin(skinData);
|
||||
}, [loaded]);
|
||||
|
||||
const importFromURL = (url: string) => {
|
||||
setSkins([...skins, { is_slim: false, skin_url: url }]);
|
||||
setSkins(prev => [...prev, { is_slim: false, skin_url: url }]);
|
||||
};
|
||||
|
||||
const importFromUsername = async (username: string) => {
|
||||
|
|
@ -153,30 +143,27 @@ function RouteComponent() {
|
|||
message: `${username} doesn't exist`,
|
||||
});
|
||||
const playerProfile = await bindings.core.fetchMinecraftProfile(id);
|
||||
if (playerProfile.skin_url)
|
||||
setSkins([...skins, { is_slim: playerProfile.is_slim, skin_url: playerProfile.skin_url }]);
|
||||
toast({
|
||||
type: 'success',
|
||||
title: 'Import Skin',
|
||||
message: `Imported skin from ${username}`,
|
||||
});
|
||||
if (playerProfile.skin_url) {
|
||||
setSkins(prev => [...prev, { is_slim: playerProfile.is_slim, skin_url: getSkinUrl(playerProfile.skin_url) }]);
|
||||
toast({
|
||||
type: 'success',
|
||||
title: 'Import Skin',
|
||||
message: `Imported skin from ${username}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const [shouldShowElytra, setShouldShowElytra] = useState<boolean>(false);
|
||||
|
||||
const saveSkinToAccount = async () => {
|
||||
try {
|
||||
if (!currentAccount)
|
||||
return;
|
||||
await bindings.core.changeSkin(currentAccount.access_token, selectedSkin.skin_url, selectedSkin.is_slim ? 'slim' : 'classic');
|
||||
await bindings.core.changeSkin(profile.access_token, selectedSkin.skin_url, selectedSkin.is_slim ? 'slim' : 'classic');
|
||||
if (selectedCape === '') {
|
||||
await bindings.core.removeCape(currentAccount.access_token);
|
||||
await bindings.core.removeCape(profile.access_token);
|
||||
}
|
||||
else {
|
||||
const capeData = capes.find(cape => cape.url === selectedCape);
|
||||
if (!capeData)
|
||||
return;
|
||||
await bindings.core.changeCape(currentAccount.access_token, capeData.id);
|
||||
await bindings.core.changeCape(profile.access_token, capeData.id);
|
||||
}
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['getDefaultUser'],
|
||||
|
|
@ -193,14 +180,8 @@ function RouteComponent() {
|
|||
}
|
||||
};
|
||||
|
||||
if (currentAccount === null)
|
||||
return (
|
||||
<SheetPage headerLarge={<></>} headerSmall={<></>}>
|
||||
<SheetPage.Content>
|
||||
<p>No accounts added</p>
|
||||
</SheetPage.Content>
|
||||
</SheetPage>
|
||||
);
|
||||
const [animation, setAnimation] = useState<PlayerAnimation>(animations[0].animation);
|
||||
const [animationName, setAnimationName] = useState<string>(animations[0].name);
|
||||
|
||||
const getNextAnimationData = () => {
|
||||
const animationIndex = animations.findIndex(animationData => animationData.name === animationName);
|
||||
|
|
@ -219,13 +200,10 @@ function RouteComponent() {
|
|||
setAnimationName(data.name);
|
||||
};
|
||||
|
||||
if (!selectedSkin.skin_url)
|
||||
return <></>;
|
||||
|
||||
return (
|
||||
<SheetPage
|
||||
headerLarge={(
|
||||
<HeaderLarge save={saveSkinToAccount} username={profile?.username || 'UNKNOWN'} />
|
||||
<HeaderLarge save={saveSkinToAccount} username={profileData.username || 'UNKNOWN'} />
|
||||
)}
|
||||
headerSmall={<HeaderSmall />}
|
||||
>
|
||||
|
|
@ -282,7 +260,7 @@ function RouteComponent() {
|
|||
);
|
||||
}
|
||||
|
||||
function SkinHistoryRow({ selected, animation, setSelectedSkin, skins, setSkins, importFromURL, importFromUsername, capeURL, shouldShowElytra }: { selected: Skin; animation: PlayerAnimation; setSelectedSkin: (skin: Skin) => void; skins: Array<Skin>; setSkins: React.Dispatch<React.SetStateAction<Array<Skin>>>; importFromURL: (url: string) => void; importFromUsername: (username: string) => void; capeURL: string; shouldShowElytra: boolean }) {
|
||||
function SkinHistoryRow({ selected, animation, setSelectedSkin, skins, setSkins, importFromURL, importFromUsername, capeURL, shouldShowElytra }: { selected: Skin; animation: PlayerAnimation; setSelectedSkin: (skin: Skin) => void; skins: Array<Skin>; setSkins: (updater: (prev: Array<Skin>) => Array<Skin>) => void; importFromURL: (url: string) => void; importFromUsername: (username: string) => void; capeURL: string; shouldShowElytra: boolean }) {
|
||||
return (
|
||||
<div className="flex flex-col h-full justify-around">
|
||||
<div className="flex flex-col justify-center items-center">
|
||||
|
|
@ -317,7 +295,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<React.SetStateAction<Array<Skin>>>; capeURL: string; shouldShowElytra: boolean }) {
|
||||
function RenderSkin({ skin, selected, animation, setSelectedSkin, setSkins, capeURL, shouldShowElytra }: { skin: Skin; selected: Skin; animation: PlayerAnimation; setSelectedSkin: (skin: Skin) => void; setSkins: (updater: (prev: Array<Skin>) => Array<Skin>) => void; capeURL: string; shouldShowElytra: boolean }) {
|
||||
const exportSkin = async () => {
|
||||
try {
|
||||
if (!skin.skin_url)
|
||||
|
|
@ -10,6 +10,7 @@ import { Button } from '@onelauncher/common/components';
|
|||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { createFileRoute, Link } from '@tanstack/react-router';
|
||||
import { Pencil01Icon, Trash01Icon } from '@untitled-theme/icons-react';
|
||||
import { useCallback } from 'react';
|
||||
import { Button as AriaButton, DialogTrigger } from 'react-aria-components';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
|
|
@ -129,6 +130,16 @@ function AccountRow({
|
|||
}) {
|
||||
const { isError } = usePlayerProfile(profile.id);
|
||||
|
||||
const navigate = Route.useNavigate();
|
||||
const manageSkin = useCallback(() => {
|
||||
navigate({
|
||||
to: `/app/account/skins`,
|
||||
search: {
|
||||
profile,
|
||||
},
|
||||
});
|
||||
}, [profile, navigate]);
|
||||
|
||||
return (
|
||||
<AriaButton
|
||||
className={twMerge(
|
||||
|
|
@ -153,17 +164,15 @@ function AccountRow({
|
|||
</div>
|
||||
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Link to="/app/accountSkin">
|
||||
{/* TODO: Find a better way to handle handle a user that isn't just changing the default user */}
|
||||
<Button
|
||||
className="group w-8 h-8"
|
||||
color="ghost"
|
||||
onPress={() => onPress()}
|
||||
size="icon"
|
||||
>
|
||||
<Pencil01Icon className="group-hover:stroke-brand-hover" />
|
||||
</Button>
|
||||
</Link>
|
||||
<Button
|
||||
className="group w-8 h-8"
|
||||
color="ghost"
|
||||
onPress={manageSkin}
|
||||
size="icon"
|
||||
>
|
||||
<Pencil01Icon className="group-hover:stroke-brand-hover" />
|
||||
</Button>
|
||||
|
||||
<DialogTrigger>
|
||||
<Button className="group w-8 h-8" color="ghost" size="icon">
|
||||
<Trash01Icon className="group-hover:stroke-danger" />
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue