fix: Package removal and syncing

feat: Added fetching managed packages on ClusterMods Page
This commit is contained in:
LynithDev 2024-09-08 12:53:18 +02:00
parent 3025502d82
commit 02f1aaa429
7 changed files with 181 additions and 61 deletions

View file

@ -71,6 +71,7 @@ macro_rules! collect_commands {
add_cluster_package,
remove_cluster_package,
sync_cluster_packages,
sync_cluster_packages_by_type,
// Updater
check_for_update,
install_update

View file

@ -103,9 +103,10 @@ pub async fn download_provider_package(
.await?
.ok_or("cluster not found")?;
let pkg = provider.get(&package_id).await?;
let mgd_pkg = provider.get(&package_id).await?;
package::download_package(&pkg, &mut cluster, game_version, loader, package_version).await?;
let (pkg_path, pkg) = package::download_package(&mgd_pkg, &mut cluster, game_version, loader, package_version).await?;
package::add_package(&cluster.cluster_path(), pkg_path, pkg, Some(mgd_pkg.package_type)).await?;
Ok(())
}
@ -161,3 +162,10 @@ pub async fn sync_cluster_packages(cluster_path: ClusterPath) -> Result<(), Stri
package::sync_packages(&cluster_path).await?;
Ok(())
}
#[specta::specta]
#[tauri::command]
pub async fn sync_cluster_packages_by_type(cluster_path: ClusterPath, package_type: PackageType) -> Result<(), String> {
package::sync_packages_by_type(&cluster_path, package_type).await?;
Ok(())
}

View file

@ -1,19 +1,56 @@
import { Edit02Icon, FilterFunnel01Icon, SearchMdIcon, Trash03Icon } from '@untitled-theme/icons-solid';
import { For, Match, Switch, createEffect, createSignal } from 'solid-js';
import { FilterFunnel01Icon, SearchMdIcon, Trash03Icon } from '@untitled-theme/icons-solid';
import { For, Match, Show, Switch, createEffect, createResource, createSignal } from 'solid-js';
import UFuzzy from '@leeoniya/ufuzzy';
import type { Package } from '@onelauncher/client/bindings';
import type { ManagedPackage, Package, Providers } from '@onelauncher/client/bindings';
import Button from '~ui/components/base/Button';
import TextField from '~ui/components/base/TextField';
import ScrollableContainer from '~ui/components/ScrollableContainer';
import Sidebar from '~ui/components/Sidebar';
import useClusterContext from '~ui/hooks/useCluster';
import useCommand from '~ui/hooks/useCommand';
import useCommand, { tryResult } from '~ui/hooks/useCommand';
import { bridge } from '~imports';
import Spinner from '~ui/components/Spinner';
import useBrowser from '~ui/hooks/useBrowser';
// TODO: Possibly optimise this as it has 2 cloned lists, and another containing only the names
function ClusterMods() {
const [cluster] = useClusterContext();
const [mods] = useCommand(cluster, () => bridge.commands.getClusterPackages(cluster()?.path || '', 'mod'));
const [mods, { refetch }] = useCommand(cluster, () => bridge.commands.getClusterPackages(cluster()?.path || '', 'mod'));
const [managedMods] = createResource(mods, async () => {
const list = mods();
if (!list)
return Promise.resolve(new Map<Providers, ManagedPackage[]>());
const providers: Map<Providers, string[]> = new Map();
list.forEach((mod) => {
if (mod.meta.type === 'managed')
providers.set(mod.meta.provider, [
...(providers.get(mod.meta.provider) || []),
mod.meta.package_id,
]);
});
const formatted: Map<Providers, ManagedPackage[]> = new Map();
for (const [provider, packageIds] of providers.entries()) {
const result = await tryResult(() => bridge.commands.getProviderPackages(provider, packageIds));
formatted.set(provider, result);
}
return formatted;
});
function getManagedData(mod: Package): ManagedPackage | undefined {
const meta = mod.meta;
if (meta.type === 'managed') {
const provider = managedMods()?.get(meta.provider);
if (provider && provider.length > 0)
return provider.find(pkg => pkg.id === meta.package_id);
}
return undefined;
}
// Mods to display, should be a clone of the mods array but sorted
const [displayedMods, setDisplayedMods] = createSignal<Package[]>([]);
@ -73,32 +110,39 @@ function ClusterMods() {
<Sidebar.Page>
<div class="w-full flex flex-row justify-between">
<h1>Mods</h1>
<div class="flex flex-row items-center justify-end gap-x-2">
<Button
buttonStyle="secondary"
onClick={() => toggleNameSort()}
children={sortingAtoZ() ? 'A-Z' : 'Z-A'}
iconLeft={<FilterFunnel01Icon />}
/>
<div>
<div class="flex flex-row items-stretch justify-end gap-x-2">
<Button
buttonStyle="secondary"
onClick={() => toggleNameSort()}
children={sortingAtoZ() ? 'A-Z' : 'Z-A'}
iconLeft={<FilterFunnel01Icon />}
/>
<TextField iconLeft={<SearchMdIcon />} placeholder="Search..." onInput={e => search(e.target.value)} />
<TextField iconLeft={<SearchMdIcon />} placeholder="Search..." onInput={e => search(e.target.value)} />
</div>
</div>
</div>
<ScrollableContainer>
<Switch>
<Match when={displayedMods().length > 0}>
<For each={displayedMods()}>
{mod => (
<ModEntry pkg={mod} />
)}
</For>
</Match>
<Spinner.Suspense>
<Show when={managedMods.loading === false}>
<Switch>
<Match when={displayedMods().length > 0}>
<For each={displayedMods()}>
{(mod) => {
const managed = getManagedData(mod);
return <ModEntry pkg={mod} managed={managed} refetch={refetch} />;
}}
</For>
</Match>
<Match when={displayedMods().length === 0}>
<p class="my-4 text-center text-2lg">No mods were found</p>
</Match>
</Switch>
<Match when={displayedMods().length === 0}>
<p class="my-4 text-center text-2lg">No mods were found</p>
</Match>
</Switch>
</Show>
</Spinner.Suspense>
</ScrollableContainer>
</Sidebar.Page>
);
@ -108,13 +152,14 @@ export default ClusterMods;
interface ModEntryProps {
pkg: Package;
refetch: () => void;
managed?: ManagedPackage | undefined;
};
function icon() {
return '';
}
function ModEntry(props: ModEntryProps) {
const [cluster] = useClusterContext();
const browser = useBrowser();
const name = () => {
if (props.pkg.meta.type === 'unknown')
return props.pkg.file_name;
@ -144,10 +189,45 @@ function ModEntry(props: ModEntryProps) {
return tag;
};
const icon = () => {
if (props.pkg.meta.type === 'managed')
return props.managed?.icon_url;
return undefined;
};
function onClick(e: MouseEvent) {
if (props.managed) {
e.preventDefault();
e.stopPropagation();
browser.displayPackage(props.managed.id, props.managed.provider);
}
}
async function deletePackage(e: MouseEvent) {
e.preventDefault();
e.stopPropagation();
const path = cluster()?.path;
if (path) {
await bridge.commands.removeClusterPackage(path, props.pkg.file_name, 'mod');
await bridge.commands.syncClusterPackages(path);
props.refetch();
}
}
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">
<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"
onClick={onClick}
>
<div>
<img src={icon()} alt={name()} class="aspect-ratio-square h-10 rounded-lg" />
<Show
when={icon()}
fallback={<div class="aspect-ratio-square h-10 rounded-lg bg-gray-05" />}
children={<img src={icon()!} alt={name()} class="aspect-ratio-square h-10 rounded-lg" />}
/>
</div>
<div class="flex flex-1 flex-col">
<div class="flex flex-row items-center justify-between">
@ -169,12 +249,12 @@ function ModEntry(props: ModEntryProps) {
</div>
<div class="flex flex-row items-end justify-center gap-2">
<Button buttonStyle="iconSecondary">
{/* <Button buttonStyle="iconSecondary">
<Edit02Icon />
</Button>
</Button> */}
<Button buttonStyle="iconSecondary">
<Trash03Icon class="!stroke-danger" />
<Button buttonStyle="iconDanger" onClick={deletePackage}>
<Trash03Icon />
</Button>
</div>
</div>

View file

@ -1,6 +1,6 @@
use onelauncher::data::{Loader, PackageType};
use onelauncher::package::content::Providers;
use onelauncher::store::Cluster;
use onelauncher::store::{Cluster, PackagePath};
use onelauncher::{cluster, Result};
const CLUSTER_NAME: &str = "Test Packages";
@ -18,7 +18,7 @@ async fn main() -> Result<()> {
// Print Packages first
print_packages(&cluster).await?;
// Download the first mod, add it to the PackageManager and sync packages
println!("Download the first mod, adding it to the PackageManager and sync packages");
download_mod(
&mut cluster,
MODRINTH_PACKAGE_ONE,
@ -32,8 +32,8 @@ async fn main() -> Result<()> {
// Print Packages again
print_packages(&cluster).await?;
// Download the second mod, DON'T add it to the PackageManager and sync packages
download_mod(
println!("Download the second mod, NOT adding it to the PackageManager and sync packages");
let package_path = download_mod(
&mut cluster,
MODRINTH_PACKAGE_TWO,
MODRINTH_PACKAGE_TWO_VERSION,
@ -46,6 +46,15 @@ async fn main() -> Result<()> {
// Print Packages again
print_packages(&cluster).await?;
println!("Removing the second mod from the PackageManager and sync packages");
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)
.await?;
Ok(())
}
@ -74,7 +83,7 @@ async fn create_cluster() -> Result<Cluster> {
Ok(cluster)
}
async fn download_mod(cluster: &mut Cluster, pkg: &str, ver: &str, add: bool) -> Result<()> {
async fn download_mod(cluster: &mut Cluster, pkg: &str, ver: &str, add: bool) -> Result<PackagePath> {
let package = Providers::Modrinth.get(pkg).await?;
let (package_path, package) = cluster::content::package::download_package(
@ -96,7 +105,7 @@ async fn download_mod(cluster: &mut Cluster, pkg: &str, ver: &str, add: bool) ->
.await?;
}
Ok(())
Ok(package_path)
}
async fn print_packages(cluster: &Cluster) -> Result<()> {

View file

@ -242,7 +242,7 @@ pub async fn sync_packages_by_type(
.get_mut(cluster_path)
.ok_or(anyhow::anyhow!("cluster not found in packages map"))?;
manager.sync_packages(&state.directories).await;
manager.sync_packages_by_type(&state.directories, package_type).await?;
Ok(())
}

View file

@ -290,7 +290,7 @@ pub async fn get(id: &str) -> Result<ModrinthPackage> {
pub async fn get_multiple(ids: &[String]) -> Result<Vec<ModrinthPackage>> {
Ok(serde_json::from_slice(
&fetch(
format_url!("/projects?ids=[{}]", ids.join(",")).as_str(),
format_url!("/projects?ids=[{}]", ids.iter().map(|id| format!("\"{}\"", id)).collect::<Vec<String>>().join(",")).as_str(),
None,
&State::get().await?.fetch_semaphore,
)

View file

@ -7,7 +7,7 @@ use crate::utils::io;
use async_zip::tokio::read::fs::ZipFileReader;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use tokio::fs::DirEntry;
@ -354,6 +354,9 @@ impl PackageManager {
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());
io::remove_file(full_path).await?;
let packages = &mut self.get_mut(package_type).packages;
packages.remove(package_path);
@ -420,7 +423,7 @@ impl PackageManager {
/// returns a list of packages that have a file but are not synced in the manager
#[tracing::instrument]
async fn insert_unsynced_packages(
async fn sync_packages_from_local(
&self,
dirs: &Directories,
packages: &mut PackagesMap,
@ -434,26 +437,45 @@ impl PackageManager {
)
.await?;
let mut packages_to_keep = HashSet::<PackagePath>::new();
while let Some(file) = files.next_entry().await? {
if file
.file_name()
// Skip .packages.json meta file
if file.file_name()
.to_string_lossy()
.eq(&package_type.get_meta_file_name())
{
continue;
}
let path = PackagePath::new(&file.path());
let package_path = PackagePath::new(&file.path());
if let std::collections::hash_map::Entry::Vacant(e) = packages.entry(path) {
if package_type.file_matches(&file).await? {
// TODO: Infer
let package =
Package::new(&PackagePath::new(&file.path()), PackageMetadata::Unknown)
.await?;
e.insert(package);
}
// Check if the file is in the packages list already
if let Some(_package) = packages.get(&package_path) {
// Package path is in manager and file system
// match package.meta {
// PackageMetadata::Unknown => {
// // TODO: Infer
// }
// _ => {}
// }
} else {
// Package path is not in the packages list but exists on file system, lets add it to the manager
// TODO: Infer package
let meta = PackageMetadata::Unknown;
let package = Package::new(&package_path, meta).await?;
packages.insert(package_path.clone(), package);
}
// Add the package to the set to not remove it from the manager
packages_to_keep.insert(package_path);
}
if packages.len() != packages_to_keep.len() {
packages.retain(|pkg_path, _| packages_to_keep.contains(pkg_path))
}
Ok(())
@ -461,7 +483,7 @@ impl PackageManager {
// sync packages in a cluster
#[tracing::instrument]
async fn sync_packages_by_type(
pub async fn sync_packages_by_type(
&mut self,
dirs: &Directories,
package_type: PackageType,
@ -471,8 +493,8 @@ impl PackageManager {
let mut new_packages = self.get_from_file(dirs, package_type).await?.packages;
packages.extend(new_packages.drain());
// Insert unsynced packages (files that exist but are not in the manager)
self.insert_unsynced_packages(dirs, &mut packages, package_type)
// Sync the packages from FS
self.sync_packages_from_local(dirs, &mut packages, package_type)
.await?;
// Finally store the new list in memory and on disk