Skip to content

Commit f40e9fc

Browse files
feat: mod options order
close #51
1 parent 44803e3 commit f40e9fc

File tree

8 files changed

+253
-3
lines changed

8 files changed

+253
-3
lines changed

resources/dist.rc

1.69 KB
Binary file not shown.

src/blacklist.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ pub struct ModBlacklist {
1616
pub struct ModBlacklistProfile {
1717
pub name: String,
1818
pub mods: Vec<ModBlacklist>,
19+
#[serde(default)]
20+
pub mod_options_order: Vec<String>,
1921
}
2022

2123
pub fn apply_mod_blacklist_profile(
@@ -47,6 +49,49 @@ pub fn apply_mod_blacklist_profile(
4749
.join("\n")),
4850
)?;
4951

52+
write_mod_options_order(game_path, &profile.mod_options_order)?;
53+
54+
Ok(())
55+
}
56+
57+
/// Write modoptionsorder.txt from the given ordered file list.
58+
fn write_mod_options_order(game_path: &String, order: &[String]) -> anyhow::Result<()> {
59+
let path = Path::new(game_path).join("Mods").join("modoptionsorder.txt");
60+
if order.is_empty() {
61+
// If no order specified, remove the file so Everest uses alphabetical order
62+
if path.exists() {
63+
fs::remove_file(&path)?;
64+
}
65+
return Ok(());
66+
}
67+
let content = format!(
68+
"# Mod Options order\n# This file is generated by CeleMod\n\n{}\n",
69+
order.join("\n")
70+
);
71+
fs::write(path, content)?;
72+
Ok(())
73+
}
74+
75+
/// Update the mod_options_order in a profile and immediately apply it to modoptionsorder.txt.
76+
pub fn set_mod_options_order(
77+
game_path: &String,
78+
profile_name: &String,
79+
order: Vec<String>,
80+
) -> anyhow::Result<()> {
81+
let mut profile = get_mod_blacklist_profiles(game_path)
82+
.into_iter()
83+
.find(|v| &v.name == profile_name)
84+
.context("Profile not found")?;
85+
86+
profile.mod_options_order = order;
87+
88+
let profile_path = Path::new(game_path)
89+
.join("celemod_blacklist_profiles")
90+
.join(format!("{}.json", profile_name));
91+
fs::write(&profile_path, serde_json::to_string_pretty(&profile).unwrap())?;
92+
93+
write_mod_options_order(game_path, &profile.mod_options_order)?;
94+
5095
Ok(())
5196
}
5297

@@ -110,6 +155,7 @@ pub fn get_mod_blacklist_profiles(game_path: &String) -> Vec<ModBlacklistProfile
110155
}
111156
})
112157
.collect(),
158+
mod_options_order: vec![],
113159
};
114160

115161
fs::write(
@@ -127,6 +173,7 @@ pub fn get_mod_blacklist_profiles(game_path: &String) -> Vec<ModBlacklistProfile
127173
let profile = ModBlacklistProfile {
128174
name: "Default".to_string(),
129175
mods: vec![],
176+
mod_options_order: vec![],
130177
};
131178
let blacklist_path = Path::new(game_path).join("celemod_blacklist_profiles");
132179
fs::create_dir_all(&blacklist_path).unwrap();
@@ -193,6 +240,7 @@ pub fn new_mod_blacklist_profile(game_path: &String, profile_name: &String) -> a
193240
serde_json::to_string_pretty(&ModBlacklistProfile {
194241
name: profile_name.clone(),
195242
mods: vec![],
243+
mod_options_order: vec![],
196244
})
197245
.unwrap(),
198246
)?;
@@ -232,6 +280,12 @@ pub fn sync_blacklist_profile_from_file(
232280
}
233281
let data = fs::read_to_string(blacklist)?;
234282
let mods = get_installed_mods_sync(game_path.clone() + "/Mods");
283+
// Preserve existing mod_options_order if profile already exists
284+
let existing_order = get_mod_blacklist_profiles(game_path)
285+
.into_iter()
286+
.find(|v| &v.name == profile_name)
287+
.map(|v| v.mod_options_order)
288+
.unwrap_or_default();
235289
let profile = ModBlacklistProfile {
236290
name: profile_name.clone(),
237291
mods: data
@@ -249,6 +303,7 @@ pub fn sync_blacklist_profile_from_file(
249303
file: v.to_string(),
250304
})
251305
.collect(),
306+
mod_options_order: existing_order,
252307
};
253308
let blacklist_path = Path::new(game_path)
254309
.join("celemod_blacklist_profiles")

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,5 +229,7 @@
229229
"将要删除:": "将要删除:",
230230
"以下 Mod 将不再被任何 Mod 引用,是否一并删除?": "以下 Mod 将不再被任何 Mod 引用,是否一并删除?",
231231
"确认删除": "确认删除",
232-
"补全缺失依赖 ({count})": "Fix Missing Deps ({count})"
232+
"补全缺失依赖 ({count})": "Fix Missing Deps ({count})",
233+
"Mod Options 顺序": "Mod Options Order",
234+
"置顶": "Pin to top"
233235
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,5 +229,7 @@
229229
"将要删除:": "将要删除:",
230230
"以下 Mod 将不再被任何 Mod 引用,是否一并删除?": "以下 Mod 将不再被任何 Mod 引用,是否一并删除?",
231231
"确认删除": "确认删除",
232-
"补全缺失依赖 ({count})": "补全缺失依赖 ({count})"
232+
"补全缺失依赖 ({count})": "补全缺失依赖 ({count})",
233+
"Mod Options 顺序": "Mod Options 顺序",
234+
"置顶": "置顶"
233235
}

src/celemod-ui/src/ipc/blacklist.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ export interface ModBlacklist {
66
export interface ModBlacklistProfile {
77
name: string,
88
mods: ModBlacklist[],
9-
}
9+
mod_options_order: string[],
10+
}

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

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,85 @@
161161

162162

163163
}
164+
165+
.mod-options-order {
166+
margin-top: 12px;
167+
border-top: 1px solid rgba(255, 255, 255, 0.1);
168+
padding-top: 8px;
169+
170+
.moo-header {
171+
cursor: pointer;
172+
font-size: 13px;
173+
font-weight: 600;
174+
color: rgba(255, 255, 255, 0.7);
175+
padding: 2px 0 4px;
176+
177+
&:hover {
178+
color: rgba(255, 255, 255, 0.9);
179+
}
180+
181+
span {
182+
margin-left: 4px;
183+
}
184+
}
185+
186+
.moo-list {
187+
margin-top: 4px;
188+
max-height: 320px;
189+
overflow-y: auto;
190+
191+
.moo-item {
192+
position: relative;
193+
padding: 3px 72px 3px 4px;
194+
border-radius: 4px;
195+
height: 20px;
196+
197+
&:hover {
198+
background: rgba(255, 255, 255, 0.05);
199+
}
200+
201+
.moo-name {
202+
display: inline-block;
203+
font-size: 11px;
204+
color: rgba(255, 255, 255, 0.7);
205+
width: 100%;
206+
overflow: hidden;
207+
text-overflow: ellipsis;
208+
white-space: nowrap;
209+
}
210+
211+
.moo-btns {
212+
position: absolute;
213+
right: 4px;
214+
top: 2px;
215+
flow: horizontal-flow;
216+
217+
button {
218+
width: 20px;
219+
height: 20px;
220+
padding: 0;
221+
border: none;
222+
border-radius: 3px;
223+
background: rgba(255, 255, 255, 0.1);
224+
color: rgba(255, 255, 255, 0.6);
225+
cursor: pointer;
226+
font-size: 11px;
227+
margin-left: 2px;
228+
229+
&:hover {
230+
background: rgba(255, 255, 255, 0.2);
231+
color: white;
232+
}
233+
234+
&.disabled {
235+
opacity: 0.25;
236+
cursor: default;
237+
}
238+
}
239+
}
240+
}
241+
}
242+
}
164243
}
165244

166245
.title {

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

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,89 @@ const formatSize = (size: number) => {
449449

450450
let lastApplyReq = -1;
451451

452+
const ModOptionsOrderPanel = ({
453+
gamePath,
454+
currentProfileName,
455+
currentProfile,
456+
installedMods,
457+
onOrderChange,
458+
}: {
459+
gamePath: string;
460+
currentProfileName: string;
461+
currentProfile: import('../ipc/blacklist').ModBlacklistProfile | null;
462+
installedMods: import('../states').BackendModInfo[];
463+
onOrderChange: (order: string[]) => void;
464+
}) => {
465+
const [expanded, setExpanded] = useState(false);
466+
467+
const order: string[] = currentProfile?.mod_options_order ?? [];
468+
469+
const allFiles = useMemo(() => {
470+
const files = installedMods.map((m) => m.file);
471+
const inOrder = order.filter((f) => files.includes(f));
472+
const rest = files
473+
.filter((f) => !order.includes(f))
474+
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
475+
return [...inOrder, ...rest];
476+
}, [installedMods, order]);
477+
478+
const applyOrder = (newOrder: string[]) => {
479+
onOrderChange(newOrder);
480+
callRemote('set_mod_options_order', gamePath, currentProfileName, JSON.stringify(newOrder));
481+
};
482+
483+
const move = (index: number, direction: -1 | 1) => {
484+
const target = index + direction;
485+
if (target < 0 || target >= allFiles.length) return;
486+
const next = [...allFiles];
487+
[next[index], next[target]] = [next[target], next[index]];
488+
applyOrder(next);
489+
};
490+
491+
const moveToTop = (index: number) => {
492+
if (index === 0) return;
493+
const next = [...allFiles];
494+
const [item] = next.splice(index, 1);
495+
next.unshift(item);
496+
applyOrder(next);
497+
};
498+
499+
if (!currentProfile) return null;
500+
501+
return (
502+
<div className="mod-options-order">
503+
<div className="moo-header" onClick={() => setExpanded((v) => !v)}>
504+
<Icon name={expanded ? 'i-down' : 'i-right'} />
505+
<span>{_i18n.t('Mod Options 顺序')}</span>
506+
</div>
507+
{expanded && (
508+
<div className="moo-list">
509+
{allFiles.map((file, i) => (
510+
<div className="moo-item" key={file}>
511+
<span className="moo-name" title={file}>{file}</span>
512+
<span className="moo-btns">
513+
<button
514+
className={i === 0 ? 'disabled' : ''}
515+
onClick={() => moveToTop(i)}
516+
title={_i18n.t('置顶')}
517+
></button>
518+
<button
519+
className={i === 0 ? 'disabled' : ''}
520+
onClick={() => move(i, -1)}
521+
></button>
522+
<button
523+
className={i === allFiles.length - 1 ? 'disabled' : ''}
524+
onClick={() => move(i, 1)}
525+
></button>
526+
</span>
527+
</div>
528+
))}
529+
</div>
530+
)}
531+
</div>
532+
);
533+
};
534+
452535
export const Manage = () => {
453536
const noEverest = enforceEverest();
454537
if (noEverest) return noEverest;
@@ -1280,6 +1363,19 @@ export const Manage = () => {
12801363
{_i18n.t('新建')}
12811364
</Button>
12821365
</div>
1366+
1367+
<ModOptionsOrderPanel
1368+
gamePath={gamePath}
1369+
currentProfileName={currentProfileName}
1370+
currentProfile={currentProfile}
1371+
installedMods={installedMods}
1372+
onOrderChange={(newOrder) => {
1373+
if (currentProfile) {
1374+
currentProfile.mod_options_order = newOrder;
1375+
setCurrentProfile({ ...currentProfile });
1376+
}
1377+
}}
1378+
/>
12831379
</div>
12841380
</modListContext.Provider>
12851381
</div>

src/main.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -867,6 +867,20 @@ impl Handler {
867867
}
868868
}
869869

870+
fn set_mod_options_order(&self, game_path: String, profile_name: String, order_json: String) -> String {
871+
let order: Vec<String> = match serde_json::from_str(&order_json) {
872+
Ok(v) => v,
873+
Err(e) => return format!("Failed to parse order: {}", e),
874+
};
875+
let result = blacklist::set_mod_options_order(&game_path, &profile_name, order);
876+
if let Err(e) = result {
877+
eprintln!("Failed to set mod options order: {}", e);
878+
format!("Failed to set mod options order: {}", e)
879+
} else {
880+
"Success".to_string()
881+
}
882+
}
883+
870884
fn open_url(&self, url: String) {
871885
if let Err(e) = open::that(url) {
872886
eprintln!("Failed to open url: {}", e);
@@ -1097,6 +1111,7 @@ impl sciter::EventHandler for Handler {
10971111
fn sync_blacklist_profile_from_file(String, String);
10981112
fn is_using_cache();
10991113
fn get_database_path();
1114+
fn set_mod_options_order(String, String, String);
11001115
}
11011116
}
11021117

0 commit comments

Comments
 (0)