Rework the main page to be more rimworld like

This commit is contained in:
2025-03-16 00:17:27 +01:00
parent c5cbbe5b7e
commit 4a92ba7340

View File

@@ -55,8 +55,7 @@ class _ModManagerHomePageState extends State<ModManagerHomePage> {
int _selectedIndex = 0; int _selectedIndex = 0;
final List<Widget> _pages = [ final List<Widget> _pages = [
const ModListPage(), const ModManagerPage(),
const LoadOrderPage(),
const TroubleshootingPage(), const TroubleshootingPage(),
]; ];
@@ -74,10 +73,6 @@ class _ModManagerHomePageState extends State<ModManagerHomePage> {
}, },
items: const [ items: const [
BottomNavigationBarItem(icon: Icon(Icons.extension), label: 'Mods'), BottomNavigationBarItem(icon: Icon(Icons.extension), label: 'Mods'),
BottomNavigationBarItem(
icon: Icon(Icons.reorder),
label: 'Load Order',
),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: Icon(Icons.build), icon: Icon(Icons.build),
label: 'Troubleshoot', label: 'Troubleshoot',
@@ -88,21 +83,31 @@ class _ModManagerHomePageState extends State<ModManagerHomePage> {
} }
} }
// Page to display all installed mods with enable/disable toggles // Combined page for mod management with two-panel layout
class ModListPage extends StatefulWidget { class ModManagerPage extends StatefulWidget {
const ModListPage({super.key}); const ModManagerPage({super.key});
@override @override
State<ModListPage> createState() => _ModListPageState(); State<ModManagerPage> createState() => _ModManagerPageState();
} }
class _ModListPageState extends State<ModListPage> { class _ModManagerPageState extends State<ModManagerPage> {
List<Mod> _loadedMods = []; // For all available mods (left panel)
List<Mod> _availableMods = [];
// For active mods (right panel)
List<Mod> _activeMods = [];
bool _isLoading = false; bool _isLoading = false;
String _loadingStatus = ''; String _statusMessage = '';
int _totalModsFound = 0; int _totalModsFound = 0;
bool _skipFileCount = bool _skipFileCount = false;
false; // Skip file counting by default for faster loading bool _hasCycles = false;
List<String>? _cycleInfo;
List<List<String>> _incompatibleMods = [];
final TextEditingController _searchController = TextEditingController();
String _searchQuery = '';
@override @override
void initState() { void initState() {
@@ -111,14 +116,30 @@ class _ModListPageState extends State<ModListPage> {
if (modManager.modsLoaded) { if (modManager.modsLoaded) {
_loadModsFromGlobalState(); _loadModsFromGlobalState();
} }
_searchController.addListener(() {
setState(() {
_searchQuery = _searchController.text.toLowerCase();
});
});
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
} }
void _loadModsFromGlobalState() { void _loadModsFromGlobalState() {
setState(() { setState(() {
_loadedMods = modManager.mods.values.toList(); // Get all mods for the left panel (sorted alphabetically)
_loadedMods.sort((a, b) => a.name.compareTo(b.name)); _availableMods = modManager.mods.values.toList();
_availableMods.sort((a, b) => a.name.compareTo(b.name));
// Get active mods for the right panel (in load order)
_activeMods = modManager.mods.values.where((m) => m.enabled).toList();
_isLoading = false; _isLoading = false;
_loadingStatus = modManager.loadingStatus; _statusMessage = modManager.loadingStatus;
_totalModsFound = modManager.totalModsFound; _totalModsFound = modManager.totalModsFound;
}); });
} }
@@ -127,9 +148,28 @@ class _ModListPageState extends State<ModListPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: body:
_loadedMods.isEmpty && !_isLoading _isLoading && _availableMods.isEmpty
? _buildLoadingView()
: _availableMods.isEmpty
? _buildEmptyState() ? _buildEmptyState()
: _buildModList(), : _buildSplitView(),
);
}
Widget _buildLoadingView() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(
_statusMessage,
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
],
),
); );
} }
@@ -140,7 +180,10 @@ class _ModListPageState extends State<ModListPage> {
children: [ children: [
const Icon(Icons.extension, size: 64), const Icon(Icons.extension, size: 64),
const SizedBox(height: 16), const SizedBox(height: 16),
Text('Mod List', style: Theme.of(context).textTheme.headlineMedium), Text(
'Mod Manager',
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
'Ready to scan for Rimworld mods.', 'Ready to scan for Rimworld mods.',
@@ -171,47 +214,174 @@ class _ModListPageState extends State<ModListPage> {
); );
} }
Widget _buildModList() { Widget _buildSplitView() {
// Filter both available and active mods based on search query
final filteredAvailableMods =
_searchQuery.isEmpty
? _availableMods
: _availableMods
.where(
(mod) =>
mod.name.toLowerCase().contains(_searchQuery) ||
mod.id.toLowerCase().contains(_searchQuery),
)
.toList();
final filteredActiveMods =
_searchQuery.isEmpty
? _activeMods
: _activeMods
.where(
(mod) =>
mod.name.toLowerCase().contains(_searchQuery) ||
mod.id.toLowerCase().contains(_searchQuery),
)
.toList();
return Column( return Column(
children: [ children: [
if (_isLoading)
Padding( Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(8.0),
child: Column( child: Row(
children: [ children: [
const CircularProgressIndicator(), // Search field
const SizedBox(height: 16), Expanded(
Text( child: TextField(
_loadingStatus, controller: _searchController,
style: Theme.of(context).textTheme.bodyMedium, decoration: InputDecoration(
textAlign: TextAlign.center, hintText: 'Search mods by name or ID...',
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
),
suffixIcon:
_searchQuery.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
},
)
: null,
),
),
),
const SizedBox(width: 8),
// Reload button
IconButton(
icon: const Icon(Icons.refresh),
tooltip: 'Reload mods',
onPressed: _startLoadingMods,
),
const SizedBox(width: 8),
// Auto-sort button
ElevatedButton.icon(
icon: const Icon(Icons.sort),
label: const Text('Auto-Sort'),
onPressed: _sortActiveMods,
),
const SizedBox(width: 8),
// Save button
ElevatedButton.icon(
icon: const Icon(Icons.save),
label: const Text('Save'),
onPressed: _saveModOrder,
), ),
], ],
), ),
), ),
if (!_isLoading && _loadedMods.isEmpty)
Center( // Status message
child: Padding( if (!_isLoading && _statusMessage.isNotEmpty)
padding: const EdgeInsets.all(16.0), Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
child: Text( child: Text(
'No mods found. Try reloading.', _statusMessage,
style: TextStyle(
color:
_hasCycles || _incompatibleMods.isNotEmpty
? Colors.orange
: Colors.green,
),
),
),
// Cycle warnings
if (_hasCycles && _cycleInfo != null)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
'Dependency cycle detected: ${_cycleInfo!.join(" -> ")}',
style: const TextStyle(color: Colors.orange),
),
),
// Incompatible mod warnings
if (_incompatibleMods.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
children: [
const Icon(Icons.warning, color: Colors.orange, size: 16),
const SizedBox(width: 4),
Text(
'${_incompatibleMods.length} incompatible mod pairs found',
style: const TextStyle(color: Colors.orange),
),
],
),
),
// Main split view
Expanded(
child: Row(
children: [
// LEFT PANEL - All available mods (alphabetical)
Expanded(
child: Card(
margin: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
color: Theme.of(context).primaryColor,
padding: const EdgeInsets.all(8.0),
child: Text(
'Available Mods (${filteredAvailableMods.length}${_searchQuery.isNotEmpty ? '/' + _availableMods.length.toString() : ''})',
style: const TextStyle(fontWeight: FontWeight.bold),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
), ),
if (_searchQuery.isNotEmpty)
Padding(
padding: const EdgeInsets.all(4.0),
child: Text(
'Searching: "$_searchQuery"',
style: TextStyle(
fontSize: 12,
fontStyle: FontStyle.italic,
color: Colors.grey.shade400,
),
textAlign: TextAlign.center,
),
), ),
if (_loadedMods.isNotEmpty)
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(
itemCount: _loadedMods.length, itemCount: filteredAvailableMods.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final mod = _loadedMods[index]; final mod = filteredAvailableMods[index];
return Card( final isActive = mod.enabled;
margin: const EdgeInsets.symmetric(
horizontal: 16, return GestureDetector(
vertical: 4, onDoubleTap: () => _toggleModActive(mod),
),
child: ListTile( child: ListTile(
title: Text(mod.name), title: Text(
mod.name,
style: TextStyle(
color:
isActive ? Colors.green : Colors.white,
),
),
subtitle: Text( subtitle: Text(
'ID: ${mod.id}\nSize: ${mod.size} files', 'ID: ${mod.id}\nSize: ${mod.size} files',
style: Theme.of(context).textTheme.bodySmall, style: Theme.of(context).textTheme.bodySmall,
@@ -220,7 +390,7 @@ class _ModListPageState extends State<ModListPage> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (mod.isBaseGame) if (mod.isBaseGame)
Tooltip( const Tooltip(
message: 'Base Game', message: 'Base Game',
child: Icon( child: Icon(
Icons.home, Icons.home,
@@ -229,7 +399,7 @@ class _ModListPageState extends State<ModListPage> {
), ),
), ),
if (mod.isExpansion) if (mod.isExpansion)
Tooltip( const Tooltip(
message: 'Expansion', message: 'Expansion',
child: Icon( child: Icon(
Icons.star, Icons.star,
@@ -237,271 +407,186 @@ class _ModListPageState extends State<ModListPage> {
size: 16, size: 16,
), ),
), ),
Icon( const SizedBox(width: 4),
Icons.circle, if (mod.hardDependencies.isNotEmpty)
color: Tooltip(
mod.hardDependencies.isNotEmpty message:
? Colors.orange 'Hard dependencies:\n${mod.hardDependencies.join('\n')}',
: Colors.green, child: const Icon(
size: 12, Icons.link,
color: Colors.orange,
size: 16,
),
),
if (mod.loadAfter.isNotEmpty)
Tooltip(
message:
'Loads after:\n${mod.loadAfter.join('\n')}',
child: const Icon(
Icons.arrow_downward,
color: Colors.blue,
size: 16,
),
),
if (mod.loadBefore.isNotEmpty)
Tooltip(
message:
'Loads before:\n${mod.loadBefore.join('\n')}',
child: const Icon(
Icons.arrow_upward,
color: Colors.green,
size: 16,
),
), ),
], ],
), ),
onTap: () { onTap: () {
// TODO: Show mod details // Show mod details in future
}, },
), ),
); );
}, },
), ),
), ),
if (!_isLoading && _loadedMods.isNotEmpty)
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('${_loadedMods.length} mods loaded'),
ElevatedButton(
onPressed: _startLoadingMods,
child: const Text('Reload'),
),
], ],
), ),
), ),
], ),
);
}
void _startLoadingMods() { // RIGHT PANEL - Active mods (load order)
setState(() { Expanded(
_loadedMods.clear(); child: Card(
_isLoading = true; margin: const EdgeInsets.all(8.0),
_loadingStatus = 'Scanning for mods...';
});
// Use the simplified loading approach
modManager
.loadWithConfig(skipFileCount: _skipFileCount)
.then((_) {
setState(() {
_loadedMods = modManager.mods.values.toList();
_isLoading = false;
_loadingStatus = modManager.loadingStatus;
_totalModsFound = modManager.totalModsFound;
// Sort mods by name for better display
_loadedMods.sort((a, b) => a.name.compareTo(b.name));
});
})
.catchError((error) {
setState(() {
_isLoading = false;
_loadingStatus = 'Error loading mods: $error';
});
});
}
}
// Page to manage mod load order with dependency visualization
class LoadOrderPage extends StatefulWidget {
const LoadOrderPage({super.key});
@override
State<LoadOrderPage> createState() => _LoadOrderPageState();
}
class _LoadOrderPageState extends State<LoadOrderPage> {
bool _isLoading = false;
String _statusMessage = '';
List<Mod> _sortedMods = [];
bool _hasCycles = false;
List<String>? _cycleInfo;
List<List<String>> _incompatibleMods = [];
@override
void initState() {
super.initState();
// If we already have loaded mods, update the status message
if (modManager.modsLoaded && modManager.mods.isNotEmpty) {
_statusMessage = 'Ready to sort ${modManager.mods.length} loaded mods';
} else {
_statusMessage = 'No mods have been loaded yet. Please load mods first.';
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Text(
'Mod Load Order',
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 16),
Text(
'Automatically sort mods based on dependencies, prioritizing larger mods.',
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 24),
Row(
children: [
ElevatedButton(
onPressed: _isLoading ? null : _sortMods,
child: const Text('Auto-sort Mods'),
),
const SizedBox(width: 8),
Chip(
backgroundColor: Colors.amber.withOpacity(0.2),
label: const Text(
'Larger mods prioritized',
style: TextStyle(fontSize: 12),
),
),
const SizedBox(width: 16),
ElevatedButton(
onPressed:
_isLoading || _sortedMods.isEmpty ? null : _saveModOrder,
child: const Text('Save Load Order'),
),
],
),
const SizedBox(height: 16),
if (_isLoading)
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Center(
child: Column(
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(
_statusMessage,
textAlign: TextAlign.center,
style: TextStyle(
color:
_hasCycles || _incompatibleMods.isNotEmpty
? Colors.orange
: Colors.green,
),
),
],
),
),
),
if (!_isLoading && _statusMessage.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text(
_statusMessage,
style: TextStyle(
color:
_hasCycles || _incompatibleMods.isNotEmpty
? Colors.orange
: Colors.green,
),
),
),
if (_hasCycles && _cycleInfo != null)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
'Dependency cycle detected: ${_cycleInfo!.join(" -> ")}',
style: TextStyle(color: Colors.orange),
),
),
if (_incompatibleMods.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Incompatible mods detected:',
style: TextStyle(color: Colors.orange),
),
...List.generate(
_incompatibleMods.length > 5
? 5
: _incompatibleMods.length,
(index) {
final pair = _incompatibleMods[index];
return Text(
'- ${modManager.mods[pair[0]]?.name} and ${modManager.mods[pair[1]]?.name}',
style: TextStyle(color: Colors.orange),
);
},
),
if (_incompatibleMods.length > 5)
Text('...and ${_incompatibleMods.length - 5} more'),
],
),
),
const SizedBox(height: 16),
if (_sortedMods.isNotEmpty)
Container( Container(
padding: const EdgeInsets.all(8), color: Theme.of(context).primaryColor,
decoration: BoxDecoration( padding: const EdgeInsets.all(8.0),
color: Colors.grey.withOpacity(0.1), child: Text(
borderRadius: BorderRadius.circular(8), 'Active Mods (${filteredActiveMods.length}${_searchQuery.isNotEmpty ? '/' + _activeMods.length.toString() : ''})',
style: const TextStyle(fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
_searchQuery.isNotEmpty
? 'Searching: "$_searchQuery"'
: 'Larger mods are prioritized during auto-sorting.',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade400,
fontStyle: FontStyle.italic,
),
textAlign: TextAlign.center,
), ),
), ),
Expanded( Expanded(
child: child: ListView.builder(
_sortedMods.isEmpty itemCount: filteredActiveMods.length,
? Center(
child: Text(
modManager.modsLoaded
? 'Click "Auto-sort Mods" to generate a load order for ${modManager.mods.length} loaded mods.'
: 'Please go to the Mods tab first to load mods.',
textAlign: TextAlign.center,
),
)
: ListView.builder(
itemCount: _sortedMods.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final mod = _sortedMods[index]; final mod = filteredActiveMods[index];
return Card( // Find the actual position in the complete active mods list (for correct load order)
margin: const EdgeInsets.symmetric(vertical: 4), final actualLoadOrderPosition =
_activeMods.indexWhere((m) => m.id == mod.id) +
1;
return GestureDetector(
onDoubleTap: () {
// Don't allow deactivating base game or expansions
if (!mod.isBaseGame && !mod.isExpansion) {
_toggleModActive(mod);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Core game and expansions cannot be deactivated',
),
backgroundColor: Colors.orange,
),
);
}
},
child: Card(
margin: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 4.0,
),
child: ListTile( child: ListTile(
leading: Column( leading: SizedBox(
mainAxisAlignment: MainAxisAlignment.center, width: 50,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment:
MainAxisAlignment.center,
children: [ children: [
Text('${index + 1}'), SizedBox(
const SizedBox(height: 2), height: 24,
if (mod.size > 0) child: Container(
Tooltip( padding: const EdgeInsets.symmetric(
message: 'This mod contains ${mod.size} files.', horizontal: 4,
),
decoration: BoxDecoration(
color:
_searchQuery.isNotEmpty
? Colors.blue.withOpacity(
0.2,
)
: null,
borderRadius:
BorderRadius.circular(4),
),
child: Tooltip(
message:
'Load position: $actualLoadOrderPosition of ${_activeMods.length}${_searchQuery.isNotEmpty ? " (preserved when filtering)" : ""}',
child: Text(
'$actualLoadOrderPosition',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
color:
_searchQuery.isNotEmpty
? Colors.blue.shade300
: null,
),
),
),
),
),
SizedBox(
height: 24,
child: Center(
child:
mod.size > 0
? Tooltip(
message:
'This mod contains ${mod.size} files.',
child: Text( child: Text(
'${mod.size}', '${mod.size}',
style: TextStyle( style: const TextStyle(
fontSize: 10, fontSize: 10,
color: Colors.grey, color: Colors.grey,
), ),
), ),
)
: const SizedBox(),
),
), ),
], ],
), ),
),
title: Text(mod.name), title: Text(mod.name),
subtitle: Column( subtitle: Text(
crossAxisAlignment: CrossAxisAlignment.start, mod.id,
children: [ style: const TextStyle(fontSize: 12),
Text(mod.id, style: TextStyle(fontSize: 12)),
],
), ),
isThreeLine:
mod.hardDependencies.isNotEmpty ||
mod.loadAfter.isNotEmpty ||
mod.loadBefore.isNotEmpty,
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (mod.isBaseGame) if (mod.isBaseGame)
Tooltip( const Tooltip(
message: 'Base Game', message: 'Base Game',
child: Icon( child: Icon(
Icons.home, Icons.home,
@@ -510,7 +595,7 @@ class _LoadOrderPageState extends State<LoadOrderPage> {
), ),
), ),
if (mod.isExpansion) if (mod.isExpansion)
Tooltip( const Tooltip(
message: 'Expansion', message: 'Expansion',
child: Icon( child: Icon(
Icons.star, Icons.star,
@@ -521,8 +606,8 @@ class _LoadOrderPageState extends State<LoadOrderPage> {
if (mod.hardDependencies.isNotEmpty) if (mod.hardDependencies.isNotEmpty)
Tooltip( Tooltip(
message: message:
'Hard dependencies:\n${mod.hardDependencies.join('\n')}', 'Dependencies:\n${mod.hardDependencies.join('\n')}',
child: Icon( child: const Icon(
Icons.link, Icons.link,
color: Colors.orange, color: Colors.orange,
size: 24, size: 24,
@@ -533,7 +618,7 @@ class _LoadOrderPageState extends State<LoadOrderPage> {
Tooltip( Tooltip(
message: message:
'Loads after other mods:\n${mod.loadAfter.join('\n')}', 'Loads after other mods:\n${mod.loadAfter.join('\n')}',
child: Icon( child: const Icon(
Icons.arrow_downward, Icons.arrow_downward,
color: Colors.blue, color: Colors.blue,
size: 24, size: 24,
@@ -544,7 +629,7 @@ class _LoadOrderPageState extends State<LoadOrderPage> {
Tooltip( Tooltip(
message: message:
'Loads before other mods:\n${mod.loadBefore.join('\n')}', 'Loads before other mods:\n${mod.loadBefore.join('\n')}',
child: Icon( child: const Icon(
Icons.arrow_upward, Icons.arrow_upward,
color: Colors.green, color: Colors.green,
size: 24, size: 24,
@@ -552,6 +637,10 @@ class _LoadOrderPageState extends State<LoadOrderPage> {
), ),
], ],
), ),
onTap: () {
// Show mod details in future
},
),
), ),
); );
}, },
@@ -560,14 +649,99 @@ class _LoadOrderPageState extends State<LoadOrderPage> {
], ],
), ),
), ),
),
],
),
),
],
); );
} }
void _sortMods() async { void _startLoadingMods() {
if (modManager.mods.isEmpty) {
setState(() { setState(() {
_availableMods.clear();
_activeMods.clear();
_isLoading = true;
_statusMessage = 'Scanning for mods...';
_hasCycles = false;
_cycleInfo = null;
_incompatibleMods = [];
});
// Load mods
modManager
.loadWithConfig(skipFileCount: _skipFileCount)
.then((_) {
_loadModsFromGlobalState();
})
.catchError((error) {
setState(() {
_isLoading = false;
_statusMessage = 'Error loading mods: $error';
});
});
}
void _toggleModActive(Mod mod) {
// Cannot deactivate base game or expansions
if ((mod.isBaseGame || mod.isExpansion) && mod.enabled) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Core game and expansions cannot be deactivated'),
backgroundColor: Colors.orange,
),
);
return;
}
// Update the mod's enabled status in the global mod manager
final updatedMod = Mod(
name: mod.name,
id: mod.id,
path: mod.path,
versions: mod.versions,
description: mod.description,
hardDependencies: mod.hardDependencies,
loadAfter: mod.loadAfter,
loadBefore: mod.loadBefore,
incompatabilities: mod.incompatabilities,
enabled: !mod.enabled,
size: mod.size,
isBaseGame: mod.isBaseGame,
isExpansion: mod.isExpansion,
);
// Update in the global mod manager
modManager.mods[mod.id] = updatedMod;
// Update the UI
setState(() {
// Update in the available mods list
final index = _availableMods.indexWhere((m) => m.id == mod.id);
if (index >= 0) {
_availableMods[index] = updatedMod;
}
// Update the active mods list
if (updatedMod.enabled) {
// Add to active mods if not already there
if (!_activeMods.any((m) => m.id == updatedMod.id)) {
_activeMods.add(updatedMod);
}
} else {
// Remove from active mods
_activeMods.removeWhere((m) => m.id == updatedMod.id);
}
_statusMessage = _statusMessage =
'No mods have been loaded yet. Please go to the Mods tab and load mods first.'; 'Mod "${mod.name}" ${updatedMod.enabled ? 'activated' : 'deactivated'}';
});
}
void _sortActiveMods() async {
if (_activeMods.isEmpty) {
setState(() {
_statusMessage = 'No active mods to sort';
}); });
return; return;
} }
@@ -602,7 +776,7 @@ class _LoadOrderPageState extends State<LoadOrderPage> {
final sortedMods = modManager.getModsInLoadOrder(); final sortedMods = modManager.getModsInLoadOrder();
setState(() { setState(() {
_sortedMods = sortedMods; _activeMods = sortedMods;
_isLoading = false; _isLoading = false;
_statusMessage = 'Sorting complete! ${sortedMods.length} mods sorted.'; _statusMessage = 'Sorting complete! ${sortedMods.length} mods sorted.';
if (_hasCycles) { if (_hasCycles) {
@@ -622,7 +796,12 @@ class _LoadOrderPageState extends State<LoadOrderPage> {
} }
void _saveModOrder() async { void _saveModOrder() async {
if (_sortedMods.isEmpty) return; if (_activeMods.isEmpty) {
setState(() {
_statusMessage = 'No active mods to save';
});
return;
}
setState(() { setState(() {
_isLoading = true; _isLoading = true;
@@ -634,12 +813,11 @@ class _LoadOrderPageState extends State<LoadOrderPage> {
final configFile = ConfigFile(path: configPath); final configFile = ConfigFile(path: configPath);
// Load the current config // Load the current config
configFile.load(); await configFile.load();
// Replace the mods with our sorted list // Replace the mods with our active mods list
// We need to convert our Mods to the format expected by the config file
configFile.mods.clear(); configFile.mods.clear();
for (final mod in _sortedMods) { for (final mod in _activeMods) {
configFile.mods.add(mod); configFile.mods.add(mod);
} }
@@ -650,11 +828,27 @@ class _LoadOrderPageState extends State<LoadOrderPage> {
_isLoading = false; _isLoading = false;
_statusMessage = 'Mod load order saved successfully!'; _statusMessage = 'Mod load order saved successfully!';
}); });
// Show success message
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Mod order saved to config'),
backgroundColor: Colors.green,
),
);
} catch (e) { } catch (e) {
setState(() { setState(() {
_isLoading = false; _isLoading = false;
_statusMessage = 'Error saving mod load order: $e'; _statusMessage = 'Error saving mod load order: $e';
}); });
// Show error message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error saving mod order: $e'),
backgroundColor: Colors.red,
),
);
} }
} }
} }