Skip to content

Commit cea8121

Browse files
feat: delete mods
1 parent ad35806 commit cea8121

File tree

8 files changed

+365
-20
lines changed

8 files changed

+365
-20
lines changed

resources/dist.rc

-2.12 MB
Binary file not shown.

src/celemod-ui/locales/en-US.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,5 +219,14 @@
219219
"请先选择一个 Profile 后再启用/禁用 Mod": "Please select a Profile before enabling/disabling Mod",
220220
"确定": "Sure",
221221
"自动禁用新安装的Mod": "Automatically disable newly installed mods",
222-
"显示详细信息": "Show details"
222+
"显示详细信息": "Show details",
223+
"离线模式": "离线模式",
224+
"正在使用缓存的 Mod 数据,可能已过期或不完整": "正在使用缓存的 Mod 数据,可能已过期或不完整",
225+
"删除 Mod": "删除 Mod",
226+
"删除 Mod 确认": "删除 Mod 确认",
227+
"⚠️ 警告:以下 Mod 依赖此 Mod": "⚠️ 警告:以下 Mod 依赖此 Mod",
228+
"(已禁用)": "(已禁用)",
229+
"将要删除:": "将要删除:",
230+
"以下 Mod 将不再被任何 Mod 引用,是否一并删除?": "以下 Mod 将不再被任何 Mod 引用,是否一并删除?",
231+
"确认删除": "确认删除"
223232
}

src/celemod-ui/locales/pt-BR.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,5 +219,14 @@
219219
"请先选择一个 Profile 后再启用/禁用 Mod": "Selecione um perfil antes de ativar/desativar o mod",
220220
"确定": "Claro",
221221
"自动禁用新安装的Mod": "Desativar automaticamente mods recém -instalados",
222-
"显示详细信息": "Mostre detalhes"
222+
"显示详细信息": "Mostre detalhes",
223+
"离线模式": "离线模式",
224+
"正在使用缓存的 Mod 数据,可能已过期或不完整": "正在使用缓存的 Mod 数据,可能已过期或不完整",
225+
"删除 Mod": "删除 Mod",
226+
"删除 Mod 确认": "删除 Mod 确认",
227+
"⚠️ 警告:以下 Mod 依赖此 Mod": "⚠️ 警告:以下 Mod 依赖此 Mod",
228+
"(已禁用)": "(已禁用)",
229+
"将要删除:": "将要删除:",
230+
"以下 Mod 将不再被任何 Mod 引用,是否一并删除?": "以下 Mod 将不再被任何 Mod 引用,是否一并删除?",
231+
"确认删除": "确认删除"
223232
}

src/celemod-ui/locales/ru-RU.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,5 +219,14 @@
219219
"请先选择一个 Profile 后再启用/禁用 Mod": "Пожалуйста, выберите профиль, прежде чем включить/отключить мод",
220220
"确定": "Конечно",
221221
"自动禁用新安装的Mod": "Автоматически отключить вновь установленные моды",
222-
"显示详细信息": "Показать детали"
222+
"显示详细信息": "Показать детали",
223+
"离线模式": "离线模式",
224+
"正在使用缓存的 Mod 数据,可能已过期或不完整": "正在使用缓存的 Mod 数据,可能已过期或不完整",
225+
"删除 Mod": "删除 Mod",
226+
"删除 Mod 确认": "删除 Mod 确认",
227+
"⚠️ 警告:以下 Mod 依赖此 Mod": "⚠️ 警告:以下 Mod 依赖此 Mod",
228+
"(已禁用)": "(已禁用)",
229+
"将要删除:": "将要删除:",
230+
"以下 Mod 将不再被任何 Mod 引用,是否一并删除?": "以下 Mod 将不再被任何 Mod 引用,是否一并删除?",
231+
"确认删除": "确认删除"
223232
}

src/celemod-ui/locales/zh-CN.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,5 +219,14 @@
219219
"请先选择一个 Profile 后再启用/禁用 Mod": "请先选择一个 Profile 后再启用/禁用 Mod",
220220
"确定": "确定",
221221
"自动禁用新安装的Mod": "自动禁用新安装的Mod",
222-
"显示详细信息": "显示详细信息"
222+
"显示详细信息": "显示详细信息",
223+
"离线模式": "离线模式",
224+
"正在使用缓存的 Mod 数据,可能已过期或不完整": "正在使用缓存的 Mod 数据,可能已过期或不完整",
225+
"删除 Mod": "删除 Mod",
226+
"删除 Mod 确认": "删除 Mod 确认",
227+
"⚠️ 警告:以下 Mod 依赖此 Mod": "⚠️ 警告:以下 Mod 依赖此 Mod",
228+
"(已禁用)": "(已禁用)",
229+
"将要删除:": "将要删除:",
230+
"以下 Mod 将不再被任何 Mod 引用,是否一并删除?": "以下 Mod 将不再被任何 Mod 引用,是否一并删除?",
231+
"确认删除": "确认删除"
223232
}

src/celemod-ui/src/routes/Manage.scss

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,4 +201,153 @@
201201
color: rgba(255, 255, 255, 0.4);
202202
margin-top: 6px;
203203
}
204+
205+
.m-mod {
206+
position: relative;
207+
208+
.delete-btn {
209+
position: absolute;
210+
right: 10px;
211+
margin-top: 5px;
212+
opacity: 0.3;
213+
cursor: pointer;
214+
padding: 5px;
215+
border-radius: 3px;
216+
transition: opacity 0.2s, color 0.2s;
217+
218+
&:hover {
219+
opacity: 1;
220+
color: #ff4444;
221+
}
222+
}
223+
}
224+
}
225+
226+
.delete-mod-popup {
227+
width: 500px;
228+
max-width: 90vw;
229+
max-height: 80vh;
230+
overflow-y: auto;
231+
padding: 20px;
232+
background: theme.$bg;
233+
border-radius: 10px;
234+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
235+
236+
.title {
237+
font-size: 18px;
238+
font-weight: bold;
239+
margin-bottom: 15px;
240+
color: theme.$fg;
241+
}
242+
243+
.warning-section {
244+
margin-bottom: 15px;
245+
padding: 10px;
246+
background: rgba(255, 68, 68, 0.1);
247+
border: 1px solid rgba(255, 68, 68, 0.3);
248+
border-radius: 5px;
249+
250+
.warning-title {
251+
font-weight: bold;
252+
color: #ff4444;
253+
margin-bottom: 8px;
254+
}
255+
256+
.dependent-mods {
257+
max-height: 100px;
258+
overflow-y: auto;
259+
260+
.dependent-mod {
261+
padding: 3px 0;
262+
color: theme.$fg;
263+
opacity: 0.8;
264+
}
265+
}
266+
}
267+
268+
.delete-target {
269+
margin-bottom: 15px;
270+
padding: 10px;
271+
background: rgba(255, 165, 0, 0.1);
272+
border: 1px solid rgba(255, 165, 0, 0.3);
273+
border-radius: 5px;
274+
color: theme.$fg;
275+
276+
strong {
277+
color: #ffa500;
278+
}
279+
}
280+
281+
.orphan-section {
282+
margin-bottom: 20px;
283+
284+
.orphan-title {
285+
font-weight: bold;
286+
margin-bottom: 10px;
287+
color: theme.$fg;
288+
}
289+
290+
.orphan-list {
291+
max-height: 200px;
292+
overflow-y: auto;
293+
border: 1px solid rgba(255, 255, 255, 0.2);
294+
border-radius: 5px;
295+
padding: 10px;
296+
background: rgba(255, 255, 255, 0.05);
297+
298+
.orphan-item {
299+
display: block;
300+
align-items: center;
301+
padding: 5px 0;
302+
color: theme.$fg;
303+
cursor: pointer;
304+
305+
input[type="checkbox"] {
306+
margin-right: 10px;
307+
}
308+
309+
span {
310+
opacity: 0.9;
311+
}
312+
313+
&:hover {
314+
background: rgba(255, 255, 255, 0.1);
315+
margin: 0 -10px;
316+
padding: 5px 10px;
317+
}
318+
}
319+
}
320+
}
321+
322+
.buttons {
323+
margin-top: 20px;
324+
325+
button {
326+
margin-right: 10px;
327+
padding: 8px 16px;
328+
border: none;
329+
border-radius: 5px;
330+
cursor: pointer;
331+
font-weight: bold;
332+
transition: background-color 0.2s;
333+
334+
&:first-child {
335+
background: theme.$bg2;
336+
color: theme.$fg;
337+
338+
&:hover {
339+
background: theme.$bg3;
340+
}
341+
}
342+
343+
&.delete-confirm {
344+
background: #ff4444;
345+
color: white;
346+
347+
&:hover {
348+
background: #ff6666;
349+
}
350+
}
351+
}
352+
}
204353
}

src/celemod-ui/src/routes/Manage.tsx

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)