fix mod cards, removing mods

This commit is contained in:
Wyvest 2025-12-25 21:18:07 +09:00
parent 6dc99149fb
commit 403a9f4be8
6 changed files with 154 additions and 25 deletions

View file

@ -301,8 +301,13 @@ export type VersionType =
*/
"old_beta"
const ARGS_MAP = { 'core':'{"getLogByName":["id","name"],"createSettingsProfile":["name"],"searchPackages":["provider","query"],"downloadExternalPackage":["package","cluster_id","force","skip_compatibility"],"setDefaultUser":["uuid"],"writeSettings":["setting"],"updateClusterById":["id","request"],"getProfileOrDefault":["name"],"getDefaultUser":["fallback"],"getClusters":[],"getUsers":[],"createCluster":["options"],"removeCape":["access_token"],"launchCluster":["id","uuid"],"getLinkedPackages":["cluster_id"],"setDiscordRPCMessage":["message"],"getUsersFromAuthor":["provider","author"],"getPackage":["provider","slug"],"getWorlds":["id"],"getMultiplePackages":["provider","slugs"],"getLogs":["id"],"fetchMinecraftProfile":["uuid"],"installModpack":["modpack","cluster_id"],"convertUsernameUUID":["username_uuid"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"killProcess":["pid"],"openMsaLogin":[],"removeUser":["uuid"],"getPackageBody":["provider","body"],"getClusterById":["id"],"getRunningProcessesByClusterId":["cluster_id"],"isClusterRunning":["cluster_id"],"getGameVersions":[],"readSettings":[],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getGlobalProfile":[],"fetchLoggedInProfile":["access_token"],"changeCape":["access_token","cape_uuid"],"getLoadersForVersion":["mc_version"],"getUser":["uuid"],"getRunningProcesses":[],"getScreenshots":["id"],"updateClusterProfile":["name","profile"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"open":["input"],"removeCluster":["id"],"changeSkin":["access_token","skin_url","skin_variant"]}', 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}', 'events':'{"ingress":["event"],"message":["event"],"process":["event"]}', 'debug':'{"getPackageVersion":[],"getFamily":[],"getType":[],"getPlatform":[],"getGitCommitHash":[],"getLocale":[],"getArch":[],"getBuildTimestamp":[],"openDevTools":[],"isInDev":[],"getOsVersion":[]}', 'oneclient':'{"getVersions":[],"getClustersGroupedByMajor":[],"getBundlesFor":["cluster_id"]}' }
export type Router = { 'core': { getClusters: () => Promise<ClusterModel[]>,
const ARGS_MAP = { 'core':'{"launchCluster":["id","uuid"],"getGlobalProfile":[],"setDefaultUser":["uuid"],"getPackage":["provider","slug"],"downloadExternalPackage":["package","cluster_id","force","skip_compatibility"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getPackageBody":["provider","body"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"writeSettings":["setting"],"removePackage":["cluster_id","package_hash"],"open":["input"],"killProcess":["pid"],"getLinkedPackages":["cluster_id"],"getLogs":["id"],"setDiscordRPCMessage":["message"],"removeUser":["uuid"],"readSettings":[],"getDefaultUser":["fallback"],"getLogByName":["id","name"],"openMsaLogin":[],"getMultiplePackages":["provider","slugs"],"getGameVersions":[],"createSettingsProfile":["name"],"updateClusterById":["id","request"],"changeSkin":["access_token","skin_url","skin_variant"],"getLoadersForVersion":["mc_version"],"isClusterRunning":["cluster_id"],"getRunningProcesses":[],"getProfileOrDefault":["name"],"createCluster":["options"],"getClusters":[],"getClusterById":["id"],"getScreenshots":["id"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"removeCluster":["id"],"getUsers":[],"changeCape":["access_token","cape_uuid"],"getWorlds":["id"],"searchPackages":["provider","query"],"removeCape":["access_token"],"fetchLoggedInProfile":["access_token"],"convertUsernameUUID":["username_uuid"],"getUser":["uuid"],"updateClusterProfile":["name","profile"],"getUsersFromAuthor":["provider","author"],"installModpack":["modpack","cluster_id"],"fetchMinecraftProfile":["uuid"],"getRunningProcessesByClusterId":["cluster_id"]}', 'events':'{"message":["event"],"process":["event"],"ingress":["event"]}', 'oneclient':'{"getVersions":[],"getClustersGroupedByMajor":[],"getBundlesFor":["cluster_id"]}', 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}', 'debug':'{"getOsVersion":[],"getArch":[],"getPackageVersion":[],"getBuildTimestamp":[],"getFamily":[],"getLocale":[],"getPlatform":[],"getType":[],"isInDev":[],"getGitCommitHash":[],"openDevTools":[]}' }
export type Router = { 'folders': { fromCluster: (folderName: string) => Promise<string>,
openCluster: (folderName: string) => Promise<null> },
'oneclient': { getClustersGroupedByMajor: () => Promise<Partial<{ [key in number]: ClusterModel[] }>>,
getBundlesFor: (clusterId: number) => Promise<ModpackArchive[]>,
getVersions: () => Promise<OnlineClusterManifest> },
'core': { getClusters: () => Promise<ClusterModel[]>,
getClusterById: (id: number) => Promise<ClusterModel | null>,
removeCluster: (id: number) => Promise<null>,
createCluster: (options: CreateCluster) => Promise<ClusterModel>,
@ -339,6 +344,7 @@ downloadPackage: (provider: Provider, packageId: string, versionId: string, clus
downloadExternalPackage: (package: ExternalPackage, clusterId: number, force: boolean | null, skipCompatibility: boolean | null) => Promise<PackageModel | null>,
getUsersFromAuthor: (provider: Provider, author: PackageAuthor) => Promise<ManagedUser[]>,
getLinkedPackages: (clusterId: number) => Promise<PackageModel[]>,
removePackage: (clusterId: number, packageHash: string) => Promise<null>,
installModpack: (modpack: ModpackArchive, clusterId: number) => Promise<null>,
fetchMinecraftProfile: (uuid: string) => Promise<MojangPlayerProfile>,
fetchLoggedInProfile: (accessToken: string) => Promise<MojangFullPlayerProfile>,
@ -349,11 +355,6 @@ removeCape: (accessToken: string) => Promise<MojangFullPlayerProfile>,
convertUsernameUUID: (usernameUuid: string) => Promise<MowojangProfile>,
setDiscordRPCMessage: (message: string) => Promise<null>,
open: (input: string) => Promise<null> },
'oneclient': { getClustersGroupedByMajor: () => Promise<Partial<{ [key in number]: ClusterModel[] }>>,
getBundlesFor: (clusterId: number) => Promise<ModpackArchive[]>,
getVersions: () => Promise<OnlineClusterManifest> },
'folders': { fromCluster: (folderName: string) => Promise<string>,
openCluster: (folderName: string) => Promise<null> },
'debug': { openDevTools: () => Promise<void>,
isInDev: () => Promise<boolean>,
getArch: () => Promise<string>,

View file

@ -1,9 +1,11 @@
import type { ClusterModel, ModpackFile, ModpackFileKind } from '@/bindings.gen';
import type { ClusterModel, ModpackFile, ModpackFileKind, PackageModel } from '@/bindings.gen';
import MissingLogo from '@/assets/misc/missingLogo.svg';
import { DownloadModButton, ModTag } from '@/components';
import { ModTag } from '@/components';
import { useSettings } from '@/hooks/useSettings';
import { bindings } from '@/main';
import { createContext, useContext, useEffect, useState } from 'react';
import { useCommandMut } from '@onelauncher/common';
import { useQueryClient } from '@tanstack/react-query';
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { twMerge } from 'tailwind-merge';
export interface ModInfo {
@ -84,10 +86,11 @@ interface ModCardProps {
export type onClickOnMod = (file: ModpackFile) => void;
export interface ModCardContextApi {
showModDownloadButton?: boolean;
enableClickToDownload?: boolean;
onClickOnMod?: onClickOnMod;
useVerticalGridLayout?: boolean;
mods?: Array<ModpackFile>;
installedPackages?: Array<PackageModel>;
}
export const ModCardContext = createContext<ModCardContextApi | null>(null);
@ -101,20 +104,72 @@ export function useModCardContext() {
}
export function ModCard({ file, cluster }: ModCardProps) {
const { showModDownloadButton, onClickOnMod, useVerticalGridLayout, mods } = useModCardContext();
const { enableClickToDownload, onClickOnMod, useVerticalGridLayout, mods, installedPackages } = useModCardContext();
const queryClient = useQueryClient();
const [modMetadata, setModMetadata] = useState<ModInfo>({ name: 'Loading...', description: null, author: null, iconURL: null, url: null, managed: false, packageSlug: null });
useEffect(() => {
(async () => setModMetadata(await getModMetaData(file, useVerticalGridLayout)))();
}, [file, useVerticalGridLayout]);
const [isSelected, setSelected] = useState(mods?.includes(file) ?? false);
const kind = file.kind;
const isInstalled = useMemo(() => {
if (installedPackages)
if ('Managed' in kind) {
const [pkg, _] = kind.Managed;
return installedPackages.some(p => p.package_id === pkg.id && p.provider === pkg.provider);
}
else {
return installedPackages.some(p => p.hash === kind.External.sha1);
}
return mods?.includes(file) ?? false;
}, [installedPackages, mods, file]);
const [isSelected, setSelected] = useState(isInstalled);
useEffect(() => {
setSelected(mods?.includes(file) ?? false);
}, [mods, file]);
setSelected(isInstalled);
}, [isInstalled]);
const download = useCommandMut(async () => {
if ('Managed' in kind) {
const [pkg, version] = kind.Managed;
await bindings.core.downloadPackage(pkg.provider, pkg.id, version.version_id, cluster.id, null);
}
else {
await bindings.core.downloadExternalPackage(kind.External, cluster.id, null, null);
}
});
const remove = useCommandMut(async () => {
let hash: string | undefined;
if ('Managed' in kind) {
const [_, version] = kind.Managed;
const primary = version.files.find(f => f.primary) ?? version.files[0];
hash = primary?.sha1;
}
else {
hash = kind.External.sha1;
}
if (hash)
await bindings.core.removePackage(cluster.id, hash);
});
const handleOnClick = () => {
if (onClickOnMod)
if (enableClickToDownload)
(async () => {
if (isInstalled)
await remove.mutateAsync();
else
await download.mutateAsync();
await queryClient.invalidateQueries({ queryKey: ['getLinkedPackages', cluster.id] });
})();
else if (onClickOnMod)
onClickOnMod(file);
};
@ -146,12 +201,6 @@ export function ModCard({ file, cluster }: ModCardProps) {
</div>
</div>
{useVerticalGridLayout === true && modMetadata.description !== null && <p className={twMerge('font-normal text-fg-secondary', useGridLayout ? 'text-sm' : 'text-base')}>{modMetadata.description}</p>}
{showModDownloadButton === true && (
<div className={twMerge('flex flex-col items-center justify-center', useGridLayout ? '' : 'pr-2')}>
<DownloadModButton cluster={cluster} file={file} />
</div>
)}
</div>
);
}

View file

@ -13,10 +13,12 @@ export const Route = createFileRoute('/app/cluster/mods')({
function RouteComponent() {
const { cluster } = Route.useRouteContext();
const { data: bundles } = useCommandSuspense(['getBundlesFor', cluster.id], () => bindings.oneclient.getBundlesFor(cluster.id));
const { data: installedPackages } = useCommandSuspense(['getLinkedPackages', cluster.id], () => bindings.core.getLinkedPackages(cluster.id));
const context = useMemo<ModCardContextApi>(() => ({
showModDownloadButton: true,
}), []);
enableClickToDownload: true,
installedPackages,
}), [installedPackages]);
if (bundles.length === 0)
return <p>No bundles found {cluster.name}</p>;

View file

@ -2,7 +2,8 @@ use onelauncher_entity::loader::GameLoader;
use onelauncher_entity::package::PackageType;
use onelauncher_entity::{cluster_packages, clusters, packages};
use sea_orm::{
ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, TryInsertResult,
ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, PaginatorTrait, QueryFilter,
TryInsertResult,
};
use crate::error::{DaoError, LauncherResult};
@ -105,6 +106,34 @@ pub async fn is_package_linked_to_cluster(
Ok(linked_packages.is_some())
}
/// Checks if a package is linked to any cluster.
pub async fn is_package_used(package_hash: &str) -> LauncherResult<bool> {
let state = State::get().await?;
let db = &state.db;
let count = cluster_packages::Entity::find()
.filter(cluster_packages::Column::PackageHash.eq(package_hash))
.count(db)
.await?;
Ok(count > 0)
}
/// Unlinks a package from a cluster in database.
pub async fn unlink_package_from_cluster(
package_hash: &str,
cluster_id: i64,
) -> LauncherResult<()> {
let state = State::get().await?;
let db = &state.db;
cluster_packages::Entity::delete_by_id((cluster_id, package_hash.to_string()))
.exec(db)
.await?;
Ok(())
}
/// Retrieves a package by its hash from the database.
pub async fn get_package_by_hash(hash: PackageId) -> LauncherResult<Option<packages::Model>> {
let state = State::get().await?;

View file

@ -4,8 +4,10 @@ use onelauncher_entity::package::Provider;
use onelauncher_entity::{clusters, packages};
use reqwest::Method;
use crate::api::cluster::dao::get_cluster_by_id;
use crate::api::packages::data::{ManagedPackageBody, PackageAuthor};
use crate::error::{LauncherError, LauncherResult};
use crate::send_error;
use crate::store::Dirs;
use crate::store::ingress::SubIngress;
use crate::utils::crypto::HashAlgorithm;
@ -290,3 +292,38 @@ impl DatabaseModelExt for packages::Model {
.join(&self.file_name))
}
}
/// Removes a package from a cluster.
/// It is also deleted from the filesystem and database.
pub async fn remove_package(cluster_id: i64, package_hash: String) -> LauncherResult<()> {
if let Some(package) = dao::get_package_by_hash(package_hash.clone()).await? {
dao::unlink_package_from_cluster(&package_hash, cluster_id).await?;
// Remove the hard link from the cluster's folder
if let Some(cluster) = get_cluster_by_id(cluster_id).await? {
let cluster_dir = Dirs::get_clusters_dir()
.await?
.join(cluster.folder_name)
.join(package.package_type.folder_name())
.join(&package.file_name);
if cluster_dir.exists() {
if let Err(err) = io::remove_file(&cluster_dir).await {
send_error!("{}{}", "failed to remove file: ", err);
}
}
}
if !dao::is_package_used(&package_hash).await? {
let path = package.path().await?;
if path.exists() {
if let Err(err) = io::remove_file(&path).await {
send_error!("{}{}", "failed to remove file: ", err);
}
}
dao::delete_package_by_id(package_hash).await?;
}
}
Ok(())
}

View file

@ -199,6 +199,9 @@ pub trait TauriLauncherApi {
#[taurpc(alias = "getLinkedPackages")]
async fn get_linked_packages(cluster_id: ClusterId) -> LauncherResult<Vec<packages::Model>>;
#[taurpc(alias = "removePackage")]
async fn remove_package(cluster_id: ClusterId, package_hash: String) -> LauncherResult<()>;
// endregion: package
// MARK: API: modpack
@ -753,6 +756,14 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
Ok(model)
}
async fn remove_package(
self,
cluster_id: ClusterId,
package_hash: String,
) -> LauncherResult<()> {
api::packages::remove_package(cluster_id, package_hash).await
}
// endregion: package
// MARK: Impl: modpack