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
29 changes: 29 additions & 0 deletions src-tauri/src/commands/mods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,35 @@ pub fn enable_mod_with_layers(
result.into()
}

#[derive(Debug, serde::Deserialize, ts_rs::TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct EditModMetadataArgs {
pub display_name: Option<String>,
pub tags: Option<Vec<String>>,
pub champions: Option<Vec<String>>,
pub maps: Option<Vec<String>>,
#[serde(default)]
pub set_thumbnail_path: Option<String>,
#[serde(default)]
pub remove_thumbnail: Option<bool>,
}

/// Edit a mod's metadata (name, tags, champions, maps).
#[tauri::command]
pub fn edit_mod_metadata(
mod_id: String,
metadata: EditModMetadataArgs,
library: State<ModLibraryState>,
settings: State<SettingsState>,
) -> IpcResult<InstalledMod> {
let result: AppResult<InstalledMod> = (|| {
let settings = settings.0.lock().mutex_err()?.clone();
library.0.edit_mod_metadata(&settings, &mod_id, metadata)
})();
result.into()
}

/// Inspect a `.modpkg` file and return its metadata.
#[tauri::command]
pub fn inspect_modpkg(file_path: String) -> IpcResult<ModpkgInfo> {
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ fn main() {
commands::toggle_mod,
commands::set_mod_layers,
commands::enable_mod_with_layers,
commands::edit_mod_metadata,
commands::inspect_modpkg,
commands::get_mod_thumbnail,
commands::get_storage_directory,
Expand Down
100 changes: 100 additions & 0 deletions src-tauri/src/mods/library.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,106 @@ impl ModLibrary {
})
}

pub fn edit_mod_metadata(
&self,
settings: &Settings,
mod_id: &str,
args: crate::commands::EditModMetadataArgs,
) -> AppResult<InstalledMod> {
self.mutate_index(settings, |storage_dir, index| {
let entry = index
.mods
.iter()
.find(|m| m.id == mod_id)
.ok_or_else(|| AppError::ModNotFound(mod_id.to_string()))?;

let mod_dir = entry.metadata_dir(storage_dir);
let mut project = load_mod_project(&mod_dir)?;

if let Some(dn) = args.display_name {
project.display_name = dn;
}
if let Some(t) = args.tags {
project.tags = t.into_iter().map(ltk_mod_project::ModTag::from).collect();
}
if let Some(c) = args.champions {
project.champions = c;
}
if let Some(m) = args.maps {
project.maps = m.into_iter().map(ltk_mod_project::ModMap::from).collect();
}

if let Some(true) = args.remove_thumbnail {
let _ = fs::remove_file(mod_dir.join("thumbnail.webp"));
let _ = fs::remove_file(mod_dir.join("thumbnail.png"));
project.thumbnail = None;
} else if let Some(image_path) = args.set_thumbnail_path {
let source_path = PathBuf::from(&image_path);
if !source_path.exists() {
return Err(AppError::InvalidPath(image_path));
}

let extension = source_path
.extension()
.and_then(|e| e.to_str())
.map(|e| e.to_lowercase())
.unwrap_or_default();

let supported_formats = [
"webp", "png", "jpg", "jpeg", "gif", "bmp", "tiff", "tif", "ico",
];
if !supported_formats.contains(&extension.as_str()) {
return Err(AppError::ValidationFailed(format!(
"Unsupported image format: {}. Supported formats: {}",
extension,
supported_formats.join(", ")
)));
}

let webp_data = if extension == "webp" {
image::open(&source_path).map_err(|e| {
AppError::ValidationFailed(format!("Failed to open image: {}", e))
})?;
fs::read(&source_path)?
} else {
let img = image::open(&source_path).map_err(|e| {
AppError::ValidationFailed(format!("Failed to open image: {}", e))
})?;
let encoder = webp::Encoder::from_image(&img).map_err(|e| {
AppError::ValidationFailed(format!("Failed to encode WebP: {}", e))
})?;
encoder.encode(90.0).to_vec()
};

let target_path = mod_dir.join("thumbnail.webp");
let tmp_path = mod_dir.join("thumbnail.webp.tmp");

fs::write(&tmp_path, webp_data)?;

if target_path.exists() {
let _ = fs::remove_file(&target_path);
}
fs::rename(&tmp_path, &target_path)?;

let _ = fs::remove_file(mod_dir.join("thumbnail.png"));
project.thumbnail = Some("thumbnail.webp".to_string());
}

let config_path = mod_dir.join("mod.config.json");
std::fs::write(config_path, serde_json::to_string_pretty(&project)?)?;

// Determine if enabled
let mut enabled = false;
let mut layer_states = None;
if let Ok(active_profile) = super::get_active_profile(index) {
enabled = active_profile.enabled_mods.contains(&mod_id.to_string());
layer_states = active_profile.layer_states.get(mod_id);
}

read_installed_mod(entry, enabled, storage_dir, layer_states)
})
}

pub fn uninstall_mod_by_id(&self, settings: &Settings, mod_id: &str) -> AppResult<()> {
self.mutate_index(settings, |storage_dir, index| {
let Some(pos) = index.mods.iter().position(|m| m.id == mod_id) else {
Expand Down
10 changes: 10 additions & 0 deletions src/lib/bindings/EditModMetadataArgs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

export type EditModMetadataArgs = {
displayName: string | null;
tags: Array<string> | null;
champions: Array<string> | null;
maps: Array<string> | null;
setThumbnailPath?: string | null;
removeThumbnail?: boolean | null;
};
1 change: 1 addition & 0 deletions src/lib/bindings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type { ContentTree } from "./ContentTree";
export type { CreateProjectArgs } from "./CreateProjectArgs";
export type { CslolModInfo } from "./CslolModInfo";
export type { DiagnosticReport } from "./DiagnosticReport";
export type { EditModMetadataArgs } from "./EditModMetadataArgs";
export type { ErrorCode } from "./ErrorCode";
export type { FantomeImportProgress } from "./FantomeImportProgress";
export type { FantomeImportStage } from "./FantomeImportStage";
Expand Down
3 changes: 3 additions & 0 deletions src/lib/tauri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
CreateProjectArgs,
CslolModInfo,
DiagnosticReport,
EditModMetadataArgs,
FantomePeekResult,
HotkeyAction,
ImportFantomeArgs,
Expand Down Expand Up @@ -103,6 +104,8 @@ export const api = {
invokeResult<void>("set_mod_layers", { modId, layerStates }),
enableModWithLayers: (modId: string, layerStates: Record<string, boolean>) =>
invokeResult<void>("enable_mod_with_layers", { modId, layerStates }),
editModMetadata: (modId: string, metadata: EditModMetadataArgs) =>
invokeResult<InstalledMod>("edit_mod_metadata", { modId, metadata }),
getModWadReport: (modId: string) =>
invokeResult<ModWadReport | null>("get_mod_wad_report", { modId }),
getAllModWadReports: () => invokeResult<Record<string, ModWadReport>>("get_all_mod_wad_reports"),
Expand Down
1 change: 1 addition & 0 deletions src/modules/library/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export { useAnalyzeModWads } from "./useAnalyzeModWads";
export { useBulkInstallMods } from "./useBulkInstallMods";
export { useCreateProfile } from "./useCreateProfile";
export { useDeleteProfile } from "./useDeleteProfile";
export { useEditMod } from "./useEditMod";
export { useEnableModWithLayers } from "./useEnableModWithLayers";
export { useFilteredMods } from "./useFilteredMods";
export type { FilterOptions } from "./useFilterOptions";
Expand Down
35 changes: 35 additions & 0 deletions src/modules/library/api/useEditMod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";

import { api, type AppError, type EditModMetadataArgs, type InstalledMod } from "@/lib/tauri";
import { unwrapForQuery } from "@/utils/query";

import { libraryKeys } from "./keys";

interface EditModVariables {
modId: string;
metadata: EditModMetadataArgs;
}

/**
* Hook to edit a mod's metadata.
* Returns the updated InstalledMod.
*/
export function useEditMod() {
const queryClient = useQueryClient();

return useMutation<InstalledMod, AppError, EditModVariables>({
mutationFn: async ({ modId, metadata }) => {
const result = await api.editModMetadata(modId, metadata);
return unwrapForQuery(result);
},
onSuccess: (updatedMod) => {
// Update the cache with the new mod
queryClient.setQueryData<InstalledMod[]>(libraryKeys.mods(), (old) =>
old?.map((mod) => (mod.id === updatedMod.id ? updatedMod : mod)),
);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: libraryKeys.mods() });
},
});
}
3 changes: 3 additions & 0 deletions src/modules/library/api/useLibraryContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export function useLibraryContent({
const { data: patcherStatus } = usePatcherStatus();
const isPatcherActive = patcherStatus?.running ?? false;
const [detailsMod, setDetailsMod] = useState<InstalledMod | null>(null);
const [editMod, setEditMod] = useState<InstalledMod | null>(null);
const filteredMods = useFilteredMods(mods, searchQuery);
const hasActiveFilters = useHasActiveFilters();
const { sort } = useLibraryFilterStore();
Expand Down Expand Up @@ -140,5 +141,7 @@ export function useLibraryContent({
contentView,
detailsMod,
setDetailsMod,
editMod,
setEditMod,
};
}
Loading
Loading