Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions apps/oneclient/frontend/src/bindings.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand Down Expand Up @@ -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 }
Expand Down
40 changes: 40 additions & 0 deletions apps/oneclient/frontend/src/routes/app/account/route.tsx
Original file line number Diff line number Diff line change
@@ -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>

</>
);
}
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -29,32 +25,22 @@
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');
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<Skin>;
}
Expand All @@ -72,71 +58,75 @@
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 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 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;
});
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 },
];

saveSkinHistory(filteredDupes);
setSkinsSTATE(filteredDupes);
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 { profileData, profile, queryClient } = Route.useRouteContext();

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));
const [selectedCape, setSelectedCape] = useState<string>('');
const [shouldShowElytra, setShouldShowElytra] = useState<boolean>(false);

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);
}
setCapes([{ url: '', id: '' }, ...profileData.capes.map(cape => ({ url: cape.url, id: cape.id }))]);
}, []);

Check warning on line 114 in apps/oneclient/frontend/src/routes/app/account/skins.tsx

View workflow job for this annotation

GitHub Actions / ES Checks

React Hook useEffect has a missing dependency: 'profileData.capes'. Either include it or remove the dependency array

fetchSkins();
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) };

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)
if (!loaded)
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>('');
setSkins(prev => [...prev, skinData]); // ✅ always merges with latest
setSelectedSkin(skinData);
}, [loaded]);

Check warning on line 126 in apps/oneclient/frontend/src/routes/app/account/skins.tsx

View workflow job for this annotation

GitHub Actions / ES Checks

React Hook useEffect has missing dependencies: 'setSkins' and 'skinData'. Either include them or remove the dependency array

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) => {
Expand All @@ -153,30 +143,27 @@
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'],
Expand All @@ -193,14 +180,8 @@
}
};

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);
Expand All @@ -219,13 +200,10 @@
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 />}
>
Expand Down Expand Up @@ -282,7 +260,7 @@
);
}

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">
Expand Down Expand Up @@ -317,7 +295,7 @@
);
}

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)
Expand Down
31 changes: 20 additions & 11 deletions apps/oneclient/frontend/src/routes/app/accounts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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(
Expand All @@ -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" />
Expand Down
Loading