LAUNCHER-14 feat: Disabling/enabling packages

This commit is contained in:
LynithDev 2025-01-16 13:51:41 +01:00
parent 04736cc6a4
commit 1912df7698
12 changed files with 200 additions and 128 deletions

View file

@ -76,6 +76,7 @@ macro_rules! collect_commands {
get_cluster_packages,
add_cluster_package,
remove_cluster_package,
set_cluster_package_enabled,
sync_cluster_packages,
sync_cluster_packages_by_type,
// Updater

View file

@ -152,7 +152,6 @@ pub async fn download_provider_package(
&cluster.cluster_path(),
pkg_path,
pkg,
Some(mgd_pkg.package_type),
)
.await?;
@ -182,11 +181,12 @@ pub async fn get_cluster_packages(
#[tauri::command]
pub async fn add_cluster_package(
cluster_path: ClusterPath,
package_path: PackagePath,
file_name: String,
package_type: PackageType,
pkg: Package,
package_type: Option<PackageType>,
) -> Result<(), String> {
package::add_package(&cluster_path, package_path, pkg, package_type).await?;
let package_path = PackagePath::new(package_type, file_name)?;
package::add_package(&cluster_path, package_path, pkg).await?;
Ok(())
}
@ -194,10 +194,24 @@ pub async fn add_cluster_package(
#[tauri::command]
pub async fn remove_cluster_package(
cluster_path: ClusterPath,
package_path: PackagePath,
file_name: String,
package_type: PackageType,
) -> Result<(), String> {
package::remove_package(&cluster_path, &package_path, package_type).await?;
let package_path = PackagePath::new(package_type, file_name)?;
package::remove_package(&cluster_path, &package_path).await?;
Ok(())
}
#[specta::specta]
#[tauri::command]
pub async fn set_cluster_package_enabled(
cluster_path: ClusterPath,
file_name: String,
package_type: PackageType,
enabled: bool,
) -> Result<(), String> {
let package_path = PackagePath::new(package_type, file_name)?;
package::set_package_enabled(&cluster_path, &package_path, enabled).await?;
Ok(())
}

View file

@ -1,5 +1,5 @@
import { CheckIcon } from '@untitled-theme/icons-solid';
import { createSignal, type JSX, splitProps } from 'solid-js';
import { createSignal, type JSX, Show, splitProps, untrack } from 'solid-js';
import styles from './Checkbox.module.scss';
type CheckboxProps = JSX.HTMLAttributes<HTMLDivElement> & {
@ -11,8 +11,11 @@ function Checkbox(props: CheckboxProps) {
const [checked, setChecked] = createSignal(props.defaultChecked || false);
const [split, rest] = splitProps(props, ['class', 'defaultChecked', 'onChecked', 'text']);
function toggle() {
const newValue = !checked();
function toggle(e: MouseEvent) {
e.preventDefault();
e.stopPropagation();
const newValue = !untrack(checked);
setChecked(newValue);
props.onChecked?.(newValue);
}
@ -20,11 +23,13 @@ function Checkbox(props: CheckboxProps) {
return (
<div
class={`${styles.checkbox} ${split.class ?? ''}`}
onClick={() => toggle()}
onClick={toggle}
{...rest}
>
<div class={`${styles.box} ${checked() ? styles.checked : ''}`}>
<CheckIcon />
<Show when={checked()}>
<CheckIcon />
</Show>
</div>
{props.children}
</div>

View file

@ -3,12 +3,13 @@ import UFuzzy from '@leeoniya/ufuzzy';
import { FilterFunnel01Icon, RefreshCw01Icon, SearchMdIcon, Trash03Icon } from '@untitled-theme/icons-solid';
import { bridge } from '~imports';
import Button from '~ui/components/base/Button';
import Checkbox from '~ui/components/base/Checkbox';
import TextField from '~ui/components/base/TextField';
import ScrollableContainer from '~ui/components/ScrollableContainer';
import Sidebar from '~ui/components/Sidebar';
import useBrowser from '~ui/hooks/useBrowser';
import useClusterContext from '~ui/hooks/useCluster';
import useCommand from '~ui/hooks/useCommand';
import useCommand, { tryResult } from '~ui/hooks/useCommand';
import useProcessor from '~ui/hooks/useProcessor';
import { type Accessor, createEffect, createSignal, For, Match, on, Show, Switch, untrack } from 'solid-js';
@ -206,6 +207,25 @@ function ModEntry(props: ModEntryProps) {
}
}
async function togglePackage(checked: boolean) {
try {
const path = cluster()?.path;
if (path) {
// eslint-disable-next-line solid/reactivity -- bs lol
await tryResult(() => bridge.commands.setClusterPackageEnabled(
path,
props.pkg.file_name,
'mod',
checked,
));
props.refetch();
}
}
catch (err) {
console.error(err);
}
}
return (
<div
class="flex flex-row items-center gap-3 rounded-xl bg-component-bg p-3 active:bg-component-bg-pressed hover:bg-component-bg-hover"
@ -251,9 +271,7 @@ function ModEntry(props: ModEntryProps) {
</div>
<div class="flex flex-row items-end justify-center gap-2">
{/* <Button buttonStyle="iconSecondary">
<Edit02Icon />
</Button> */}
<Checkbox defaultChecked={!props.pkg.disabled} onChecked={togglePackage} />
<Button
buttonStyle="iconDanger"

View file

@ -50,7 +50,6 @@ async fn main() -> Result<()> {
cluster::content::package::remove_package(
&cluster.cluster_path(),
&package_path,
PackageType::Mod,
)
.await?;
cluster::content::package::sync_packages_by_type(&cluster.cluster_path(), PackageType::Mod, None)
@ -106,7 +105,6 @@ async fn download_mod(
&cluster.cluster_path(),
package_path.clone(),
package,
Some(PackageType::Mod),
)
.await?;
}

View file

@ -4,7 +4,7 @@
"version": "1.0.0-alpha.1",
"private": true,
"engineStrict": true,
"packageManager": "pnpm@9.15.1",
"packageManager": "pnpm@9.15.4",
"description": "Next-generation open source Minecraft launcher and tools for interacting with Polyfrost",
"license": "GPL-3.0-only",
"homepage": "https://polyfrost.org",

View file

@ -464,9 +464,9 @@ export const commands = {
else return { status: 'error', error: e as any };
}
},
async addClusterPackage(clusterPath: ClusterPath, packagePath: PackagePath, pkg: Package, packageType: PackageType | null): Promise<Result<null, string>> {
async addClusterPackage(clusterPath: ClusterPath, fileName: string, packageType: PackageType, pkg: Package): Promise<Result<null, string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('add_cluster_package', { clusterPath, packagePath, pkg, packageType }) };
return { status: 'ok', data: await TAURI_INVOKE('add_cluster_package', { clusterPath, fileName, packageType, pkg }) };
}
catch (e) {
if (e instanceof Error)
@ -474,9 +474,19 @@ export const commands = {
else return { status: 'error', error: e as any };
}
},
async removeClusterPackage(clusterPath: ClusterPath, packagePath: PackagePath, packageType: PackageType): Promise<Result<null, string>> {
async removeClusterPackage(clusterPath: ClusterPath, fileName: string, packageType: PackageType): Promise<Result<null, string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('remove_cluster_package', { clusterPath, packagePath, packageType }) };
return { status: 'ok', data: await TAURI_INVOKE('remove_cluster_package', { clusterPath, fileName, packageType }) };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async setClusterPackageEnabled(clusterPath: ClusterPath, fileName: string, packageType: PackageType, enabled: boolean): Promise<Result<null, string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('set_cluster_package_enabled', { clusterPath, fileName, packageType, enabled }) };
}
catch (e) {
if (e instanceof Error)

View file

@ -112,11 +112,9 @@ async fn download_file(
// TODO: Implement hash checking
let cluster_path = &cluster.get_full_path().await?;
let path = PackagePath::new(
&cluster_path
let path = &cluster_path
.join(package_type.get_folder())
.join(&file.file_name),
);
.join(&file.file_name);
let ingress_id = init_ingress(
crate::IngressType::DownloadPackage {
@ -149,10 +147,10 @@ async fn download_file(
tracing::error!("{err}");
}
if let Err(err) = http::write(&path.0, &bytes, &state.io_semaphore).await {
if let Err(err) = http::write(&path, &bytes, &state.io_semaphore).await {
tracing::error!("failed to write file to cluster: {err}");
if path.0.exists() {
let _ = io::remove_file(&path.0).await;
if path.exists() {
let _ = io::remove_file(&path).await;
}
return Err(err);
@ -164,7 +162,7 @@ async fn download_file(
drop(state);
Ok(path)
Ok(PackagePath::from_path(path).await?)
}
/// Add a package to a cluster.
@ -173,7 +171,6 @@ pub async fn add_package(
cluster_path: &ClusterPath,
package_path: PackagePath,
package: Package,
package_type: Option<PackageType>,
) -> Result<()> {
let state = State::get().await?;
let mut manager = state.packages.write().await;
@ -182,7 +179,7 @@ pub async fn add_package(
.ok_or_else(|| anyhow::anyhow!("cluster not found in packages map"))?;
manager
.add_package(package_path, package, package_type)
.add_package(package_path, package)
.await?;
Ok(())
@ -193,7 +190,6 @@ pub async fn add_package(
pub async fn remove_package(
cluster_path: &ClusterPath,
package_path: &PackagePath,
package_type: PackageType,
) -> Result<()> {
let state = State::get().await?;
let mut manager = state.packages.write().await;
@ -201,7 +197,24 @@ pub async fn remove_package(
.get_mut(cluster_path)
.ok_or_else(|| anyhow::anyhow!("cluster not found in packages map"))?;
manager.remove_package(package_path, package_type).await?;
manager.remove_package(package_path).await?;
Ok(())
}
#[tracing::instrument]
pub async fn set_package_enabled(
cluster_path: &ClusterPath,
package_path: &PackagePath,
enabled: bool,
) -> Result<()> {
let state = State::get().await?;
let mut manager = state.packages.write().await;
let manager = manager
.get_mut(cluster_path)
.ok_or_else(|| anyhow::anyhow!("cluster not found in packages map"))?;
manager.set_package_enabled(package_path, enabled).await?;
Ok(())
}

View file

@ -9,7 +9,7 @@ use crate::proxy::send::send_cluster;
use crate::prelude::{ClusterPath, JavaVersion, PackagePath};
use crate::proxy::ClusterPayloadType;
pub use crate::store::{Cluster, JavaOptions, State};
use crate::store::{ClusterStage, MinecraftCredentials, ProcessorChild};
use crate::store::{MinecraftCredentials, ProcessorChild};
use onelauncher_utils::io::{self, IOError};
@ -256,12 +256,12 @@ pub async fn get_full_path(path: &ClusterPath) -> crate::Result<PathBuf> {
/// get a specific mod's full path in the filesystem by it's [`ClusterPath`] and [`PackagePath`].
#[tracing::instrument]
pub async fn get_mod_path(
pub async fn get_package_full_path(
cluster_path: &ClusterPath,
package_path: &PackagePath,
) -> crate::Result<PathBuf> {
if get(cluster_path).await?.is_some() {
let full_path = io::canonicalize(package_path.full_path(cluster_path.clone()).await?)?;
let full_path = io::canonicalize(package_path.full_path(cluster_path).await?)?;
return Ok(full_path);
}
@ -269,7 +269,7 @@ pub async fn get_mod_path(
Err(anyhow::anyhow!(
"failed to get the full path of a cluster at path {}",
package_path
.full_path(cluster_path.clone())
.full_path(cluster_path)
.await?
.display()
)

View file

@ -8,6 +8,7 @@ use notify_debouncer_mini::Debouncer;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use uuid::Uuid;
use crate::constants::CLUSTER_FILE;
@ -51,7 +52,7 @@ impl ClusterStage {
}
}
impl std::str::FromStr for ClusterStage {
impl FromStr for ClusterStage {
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"installed" => Self::Installed,
@ -188,7 +189,7 @@ impl From<InnerPathLinux> for RawPackagePath {
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)]
#[serde(transparent)]
pub struct PackagePath(pub PathBuf);
pub struct PackagePath(PathBuf);
impl PackagePath {
/// Transform a full [`PathBuf`] into a relative [`PackagePath`].
@ -207,28 +208,32 @@ impl PackagePath {
Ok(Self(path))
}
pub fn new(package_type: PackageType, file_name: String) -> crate::Result<Self> {
Ok(Self(
PathBuf::from_str(format!("{}/{}", package_type.get_folder(), file_name).as_str())
.map_err(|e| anyhow::anyhow!("couldn't create package path from '{package_type}' '{file_name}': {e:?}"))?
))
}
/// Get the full [`PathBuf`] of the current package path.
pub async fn full_path(&self, cluster: ClusterPath) -> crate::Result<PathBuf> {
pub async fn full_path(&self, cluster: &ClusterPath) -> crate::Result<PathBuf> {
let cluster_dir = cluster.full_path().await?;
Ok(cluster_dir.join(&self.0))
}
/// Get the [`InnerPathLinux`] of a [`PackagePath`].
#[must_use]
pub fn inner_path(&self) -> InnerPathLinux {
InnerPathLinux(
self.0
.components()
.map(|c| c.as_os_str().to_string_lossy().to_string())
.collect::<Vec<_>>()
.join("/"),
)
pub fn package_type(&self) -> crate::Result<PackageType> {
PackageType::from_parent(&self.0).ok_or_else(|| anyhow::anyhow!("couldn't get package type of package path: {:?}", self.0).into())
}
/// Create a new `PackagePath` from a relative path.
#[must_use]
pub fn new(path: &Path) -> Self {
Self(PathBuf::from(path))
pub fn file_name(&self) -> crate::Result<String> {
self.0
.file_name()
.map(|p| p.to_string_lossy().to_string())
.ok_or_else(|| anyhow::anyhow!("couldn't get file name of package path: {:?}", self.0).into())
}
pub fn relative_path(&self) -> PathBuf {
self.0.clone()
}
}

View file

@ -183,15 +183,12 @@ pub struct Package {
}
impl Package {
pub fn new(path: &PackagePath, meta: PackageMetadata) -> crate::Result<Self> {
let file_name = path
.0
.file_name()
.ok_or_else(|| crate::ErrorKind::AnyhowError(anyhow::anyhow!("no file name")))?
pub fn new(path: &PathBuf, meta: PackageMetadata) -> crate::Result<Self> {
let file_name = path.file_name()
.ok_or_else(|| anyhow::anyhow!("couldn't get filename of path {path:?}"))?
.to_string_lossy()
.to_string();
let sha1 = crypto::sha1_file(&path.0)?;
let sha1 = crypto::sha1_file(path)?;
Ok(Self {
sha1,
@ -341,7 +338,7 @@ impl PackageManager {
if let Ok(meta) = serde_json::from_slice(&io::read(packages_meta).await?) {
meta
} else {
io::copy(packages_meta, packages_meta.with_extension("bak")).await?;
io::copy(packages_meta, packages_meta.with_extension(".json.bak")).await?;
io::write(packages_meta, "{}").await?;
PackagesMeta::new(package_type)
}
@ -359,14 +356,10 @@ impl PackageManager {
&mut self,
package_path: PackagePath,
package: Package,
package_type: Option<PackageType>,
) -> crate::Result<()> {
let package_type = package
.meta
.get_package_type()
.unwrap_or(package_type.ok_or_else(|| anyhow::anyhow!("no package type"))?);
let package_type = package_path.package_type()?;
let packages = &mut self.get_mut(package_type).packages;
packages.insert(package_path, package);
insert_package_by_path(packages, package_path, package);
Ok(())
}
@ -376,15 +369,10 @@ impl PackageManager {
pub async fn remove_package(
&mut self,
package_path: &PackagePath,
package_type: PackageType,
) -> crate::Result<()> {
let full_path = self
.cluster_path
.clone()
.full_path()
.await?
.join(package_type.get_folder())
.join(package_path.0.clone());
let package_type = package_path.package_type()?;
let full_path = package_path.full_path(&self.cluster_path).await?;
io::remove_file(full_path).await?;
let packages = &mut self.get_mut(package_type).packages;
@ -393,6 +381,51 @@ impl PackageManager {
Ok(())
}
// disable a package in the manager
#[tracing::instrument(skip(self))]
pub async fn set_package_enabled(
&mut self,
package_path: &PackagePath,
enabled: bool,
) -> crate::Result<()> {
let state = State::get().await?;
let package_type = package_path.package_type()?;
let full_path = package_path.full_path(&self.cluster_path).await?;
let packages = &mut self.get_mut(package_type).packages;
let was_disabled = full_path.extension().map_or(false, |e| e == "disabled");
let mut new_path = full_path.clone();
let changed = if enabled && was_disabled {
new_path.set_extension("") // removes .disabled
} else if !enabled && !was_disabled {
new_path.set_extension("jar.disabled") // adds .disabled
} else {
false
};
if changed {
io::rename(full_path, new_path.clone()).await?;
if let Some(mut package) = packages.remove(package_path) {
let file_name = new_path.file_name().ok_or_else(|| anyhow::anyhow!("couldn't get file name"))?.to_string_lossy().to_string();
let package_path = PackagePath::new(package_type.clone(), file_name.clone())?;
package.disabled = !enabled;
package.file_name = file_name;
packages.insert(package_path, package);
}
self.sync_to_file_by_type(&state.directories, package_type).await?;
}
Ok(())
}
/// sync all packages
#[tracing::instrument(skip(self, dirs))]
pub async fn sync_packages(&mut self, dirs: &Directories) {
@ -458,19 +491,21 @@ impl PackageManager {
let mut packages_to_keep = HashSet::<PackagePath>::new();
let mut packages_to_infer = HashSet::<PathBuf>::new();
let full_cluster_path = self.cluster_path.full_path().await?;
while let Some(file) = files.next_entry().await? {
// Skip .packages.json meta file
if file.file_name().to_string_lossy().eq(&package_type.get_meta_file_name()) || file.file_type().await?.is_dir()
if file.file_name().to_string_lossy().starts_with(&package_type.get_meta_file_name()) || file.file_type().await?.is_dir()
{
continue;
}
let package_path = PackagePath::new(&file.path());
let package_path = PackagePath::from_path(&file.path()).await?;
// Check if the file is in the packages list already
if packages.get(&package_path).is_none() {
// Package path is not in the packages list but exists on file system, lets try to infer it later on
packages_to_infer.insert(package_path.0.clone());
packages_to_infer.insert(full_cluster_path.join(package_path.relative_path()));
}
// Add the package to the set to not remove it from the manager
@ -490,11 +525,11 @@ impl PackageManager {
for path in not_found {
tracing::warn!("failed to infer package: {:?}", path);
let package_path = PackagePath::new(&path);
let meta = PackageMetadata::Unknown;
let package = Package::new(&package_path, meta)?;
let package = Package::new(&path, meta)?;
let package_path = PackagePath::from_path(&path).await?;
packages.insert(package_path, package);
insert_package_by_path(packages, package_path, package);
}
}
}
@ -541,48 +576,17 @@ impl PackageManager {
}
}
// #[tracing::instrument(skip(packages))]
// async fn infer_packages(
// packages: &mut PackagesMap,
// package_type: PackageType,
// to_infer: HashSet<PathBuf>,
// ) {
// let infer_order = vec![
// Providers::Modrinth,
// Providers::SkyClient,
// Providers::Curseforge,
// ];
fn insert_package_by_path(packages: &mut PackagesMap, path: PackagePath, mut package: Package) {
if let Ok(file_name) = path.file_name() {
let is_disabled = file_name.ends_with(".disabled");
if is_disabled {
package.disabled = true;
}
}
// // if let Some(not_found) = infer_provider(Providers::Modrinth, package_type, packages, packages_to_infer, crypto::sha1_file).await {
// // if let Some(not_found) = infer_provider(Providers::SkyClient, package_type, packages, not_found, crypto::md5_file).await {
// // if let Some(not_found) = infer_provider(Providers::Curseforge, package_type, packages, not_found, |path| {
// // let hash = crypto::murmur2_file(path)?;
// // Ok(hash.to_string())
// // }).await {
// // }
// // }
// // }
// let mut to_infer = to_infer;
// for provider in infer_order {
// if let Some(not_found) = infer_provider(provider, package_type, packages, to_infer, move|path| {
// Ok(provider.hash_file(path))
// }).await {
// to_infer = not_found;
// }
// }
// for path in to_infer {
// tracing::warn!("failed to infer package: {:?}", path);
// let package_path = PackagePath::new(&path);
// let meta = PackageMetadata::Unknown;
// if let Ok(package) = Package::new(&package_path, meta) {
// packages.insert(package_path, package);
// }
// }
// }
packages.insert(path, package);
}
#[tracing::instrument(skip(packages, to_infer))]
async fn infer_provider(
@ -633,7 +637,7 @@ async fn infer_provider(
let Some(version) = versions.get(hash) else { continue; };
if let Some(package_path) = hash_map.get(hash) {
if let Some(path) = hash_map.get(hash) {
let meta = PackageMetadata::Managed {
package_id: managed.id,
provider: provider.clone(),
@ -645,10 +649,14 @@ async fn infer_provider(
icon_url: managed.icon_url
};
let package_path = &PackagePath::new(package_path);
match Package::new(package_path, meta) {
let Ok(package_path) = PackagePath::from_path(path).await else {
tracing::error!("failed to create package path for {:?}", path);
continue;
};
match Package::new(path, meta) {
Ok(package) => {
packages.insert(package_path.clone(), package);
insert_package_by_path(packages, package_path, package);
},
Err(err) => {
tracing::error!("failed to create package from {}: {}", provider, err);

View file

@ -13,7 +13,7 @@ use uuid::Uuid;
use crate::api::cluster;
use crate::constants::PROCESSOR_FILE;
use crate::proxy::send::{send_ingress, send_message, send_process};
use crate::proxy::send::{send_message, send_process};
use crate::proxy::ProcessPayloadType;
use crate::utils::http::read_json;
use crate::State;