fix: Package removal and syncing
feat: Added fetching managed packages on ClusterMods Page
This commit is contained in:
parent
3025502d82
commit
02f1aaa429
7 changed files with 181 additions and 61 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue