@@ -60,6 +60,7 @@ const modListContext = createContext<{
6060 switchMod : ( id : string , enabled : boolean , recursive ?: boolean ) => void ;
6161 switchProfile : ( name : string ) => void ;
6262 removeProfile : ( name : string ) => void ;
63+ deleteMod : ( name : string ) => void ;
6364 modFolder : string ;
6465 gamePath : string ;
6566 currentProfileName : string ;
@@ -187,6 +188,7 @@ const ModLocal = ({
187188} : ModInfo & { optional ?: boolean } ) => {
188189 const { download } = useGlobalContext ( ) ;
189190 const [ expanded , setExpanded ] = useState ( false ) ;
191+ const [ hovered , setHovered ] = useState ( false ) ;
190192
191193 const ctx = useContext ( modListContext ) ;
192194
@@ -233,7 +235,12 @@ const ModLocal = ({
233235 } , [ editingComment ] ) ;
234236
235237 return (
236- < div className = { `m-mod ${ enabled && 'enabled' } ` } key = { id } >
238+ < div
239+ className = { `m-mod ${ enabled && 'enabled' } ` }
240+ key = { id }
241+ onMouseEnter = { ( ) => setHovered ( true ) }
242+ onMouseLeave = { ( ) => setHovered ( false ) }
243+ >
237244 < span
238245 className = { `expandBtn ${ expanded && 'expanded' } ${ hasDeps && 'clickable'
239246 } `}
@@ -373,6 +380,15 @@ const ModLocal = ({
373380 [{ formatSize ( size ) } · { file } ]
374381 </ span >
375382 ) }
383+ { hovered && (
384+ < span
385+ className = "delete-btn"
386+ onClick = { ( ) => ctx ?. deleteMod ( name ) }
387+ title = { _i18n . t ( '删除 Mod' ) }
388+ >
389+ < Icon name = "delete" />
390+ </ span >
391+ ) }
376392 { ( ! optional || ctx ?. fullTree ) && expanded && (
377393 < div className = { `childTree ${ expanded && 'expanded' } ` } >
378394 { dependencies . map ( ( v ) => (
@@ -867,6 +883,102 @@ export const Manage = () => {
867883 setProfilesCallback ( ( profiles ) => profiles . concat ( { name, mods : [ ] } ) ) ;
868884 setCurrentProfileName ( name ) ;
869885 } ,
886+ deleteMod : ( name : string ) => {
887+ const modToDelete = installedModMap . get ( name ) ;
888+ if ( ! modToDelete ) return ;
889+
890+ // Find mods that depend on this mod
891+ const dependentMods = modToDelete . dependedBy ;
892+
893+ // Find orphaned mods (mods that will have no references after deletion)
894+ const orphanedMods : ModInfo [ ] = [ ] ;
895+ const checkOrphans = ( mod : ModInfo ) => {
896+ for ( const dep of mod . dependencies ) {
897+ if ( '_missing' in dep ) continue ;
898+ const depInfo = installedModMap . get ( dep . name ) ;
899+ if ( ! depInfo ) continue ;
900+
901+ // Check if this dependency will be orphaned after deletion
902+ const remainingDependents = depInfo . dependedBy . filter (
903+ m => m . name !== name && ! orphanedMods . includes ( m )
904+ ) ;
905+
906+ if ( remainingDependents . length === 0 && ! orphanedMods . includes ( depInfo ) ) {
907+ orphanedMods . push ( depInfo ) ;
908+ checkOrphans ( depInfo ) ;
909+ }
910+ }
911+ } ;
912+ checkOrphans ( modToDelete ) ;
913+
914+ createPopup ( ( ) => {
915+ const { hide } = useContext ( PopupContext ) ;
916+ const [ selectedOrphans , setSelectedOrphans ] = useState < string [ ] > ( orphanedMods . map ( m => m . name ) ) ;
917+
918+ const handleDelete = ( ) => {
919+ const modsToDelete = [ name , ...selectedOrphans ] ;
920+ callRemote ( 'delete_mods' , gamePath , JSON . stringify ( modsToDelete ) , ( ) => {
921+ manageCtx . reloadMods ( ) ;
922+ hide ( ) ;
923+ } ) ;
924+ } ;
925+
926+ return (
927+ < div className = "delete-mod-popup" >
928+ < div className = "title" > { _i18n . t ( '删除 Mod 确认' ) } </ div >
929+
930+ { dependentMods . length > 0 && (
931+ < div className = "warning-section" >
932+ < div className = "warning-title" > { _i18n . t ( '⚠️ 警告:以下 Mod 依赖此 Mod' ) } </ div >
933+ < div className = "dependent-mods" >
934+ { dependentMods . map ( mod => (
935+ < div key = { mod . name } className = "dependent-mod" >
936+ { mod . name } { mod . version } { mod . enabled ? '' : _i18n . t ( '(已禁用)' ) }
937+ </ div >
938+ ) ) }
939+ </ div >
940+ </ div >
941+ ) }
942+
943+ < div className = "delete-target" >
944+ { _i18n . t ( '将要删除:' ) } < strong > { name } { modToDelete . version } </ strong >
945+ </ div >
946+
947+ { orphanedMods . length > 0 && (
948+ < div className = "orphan-section" >
949+ < div className = "orphan-title" > { _i18n . t ( '以下 Mod 将不再被任何 Mod 引用,是否一并删除?' ) } </ div >
950+ < div className = "orphan-list" >
951+ { orphanedMods . map ( mod => (
952+ < label key = { mod . name } className = "orphan-item" >
953+ < input
954+ type = "checkbox"
955+ checked = { selectedOrphans . includes ( mod . name ) }
956+ onChange = { ( e ) => {
957+ const target = e . target as HTMLInputElement ;
958+ if ( target . checked ) {
959+ setSelectedOrphans ( [ ...selectedOrphans , mod . name ] ) ;
960+ } else {
961+ setSelectedOrphans ( selectedOrphans . filter ( n => n !== mod . name ) ) ;
962+ }
963+ } }
964+ />
965+ < span > { mod . name } { mod . version } </ span >
966+ </ label >
967+ ) ) }
968+ </ div >
969+ </ div >
970+ ) }
971+
972+ < div className = "buttons" >
973+ < button onClick = { hide } > { _i18n . t ( '取消' ) } </ button >
974+ < button className = "delete-confirm" onClick = { handleDelete } >
975+ { _i18n . t ( '确认删除' ) }
976+ </ button >
977+ </ div >
978+ </ div >
979+ ) ;
980+ } ) ;
981+ } ,
870982 gamePath,
871983 modFolder : modPath ,
872984 currentProfile,
0 commit comments