diff --git a/lib/main.dart b/lib/main.dart index fd68dfc..c0e2ba8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,11 +7,11 @@ late ModList modManager; void main() { WidgetsFlutterBinding.ensureInitialized(); - + // Get a reference to the logger (now auto-initializes) final logger = Logger.instance; logger.info('Rimworld Mod Manager starting...'); - + // Initialize the mod manager modManager = ModList(path: modsRoot); @@ -55,8 +55,7 @@ class _ModManagerHomePageState extends State { int _selectedIndex = 0; final List _pages = [ - const ModListPage(), - const LoadOrderPage(), + const ModManagerPage(), const TroubleshootingPage(), ]; @@ -74,10 +73,6 @@ class _ModManagerHomePageState extends State { }, items: const [ BottomNavigationBarItem(icon: Icon(Icons.extension), label: 'Mods'), - BottomNavigationBarItem( - icon: Icon(Icons.reorder), - label: 'Load Order', - ), BottomNavigationBarItem( icon: Icon(Icons.build), label: 'Troubleshoot', @@ -88,21 +83,31 @@ class _ModManagerHomePageState extends State { } } -// Page to display all installed mods with enable/disable toggles -class ModListPage extends StatefulWidget { - const ModListPage({super.key}); +// Combined page for mod management with two-panel layout +class ModManagerPage extends StatefulWidget { + const ModManagerPage({super.key}); @override - State createState() => _ModListPageState(); + State createState() => _ModManagerPageState(); } -class _ModListPageState extends State { - List _loadedMods = []; +class _ModManagerPageState extends State { + // For all available mods (left panel) + List _availableMods = []; + + // For active mods (right panel) + List _activeMods = []; + bool _isLoading = false; - String _loadingStatus = ''; + String _statusMessage = ''; int _totalModsFound = 0; - bool _skipFileCount = - false; // Skip file counting by default for faster loading + bool _skipFileCount = false; + bool _hasCycles = false; + List? _cycleInfo; + List> _incompatibleMods = []; + + final TextEditingController _searchController = TextEditingController(); + String _searchQuery = ''; @override void initState() { @@ -111,14 +116,30 @@ class _ModListPageState extends State { if (modManager.modsLoaded) { _loadModsFromGlobalState(); } + + _searchController.addListener(() { + setState(() { + _searchQuery = _searchController.text.toLowerCase(); + }); + }); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); } void _loadModsFromGlobalState() { setState(() { - _loadedMods = modManager.mods.values.toList(); - _loadedMods.sort((a, b) => a.name.compareTo(b.name)); + // Get all mods for the left panel (sorted alphabetically) + _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; - _loadingStatus = modManager.loadingStatus; + _statusMessage = modManager.loadingStatus; _totalModsFound = modManager.totalModsFound; }); } @@ -127,9 +148,28 @@ class _ModListPageState extends State { Widget build(BuildContext context) { return Scaffold( body: - _loadedMods.isEmpty && !_isLoading + _isLoading && _availableMods.isEmpty + ? _buildLoadingView() + : _availableMods.isEmpty ? _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 { children: [ const Icon(Icons.extension, size: 64), 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), Text( 'Ready to scan for Rimworld mods.', @@ -171,403 +214,534 @@ class _ModListPageState extends State { ); } - 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( children: [ - if (_isLoading) - Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - const CircularProgressIndicator(), - const SizedBox(height: 16), - Text( - _loadingStatus, - style: Theme.of(context).textTheme.bodyMedium, - textAlign: TextAlign.center, + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + // Search field + Expanded( + child: TextField( + controller: _searchController, + decoration: InputDecoration( + 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( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - 'No mods found. Try reloading.', - textAlign: TextAlign.center, + ), + + // Status message + if (!_isLoading && _statusMessage.isNotEmpty) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + child: Text( + _statusMessage, + style: TextStyle( + color: + _hasCycles || _incompatibleMods.isNotEmpty + ? Colors.orange + : Colors.green, ), ), ), - if (_loadedMods.isNotEmpty) - Expanded( - child: ListView.builder( - itemCount: _loadedMods.length, - itemBuilder: (context, index) { - final mod = _loadedMods[index]; - return Card( - margin: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 4, - ), - child: ListTile( - title: Text(mod.name), - subtitle: Text( - 'ID: ${mod.id}\nSize: ${mod.size} files', - style: Theme.of(context).textTheme.bodySmall, - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (mod.isBaseGame) - Tooltip( - message: 'Base Game', - child: Icon( - Icons.home, - color: Colors.blue, - size: 16, - ), - ), - if (mod.isExpansion) - Tooltip( - message: 'Expansion', - child: Icon( - Icons.star, - color: Colors.yellow, - size: 16, - ), - ), - Icon( - Icons.circle, - color: - mod.hardDependencies.isNotEmpty - ? Colors.orange - : Colors.green, - size: 12, - ), - ], - ), - onTap: () { - // TODO: Show mod details - }, - ), - ); - }, - ), - ), - if (!_isLoading && _loadedMods.isNotEmpty) + + // Cycle warnings + if (_hasCycles && _cycleInfo != null) Padding( - padding: const EdgeInsets.all(16.0), + 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( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('${_loadedMods.length} mods loaded'), - ElevatedButton( - onPressed: _startLoadingMods, - child: const Text('Reload'), + 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), ), ], ), ), - ], - ); - } - void _startLoadingMods() { - setState(() { - _loadedMods.clear(); - _isLoading = true; - _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 createState() => _LoadOrderPageState(); -} - -class _LoadOrderPageState extends State { - bool _isLoading = false; - String _statusMessage = ''; - List _sortedMods = []; - bool _hasCycles = false; - List? _cycleInfo; - List> _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( - crossAxisAlignment: CrossAxisAlignment.start, - 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( + // 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: [ - const CircularProgressIndicator(), - const SizedBox(height: 16), - Text( - _statusMessage, - textAlign: TextAlign.center, - style: TextStyle( - color: - _hasCycles || _incompatibleMods.isNotEmpty - ? Colors.orange - : Colors.green, + 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, + ), + ), + 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, + ), + ), + Expanded( + child: ListView.builder( + itemCount: filteredAvailableMods.length, + itemBuilder: (context, index) { + final mod = filteredAvailableMods[index]; + final isActive = mod.enabled; + + return GestureDetector( + onDoubleTap: () => _toggleModActive(mod), + child: ListTile( + title: Text( + mod.name, + style: TextStyle( + color: + isActive ? Colors.green : Colors.white, + ), + ), + subtitle: Text( + 'ID: ${mod.id}\nSize: ${mod.size} files', + style: Theme.of(context).textTheme.bodySmall, + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (mod.isBaseGame) + const Tooltip( + message: 'Base Game', + child: Icon( + Icons.home, + color: Colors.blue, + size: 16, + ), + ), + if (mod.isExpansion) + const Tooltip( + message: 'Expansion', + child: Icon( + Icons.star, + color: Colors.yellow, + size: 16, + ), + ), + const SizedBox(width: 4), + if (mod.hardDependencies.isNotEmpty) + Tooltip( + message: + 'Hard dependencies:\n${mod.hardDependencies.join('\n')}', + child: const Icon( + 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: () { + // Show mod details in future + }, + ), + ); + }, ), ), ], ), ), ), - 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, + + // RIGHT PANEL - Active mods (load order) + 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( + '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( + child: ListView.builder( + itemCount: filteredActiveMods.length, + itemBuilder: (context, index) { + final mod = filteredActiveMods[index]; + // Find the actual position in the complete active mods list (for correct load order) + 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( + leading: SizedBox( + width: 50, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + SizedBox( + height: 24, + child: Container( + padding: const EdgeInsets.symmetric( + 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( + '${mod.size}', + style: const TextStyle( + fontSize: 10, + color: Colors.grey, + ), + ), + ) + : const SizedBox(), + ), + ), + ], + ), + ), + title: Text(mod.name), + subtitle: Text( + mod.id, + style: const TextStyle(fontSize: 12), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (mod.isBaseGame) + const Tooltip( + message: 'Base Game', + child: Icon( + Icons.home, + color: Colors.blue, + size: 24, + ), + ), + if (mod.isExpansion) + const Tooltip( + message: 'Expansion', + child: Icon( + Icons.star, + color: Colors.yellow, + size: 24, + ), + ), + if (mod.hardDependencies.isNotEmpty) + Tooltip( + message: + 'Dependencies:\n${mod.hardDependencies.join('\n')}', + child: const Icon( + Icons.link, + color: Colors.orange, + size: 24, + ), + ), + const SizedBox(width: 4), + if (mod.loadAfter.isNotEmpty) + Tooltip( + message: + 'Loads after other mods:\n${mod.loadAfter.join('\n')}', + child: const Icon( + Icons.arrow_downward, + color: Colors.blue, + size: 24, + ), + ), + const SizedBox(width: 4), + if (mod.loadBefore.isNotEmpty) + Tooltip( + message: + 'Loads before other mods:\n${mod.loadBefore.join('\n')}', + child: const Icon( + Icons.arrow_upward, + color: Colors.green, + size: 24, + ), + ), + ], + ), + onTap: () { + // Show mod details in future + }, + ), + ), + ); + }, + ), + ), + ], ), ), ), - 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( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.grey.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - ), - Expanded( - child: - _sortedMods.isEmpty - ? 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) { - final mod = _sortedMods[index]; - return Card( - margin: const EdgeInsets.symmetric(vertical: 4), - child: ListTile( - leading: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('${index + 1}'), - const SizedBox(height: 2), - if (mod.size > 0) - Tooltip( - message: 'This mod contains ${mod.size} files.', - child: Text( - '${mod.size}', - style: TextStyle( - fontSize: 10, - color: Colors.grey, - ), - ), - ), - ], - ), - title: Text(mod.name), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(mod.id, style: TextStyle(fontSize: 12)), - ], - ), - isThreeLine: - mod.hardDependencies.isNotEmpty || - mod.loadAfter.isNotEmpty || - mod.loadBefore.isNotEmpty, - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (mod.isBaseGame) - Tooltip( - message: 'Base Game', - child: Icon( - Icons.home, - color: Colors.blue, - size: 24, - ), - ), - if (mod.isExpansion) - Tooltip( - message: 'Expansion', - child: Icon( - Icons.star, - color: Colors.yellow, - size: 24, - ), - ), - if (mod.hardDependencies.isNotEmpty) - Tooltip( - message: - 'Hard dependencies:\n${mod.hardDependencies.join('\n')}', - child: Icon( - Icons.link, - color: Colors.orange, - size: 24, - ), - ), - const SizedBox(width: 4), - if (mod.loadAfter.isNotEmpty) - Tooltip( - message: - 'Loads after other mods:\n${mod.loadAfter.join('\n')}', - child: Icon( - Icons.arrow_downward, - color: Colors.blue, - size: 24, - ), - ), - const SizedBox(width: 4), - if (mod.loadBefore.isNotEmpty) - Tooltip( - message: - 'Loads before other mods:\n${mod.loadBefore.join('\n')}', - child: Icon( - Icons.arrow_upward, - color: Colors.green, - size: 24, - ), - ), - ], - ), - ), - ); - }, - ), - ), - ], + ], + ), ), - ), + ], ); } - void _sortMods() async { - if (modManager.mods.isEmpty) { + void _startLoadingMods() { + 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 = + 'Mod "${mod.name}" ${updatedMod.enabled ? 'activated' : 'deactivated'}'; + }); + } + + void _sortActiveMods() async { + if (_activeMods.isEmpty) { setState(() { - _statusMessage = - 'No mods have been loaded yet. Please go to the Mods tab and load mods first.'; + _statusMessage = 'No active mods to sort'; }); return; } @@ -602,7 +776,7 @@ class _LoadOrderPageState extends State { final sortedMods = modManager.getModsInLoadOrder(); setState(() { - _sortedMods = sortedMods; + _activeMods = sortedMods; _isLoading = false; _statusMessage = 'Sorting complete! ${sortedMods.length} mods sorted.'; if (_hasCycles) { @@ -622,7 +796,12 @@ class _LoadOrderPageState extends State { } void _saveModOrder() async { - if (_sortedMods.isEmpty) return; + if (_activeMods.isEmpty) { + setState(() { + _statusMessage = 'No active mods to save'; + }); + return; + } setState(() { _isLoading = true; @@ -634,12 +813,11 @@ class _LoadOrderPageState extends State { final configFile = ConfigFile(path: configPath); // Load the current config - configFile.load(); + await configFile.load(); - // Replace the mods with our sorted list - // We need to convert our Mods to the format expected by the config file + // Replace the mods with our active mods list configFile.mods.clear(); - for (final mod in _sortedMods) { + for (final mod in _activeMods) { configFile.mods.add(mod); } @@ -650,11 +828,27 @@ class _LoadOrderPageState extends State { _isLoading = false; _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) { setState(() { _isLoading = false; _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, + ), + ); } } }