diff --git a/src-tauri/src/commands/mods.rs b/src-tauri/src/commands/mods.rs index 6d3911c..5ba851f 100644 --- a/src-tauri/src/commands/mods.rs +++ b/src-tauri/src/commands/mods.rs @@ -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, + pub tags: Option>, + pub champions: Option>, + pub maps: Option>, + #[serde(default)] + pub set_thumbnail_path: Option, + #[serde(default)] + pub remove_thumbnail: Option, +} + +/// Edit a mod's metadata (name, tags, champions, maps). +#[tauri::command] +pub fn edit_mod_metadata( + mod_id: String, + metadata: EditModMetadataArgs, + library: State, + settings: State, +) -> IpcResult { + let result: AppResult = (|| { + 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 { diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 9d5519f..9c85ccf 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -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, diff --git a/src-tauri/src/mods/library.rs b/src-tauri/src/mods/library.rs index 8d4c95c..1f1e164 100644 --- a/src-tauri/src/mods/library.rs +++ b/src-tauri/src/mods/library.rs @@ -300,6 +300,106 @@ impl ModLibrary { }) } + pub fn edit_mod_metadata( + &self, + settings: &Settings, + mod_id: &str, + args: crate::commands::EditModMetadataArgs, + ) -> AppResult { + 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 { diff --git a/src/lib/bindings/EditModMetadataArgs.ts b/src/lib/bindings/EditModMetadataArgs.ts new file mode 100644 index 0000000..ef5a7d6 --- /dev/null +++ b/src/lib/bindings/EditModMetadataArgs.ts @@ -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 | null; + champions: Array | null; + maps: Array | null; + setThumbnailPath?: string | null; + removeThumbnail?: boolean | null; +}; diff --git a/src/lib/bindings/index.ts b/src/lib/bindings/index.ts index 84fb8a2..5108152 100644 --- a/src/lib/bindings/index.ts +++ b/src/lib/bindings/index.ts @@ -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"; diff --git a/src/lib/tauri.ts b/src/lib/tauri.ts index 5068a1d..3d257de 100644 --- a/src/lib/tauri.ts +++ b/src/lib/tauri.ts @@ -8,6 +8,7 @@ import type { CreateProjectArgs, CslolModInfo, DiagnosticReport, + EditModMetadataArgs, FantomePeekResult, HotkeyAction, ImportFantomeArgs, @@ -103,6 +104,8 @@ export const api = { invokeResult("set_mod_layers", { modId, layerStates }), enableModWithLayers: (modId: string, layerStates: Record) => invokeResult("enable_mod_with_layers", { modId, layerStates }), + editModMetadata: (modId: string, metadata: EditModMetadataArgs) => + invokeResult("edit_mod_metadata", { modId, metadata }), getModWadReport: (modId: string) => invokeResult("get_mod_wad_report", { modId }), getAllModWadReports: () => invokeResult>("get_all_mod_wad_reports"), diff --git a/src/modules/library/api/index.ts b/src/modules/library/api/index.ts index f967f80..813a1e3 100644 --- a/src/modules/library/api/index.ts +++ b/src/modules/library/api/index.ts @@ -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"; diff --git a/src/modules/library/api/useEditMod.ts b/src/modules/library/api/useEditMod.ts new file mode 100644 index 0000000..b3f8d5a --- /dev/null +++ b/src/modules/library/api/useEditMod.ts @@ -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({ + 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(libraryKeys.mods(), (old) => + old?.map((mod) => (mod.id === updatedMod.id ? updatedMod : mod)), + ); + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: libraryKeys.mods() }); + }, + }); +} diff --git a/src/modules/library/api/useLibraryContent.ts b/src/modules/library/api/useLibraryContent.ts index e338d1a..56afc41 100644 --- a/src/modules/library/api/useLibraryContent.ts +++ b/src/modules/library/api/useLibraryContent.ts @@ -44,6 +44,7 @@ export function useLibraryContent({ const { data: patcherStatus } = usePatcherStatus(); const isPatcherActive = patcherStatus?.running ?? false; const [detailsMod, setDetailsMod] = useState(null); + const [editMod, setEditMod] = useState(null); const filteredMods = useFilteredMods(mods, searchQuery); const hasActiveFilters = useHasActiveFilters(); const { sort } = useLibraryFilterStore(); @@ -140,5 +141,7 @@ export function useLibraryContent({ contentView, detailsMod, setDetailsMod, + editMod, + setEditMod, }; } diff --git a/src/modules/library/components/EditMetadataDialog.tsx b/src/modules/library/components/EditMetadataDialog.tsx new file mode 100644 index 0000000..847034a --- /dev/null +++ b/src/modules/library/components/EditMetadataDialog.tsx @@ -0,0 +1,230 @@ +import { useQueryClient } from "@tanstack/react-query"; +import { convertFileSrc } from "@tauri-apps/api/core"; +import { open as openFileDialog } from "@tauri-apps/plugin-dialog"; +import { Edit3, Image, Trash2 } from "lucide-react"; +import { useEffect, useMemo, useState } from "react"; + +import { Button, Dialog, FormField, MultiSelect, useToast } from "@/components"; +import type { InstalledMod } from "@/lib/tauri"; +import { libraryKeys } from "@/modules/library/api/keys"; +import { useEditMod } from "@/modules/library/api/useEditMod"; +import { useModThumbnail } from "@/modules/library/api/useModThumbnail"; +import { + getMapLabel, + getTagLabel, + WELL_KNOWN_MAPS, + WELL_KNOWN_TAGS, +} from "@/modules/library/utils/labels"; + +interface EditMetadataDialogProps { + mod: InstalledMod; + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export function EditMetadataDialog({ mod, open, onOpenChange }: EditMetadataDialogProps) { + const [displayName, setDisplayName] = useState(mod.displayName); + const [tags, setTags] = useState>(new Set(mod.tags)); + const [maps, setMaps] = useState>(new Set(mod.maps)); + const [championsStr, setChampionsStr] = useState(mod.champions.join(", ")); + const [thumbnailPath, setThumbnailPath] = useState(null); + const [removeThumbnail, setRemoveThumbnail] = useState(false); + + const editMod = useEditMod(); + const toast = useToast(); + const queryClient = useQueryClient(); + + const { data: currentThumbnailUrl } = useModThumbnail(mod.id); + + // Reset state when dialog opens + useEffect(() => { + if (open) { + setDisplayName(mod.displayName); + setTags(new Set(mod.tags)); + setMaps(new Set(mod.maps)); + setChampionsStr(mod.champions.join(", ")); + setThumbnailPath(null); + setRemoveThumbnail(false); + } + }, [mod, open]); + + const tagOptions = useMemo(() => { + const options = WELL_KNOWN_TAGS.map((tag) => ({ value: tag, label: getTagLabel(tag) })); + // Add any custom tags the mod already has + mod.tags.forEach((tag) => { + if (!options.some((o) => o.value === tag)) { + options.push({ value: tag, label: tag }); + } + }); + return options; + }, [mod.tags]); + + const mapOptions = useMemo(() => { + const options = WELL_KNOWN_MAPS.map((map) => ({ value: map, label: getMapLabel(map) })); + // Add any custom maps the mod already has + mod.maps.forEach((map) => { + if (!options.some((o) => o.value === map)) { + options.push({ value: map, label: map }); + } + }); + return options; + }, [mod.maps]); + + const handleSetThumbnail = async () => { + const file = await openFileDialog({ + multiple: false, + filters: [ + { + name: "Images", + extensions: ["webp", "png", "jpg", "jpeg", "gif", "bmp", "tiff", "tif", "ico"], + }, + ], + }); + if (file && typeof file === "string") { + setThumbnailPath(file); + setRemoveThumbnail(false); + } + }; + + const handleRemoveThumbnail = () => { + setThumbnailPath(null); + setRemoveThumbnail(true); + }; + + const handleSave = () => { + const champions = championsStr + .split(",") + .map((s) => s.trim()) + .filter(Boolean); + + editMod.mutate( + { + modId: mod.id, + metadata: { + displayName, + tags: Array.from(tags), + maps: Array.from(maps), + champions, + setThumbnailPath: thumbnailPath, + removeThumbnail: removeThumbnail, + }, + }, + { + onSuccess: () => { + toast.success("Metadata updated", "Mod information has been saved successfully."); + queryClient.invalidateQueries({ queryKey: libraryKeys.thumbnail(mod.id) }); + onOpenChange(false); + }, + onError: (error) => { + toast.error("Failed to update metadata", error.message); + }, + }, + ); + }; + + return ( + + + + + + + + Edit Mod Metadata + + + + + +
+
+ {!removeThumbnail && (thumbnailPath || currentThumbnailUrl) ? ( + Mod thumbnail + ) : ( +
+ +
+ )} +
+
+ + {!removeThumbnail && (thumbnailPath || currentThumbnailUrl) && ( + + )} +
+
+ + setDisplayName(e.target.value)} + placeholder="e.g. My Awesome Mod" + /> + +
+ + +
+ +
+ + +
+ + setChampionsStr(e.target.value)} + placeholder="e.g. Riven, Lee Sin" + /> +
+ + + + + +
+
+
+ ); +} diff --git a/src/modules/library/components/FolderRow.tsx b/src/modules/library/components/FolderRow.tsx index 8d17015..2377468 100644 --- a/src/modules/library/components/FolderRow.tsx +++ b/src/modules/library/components/FolderRow.tsx @@ -16,9 +16,16 @@ interface FolderRowProps { mods: InstalledMod[]; dndDisabled?: boolean; onViewDetails?: (mod: InstalledMod) => void; + onEditMetadata?: (mod: InstalledMod) => void; } -export function FolderRow({ folder, mods, dndDisabled = true, onViewDetails }: FolderRowProps) { +export function FolderRow({ + folder, + mods, + dndDisabled = true, + onViewDetails, + onEditMetadata, +}: FolderRowProps) { const expandedFolders = useLibraryViewStore((s) => s.expandedFolders); const toggleFolderExpanded = useLibraryViewStore((s) => s.toggleFolderExpanded); const isExpanded = expandedFolders.has(folder.id); @@ -65,7 +72,13 @@ export function FolderRow({ folder, mods, dndDisabled = true, onViewDetails }: F ) : dndDisabled ? (
{mods.map((mod) => ( - + ))}
) : ( @@ -77,6 +90,7 @@ export function FolderRow({ folder, mods, dndDisabled = true, onViewDetails }: F mod={mod} viewMode="list" onViewDetails={onViewDetails} + onEditMetadata={onEditMetadata} /> ))} diff --git a/src/modules/library/components/LibraryContent.tsx b/src/modules/library/components/LibraryContent.tsx index 374e928..5a9fc24 100644 --- a/src/modules/library/components/LibraryContent.tsx +++ b/src/modules/library/components/LibraryContent.tsx @@ -1,6 +1,7 @@ import type { AppError, InstalledMod } from "@/lib/tauri"; import { useLibraryContent, useReorderFolderMods, useReorderMods } from "@/modules/library/api"; +import { EditMetadataDialog } from "./EditMetadataDialog"; import { FolderHeader } from "./FolderHeader"; import { LibraryContextMenu } from "./LibraryContextMenu"; import { LibraryEmptyState, LibraryErrorState, LibraryLoadingState } from "./LibraryStates"; @@ -23,13 +24,14 @@ export function LibraryContent({ error, folderId, }: LibraryContentProps) { - const { viewMode, dndDisabled, contentView, detailsMod, setDetailsMod } = useLibraryContent({ - mods, - searchQuery, - isLoading, - hasError: error !== null, - folderId, - }); + const { viewMode, dndDisabled, contentView, detailsMod, setDetailsMod, editMod, setEditMod } = + useLibraryContent({ + mods, + searchQuery, + isLoading, + hasError: error !== null, + folderId, + }); const reorderMods = useReorderMods(); const reorderFolderMods = useReorderFolderMods(); @@ -68,6 +70,7 @@ export function LibraryContent({ onReorder={(modIds) => reorderMods.mutate(modIds)} disabled={dndDisabled} onViewDetails={setDetailsMod} + onEditMetadata={setEditMod} className={`${gridClass(viewMode)} stagger-enter`} /> @@ -77,6 +80,13 @@ export function LibraryContent({ mod={detailsMod} onClose={() => setDetailsMod(null)} /> + {editMod && ( + !open && setEditMod(null)} + /> + )} ); } @@ -95,6 +105,7 @@ export function LibraryContent({ } disabled={dndDisabled} onViewDetails={setDetailsMod} + onEditMetadata={setEditMod} className={`${gridClass(viewMode)} stagger-enter mt-4`} folderId={contentView.folder.id} /> @@ -105,6 +116,13 @@ export function LibraryContent({ mod={detailsMod} onClose={() => setDetailsMod(null)} /> + {editMod && ( + !open && setEditMod(null)} + /> + )} ); } @@ -121,6 +139,7 @@ export function LibraryContent({ dndDisabled={dndDisabled} onReorder={(modIds) => reorderMods.mutate(modIds)} onViewDetails={setDetailsMod} + onEditMetadata={setEditMod} /> @@ -129,6 +148,13 @@ export function LibraryContent({ mod={detailsMod} onClose={() => setDetailsMod(null)} /> + {editMod && ( + !open && setEditMod(null)} + /> + )} ); } diff --git a/src/modules/library/components/ModCard.tsx b/src/modules/library/components/ModCard.tsx index ca94763..2d6229c 100644 --- a/src/modules/library/components/ModCard.tsx +++ b/src/modules/library/components/ModCard.tsx @@ -1,6 +1,7 @@ import { invoke } from "@tauri-apps/api/core"; import { Copy, + Edit3, EllipsisVertical, FolderOpen, FolderX, @@ -33,9 +34,10 @@ interface ModCardProps { mod: InstalledMod; viewMode: "grid" | "list"; onViewDetails?: (mod: InstalledMod) => void; + onEditMetadata?: (mod: InstalledMod) => void; } -export function ModCard({ mod, viewMode, onViewDetails }: ModCardProps) { +export function ModCard({ mod, viewMode, onViewDetails, onEditMetadata }: ModCardProps) { const { data: thumbnailUrl } = useModThumbnail(mod.id); const toast = useToast(); const toggleMod = useToggleMod(); @@ -207,6 +209,14 @@ export function ModCard({ mod, viewMode, onViewDetails }: ModCardProps) { View Details )} + {!isFlagged && ( + } + onClick={() => onEditMetadata?.(mod)} + > + Edit Metadata + + )} } onClick={handleOpenLocation}> Open Location @@ -354,6 +364,14 @@ export function ModCard({ mod, viewMode, onViewDetails }: ModCardProps) { View Details )} + {!isFlagged && ( + } + onClick={() => onEditMetadata?.(mod)} + > + Edit Metadata + + )} } onClick={handleOpenLocation} diff --git a/src/modules/library/components/SortableFolderRow.tsx b/src/modules/library/components/SortableFolderRow.tsx index 7bdf046..f594ea8 100644 --- a/src/modules/library/components/SortableFolderRow.tsx +++ b/src/modules/library/components/SortableFolderRow.tsx @@ -13,6 +13,7 @@ interface SortableFolderRowProps { mods: InstalledMod[]; sortDisabled?: boolean; onViewDetails?: (mod: InstalledMod) => void; + onEditMetadata?: (mod: InstalledMod) => void; } export function SortableFolderRow({ @@ -21,6 +22,7 @@ export function SortableFolderRow({ mods, sortDisabled, onViewDetails, + onEditMetadata, }: SortableFolderRowProps) { const { attributes, listeners, setNodeRef, transform, transition, isDragging, isOver } = useSortable({ id: sortableId, disabled: sortDisabled ? { draggable: true } : false }); @@ -58,6 +60,7 @@ export function SortableFolderRow({ mods={mods} dndDisabled={false} onViewDetails={onViewDetails} + onEditMetadata={onEditMetadata} /> diff --git a/src/modules/library/components/SortableModCard.tsx b/src/modules/library/components/SortableModCard.tsx index 4a5e5a6..fa770c2 100644 --- a/src/modules/library/components/SortableModCard.tsx +++ b/src/modules/library/components/SortableModCard.tsx @@ -11,9 +11,15 @@ interface SortableModCardProps { mod: InstalledMod; viewMode: "grid" | "list"; onViewDetails?: (mod: InstalledMod) => void; + onEditMetadata?: (mod: InstalledMod) => void; } -export function SortableModCard({ mod, viewMode, onViewDetails }: SortableModCardProps) { +export function SortableModCard({ + mod, + viewMode, + onViewDetails, + onEditMetadata, +}: SortableModCardProps) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: mod.id, }); @@ -37,7 +43,12 @@ export function SortableModCard({ mod, viewMode, onViewDetails }: SortableModCar
)}
- +
); diff --git a/src/modules/library/components/SortableModList.tsx b/src/modules/library/components/SortableModList.tsx index cc447f2..5bf240d 100644 --- a/src/modules/library/components/SortableModList.tsx +++ b/src/modules/library/components/SortableModList.tsx @@ -42,6 +42,7 @@ interface SortableModListProps { onReorder: (modIds: string[]) => void; disabled?: boolean; onViewDetails?: (mod: InstalledMod) => void; + onEditMetadata?: (mod: InstalledMod) => void; className?: string; folderId?: string; } @@ -52,6 +53,7 @@ export function SortableModList({ onReorder, disabled, onViewDetails, + onEditMetadata, className, folderId, }: SortableModListProps) { @@ -75,7 +77,13 @@ export function SortableModList({ return (
{mods.map((mod) => ( - + ))}
); @@ -102,6 +110,7 @@ export function SortableModList({ mod={mod} viewMode={viewMode} onViewDetails={onViewDetails} + onEditMetadata={onEditMetadata} /> ))} diff --git a/src/modules/library/components/UnifiedDndGrid.tsx b/src/modules/library/components/UnifiedDndGrid.tsx index 2ae2506..34553e3 100644 --- a/src/modules/library/components/UnifiedDndGrid.tsx +++ b/src/modules/library/components/UnifiedDndGrid.tsx @@ -30,6 +30,7 @@ interface UnifiedDndGridProps { dndDisabled: boolean; onReorder: (modIds: string[]) => void; onViewDetails?: (mod: InstalledMod) => void; + onEditMetadata?: (mod: InstalledMod) => void; } export function UnifiedDndGrid({ @@ -40,6 +41,7 @@ export function UnifiedDndGrid({ dndDisabled, onReorder, onViewDetails, + onEditMetadata, }: UnifiedDndGridProps) { const hasMountedRef = useRef(false); const stagger = !hasMountedRef.current ? " stagger-enter" : ""; @@ -53,6 +55,7 @@ export function UnifiedDndGrid({ modsByFolder={modsByFolder} viewMode={viewMode} onViewDetails={onViewDetails} + onEditMetadata={onEditMetadata} staggerClass={stagger} /> ); @@ -66,6 +69,7 @@ export function UnifiedDndGrid({ viewMode={viewMode} onReorder={onReorder} onViewDetails={onViewDetails} + onEditMetadata={onEditMetadata} /> ); } @@ -76,6 +80,7 @@ interface StaticGridProps { modsByFolder: Map; viewMode: "grid" | "list"; onViewDetails?: (mod: InstalledMod) => void; + onEditMetadata?: (mod: InstalledMod) => void; staggerClass?: string; } @@ -85,6 +90,7 @@ function StaticGrid({ modsByFolder, viewMode, onViewDetails, + onEditMetadata, staggerClass = "", }: StaticGridProps) { return ( @@ -99,13 +105,20 @@ function StaticGrid({ mods={folderMods} dndDisabled onViewDetails={onViewDetails} + onEditMetadata={onEditMetadata} /> ); } return ; })} {rootMods.map((mod) => ( - + ))} ); @@ -118,6 +131,7 @@ interface DndGridProps { viewMode: "grid" | "list"; onReorder: (modIds: string[]) => void; onViewDetails?: (mod: InstalledMod) => void; + onEditMetadata?: (mod: InstalledMod) => void; } function DndGrid({ @@ -127,6 +141,7 @@ function DndGrid({ viewMode, onReorder, onViewDetails, + onEditMetadata, }: DndGridProps) { const { folderLocalOrder, @@ -178,6 +193,7 @@ function DndGrid({ mods={folderMods} sortDisabled={isDraggingMod || isDraggingFolderMod} onViewDetails={onViewDetails} + onEditMetadata={onEditMetadata} /> ); } @@ -198,6 +214,7 @@ function DndGrid({ mod={mod} viewMode={viewMode} onViewDetails={onViewDetails} + onEditMetadata={onEditMetadata} /> ))}