import 'package:flutter/material.dart'; import 'package:rimworld_modman/mod_list.dart'; import 'package:rimworld_modman/mod_list_troubleshooter.dart'; import 'main.dart'; /// A widget that provides a user interface for the mod troubleshooter functionality. /// /// This allows users to: /// - Toggle between binary and linear search modes /// - Navigate forward and backward through mod sets /// - Adjust step size for linear navigation /// - Mark mods as checked/good or problematic /// - Find specific mods causing issues in their load order class ModTroubleshooterWidget extends StatefulWidget { const ModTroubleshooterWidget({super.key}); @override State createState() => _ModTroubleshooterWidgetState(); } class _ModTroubleshooterWidgetState extends State { late ModListTroubleshooter _troubleshooter; bool _isInitialized = false; bool _isBinaryMode = false; int _stepSize = 10; // Set of mod IDs that have been checked and confirmed to be good final Set _checkedMods = {}; // Set of mod IDs that are suspected to cause issues final Set _problemMods = {}; // The currently selected mod IDs (for highlighting) List _selectedMods = []; // The next potential set of mods (from move calculation) Move? _nextForwardMove; Move? _nextBackwardMove; // Controller for step size input late TextEditingController _stepSizeController; @override void initState() { super.initState(); _stepSizeController = TextEditingController(text: _stepSize.toString()); } @override void dispose() { _stepSizeController.dispose(); super.dispose(); } void _initialize() { if (_isInitialized) return; // Initialize the troubleshooter with the global mod manager _troubleshooter = ModListTroubleshooter(modManager); // Set initial active mods for highlighting if (modManager.activeMods.isNotEmpty) { // Initially select all active mods _selectedMods = List.from(modManager.activeMods.keys); } // Calculate initial moves _updateNextMoves(); setState(() { _isInitialized = true; }); } void _updateNextMoves() { if (_isBinaryMode) { _nextForwardMove = _troubleshooter.binaryForwardMove(); _nextBackwardMove = _troubleshooter.binaryBackwardMove(); } else { _nextForwardMove = _troubleshooter.linearForwardMove(stepSize: _stepSize); _nextBackwardMove = _troubleshooter.linearBackwardMove( stepSize: _stepSize, ); } } void _navigateForward() { ModList result; if (_isBinaryMode) { result = _troubleshooter.binaryForward(); } else { result = _troubleshooter.linearForward(stepSize: _stepSize); } // Load all required dependencies for the selected mods final loadOrder = result.loadRequiredBaseGame(); // Use the mods from the load order result setState(() { _selectedMods = loadOrder.loadOrder; _updateNextMoves(); }); } void _navigateBackward() { ModList result; if (_isBinaryMode) { result = _troubleshooter.binaryBackward(); } else { result = _troubleshooter.linearBackward(stepSize: _stepSize); } // Load all required dependencies for the selected mods final loadOrder = result.loadRequiredBaseGame(); // Use the mods from the load order result setState(() { _selectedMods = loadOrder.loadOrder; _updateNextMoves(); }); } void _markAsGood(String modId) { setState(() { _checkedMods.add(modId); _problemMods.remove(modId); }); } void _markAsProblem(String modId) { setState(() { _problemMods.add(modId); _checkedMods.remove(modId); }); } void _clearMarks(String modId) { setState(() { _checkedMods.remove(modId); _problemMods.remove(modId); }); } void _resetTroubleshooter() { setState(() { _checkedMods.clear(); _problemMods.clear(); _isInitialized = false; }); _initialize(); } void _saveTroubleshootingConfig() { // Only save if we have a valid selection if (_selectedMods.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('No mods selected to save'), duration: Duration(seconds: 2), ), ); return; } // First disable all mods modManager.disableAll(); // Then enable only the selected mods modManager.enableMods(_selectedMods); // Save the configuration (we don't have direct access to save method, so show a message) ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( '${_selectedMods.length} mods prepared for testing. Please use Save button in the Mods tab to save config.', ), backgroundColor: Colors.orange, duration: const Duration(seconds: 4), ), ); } void _markSelectedAsGood() { if (_selectedMods.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('No mods selected to mark'), duration: Duration(seconds: 2), ), ); return; } setState(() { for (final modId in _selectedMods) { _checkedMods.add(modId); _problemMods.remove(modId); } }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Marked ${_selectedMods.length} mods as good'), backgroundColor: Colors.green, duration: const Duration(seconds: 2), ), ); } void _markSelectedAsProblem() { if (_selectedMods.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('No mods selected to mark'), duration: Duration(seconds: 2), ), ); return; } setState(() { for (final modId in _selectedMods) { _problemMods.add(modId); _checkedMods.remove(modId); } }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Marked ${_selectedMods.length} mods as problematic'), backgroundColor: Colors.orange, duration: const Duration(seconds: 2), ), ); } @override Widget build(BuildContext context) { // Make sure we're initialized if (!_isInitialized) { _initialize(); } if (!_isInitialized || modManager.mods.isEmpty) { return _buildEmptyState(); } return Column( children: [_buildControlPanel(), Expanded(child: _buildModList())], ); } Widget _buildEmptyState() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.build, size: AppThemeExtension.of(context).iconSizeLarge * 2, ), const SizedBox(height: 16), Text( 'Troubleshooting', style: Theme.of(context).textTheme.headlineMedium, ), const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 32.0), child: Text( 'Load mods first to use the troubleshooting tools.', style: Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.center, ), ), const SizedBox(height: 24), ElevatedButton( onPressed: () { // No direct access to the ModManagerHomePage state, so just show a message ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Please go to the Mods tab to load mods first'), duration: Duration(seconds: 3), ), ); }, child: const Text('Go to Mod Manager'), ), ], ), ); } Widget _buildControlPanel() { return Card( margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), child: Padding( padding: AppThemeExtension.of(context).paddingSmall, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ // Compact instruction Expanded( child: Text( _selectedMods.isNotEmpty ? 'Testing ${_selectedMods.length} mods. Tap highlighted mods to navigate. Mark results below:' : 'Click highlighted mods to begin testing. Blue→forward, purple←backward.', style: TextStyle( fontSize: AppThemeExtension.of(context).textSizeRegular, fontStyle: FontStyle.italic, ), ), ), ], ), const SizedBox(height: 8), Row( children: [ // Binary/Linear mode toggle Text('Mode:', style: Theme.of(context).textTheme.bodyMedium), const SizedBox(width: 8), ToggleButtons( isSelected: [!_isBinaryMode, _isBinaryMode], onPressed: (index) { setState(() { _isBinaryMode = index == 1; _updateNextMoves(); }); }, children: const [ Padding( padding: EdgeInsets.symmetric(horizontal: 8.0), child: Text('Linear'), ), Padding( padding: EdgeInsets.symmetric(horizontal: 8.0), child: Text('Binary'), ), ], ), // Step size input field (only for linear mode) if (!_isBinaryMode) ...[ const SizedBox(width: 16), Text('Step:', style: Theme.of(context).textTheme.bodyMedium), const SizedBox(width: 4), SizedBox( width: 60, child: TextField( keyboardType: TextInputType.number, decoration: const InputDecoration( border: OutlineInputBorder(), contentPadding: EdgeInsets.symmetric( horizontal: 6, vertical: 6, ), ), controller: _stepSizeController, onChanged: (value) { final parsedValue = int.tryParse(value); if (parsedValue != null && parsedValue > 0) { setState(() { _stepSize = parsedValue; _updateNextMoves(); }); } }, ), ), ], const Spacer(), // Buttons to mark selected mods if (_selectedMods.isNotEmpty) ...[ OutlinedButton.icon( icon: Icon( Icons.error, color: Colors.red.shade300, size: 16, ), label: const Text('Problem'), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 0, ), ), onPressed: _markSelectedAsProblem, ), const SizedBox(width: 4), OutlinedButton.icon( icon: Icon( Icons.check_circle, color: Colors.green.shade300, size: 16, ), label: const Text('Good'), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 0, ), ), onPressed: _markSelectedAsGood, ), const SizedBox(width: 4), ], // Reset button OutlinedButton.icon( icon: const Icon(Icons.refresh, size: 16), label: const Text('Reset'), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 0, ), ), onPressed: _resetTroubleshooter, ), if (_selectedMods.isNotEmpty) ...[ const SizedBox(width: 4), // Save config button OutlinedButton.icon( icon: const Icon(Icons.save, size: 16), label: const Text('Save'), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 0, ), ), onPressed: _saveTroubleshootingConfig, ), ], ], ), ], ), ), ); } Widget _buildModList() { // Get the original mod order from mod manager final fullModList = modManager.activeMods.keys.toList(); return Card( margin: AppThemeExtension.of(context).paddingRegular, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Container( color: Theme.of(context).primaryColor, padding: AppThemeExtension.of(context).paddingRegular, child: Column( children: [ Text( 'Active Mods (${fullModList.length})', style: const TextStyle(fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), if (_nextForwardMove != null || _nextBackwardMove != null) Text( 'Click ↓blue areas to move forward, ↑purple to move backward', style: TextStyle( fontSize: AppThemeExtension.of(context).textSizeSmall, fontStyle: FontStyle.italic, color: Colors.grey.shade300, ), textAlign: TextAlign.center, ), ], ), ), Expanded( child: ListView.builder( itemCount: fullModList.length, itemBuilder: (context, index) { final modId = fullModList[index]; final mod = modManager.mods[modId]; if (mod == null) return const SizedBox.shrink(); // Determine if this mod is in the selection range for highlighted navigation final bool isSelected = _selectedMods.contains(modId); // Check if this mod would be included in the next Forward/Backward move bool isInNextForward = false; bool isInNextBackward = false; if (_nextForwardMove != null && index >= _nextForwardMove!.startIndex && index < _nextForwardMove!.endIndex) { isInNextForward = true; } if (_nextBackwardMove != null && index >= _nextBackwardMove!.startIndex && index < _nextBackwardMove!.endIndex) { isInNextBackward = true; } // Determine mod status for coloring final bool isChecked = _checkedMods.contains(modId); final bool isProblem = _problemMods.contains(modId); return GestureDetector( onTap: () { // Navigation takes precedence if this mod is in a navigation range if (isInNextForward) { _navigateForward(); } else if (isInNextBackward) { _navigateBackward(); } // Otherwise toggle the status of this mod else if (isChecked) { _markAsProblem(modId); } else if (isProblem) { _clearMarks(modId); } else { _markAsGood(modId); } }, child: Card( margin: const EdgeInsets.symmetric( horizontal: 8.0, vertical: 4.0, ), color: _getModCardColor( isSelected: isSelected, isChecked: isChecked, isProblem: isProblem, isInNextForward: isInNextForward, isInNextBackward: isInNextBackward, ), child: ListTile( leading: Text( '${index + 1}', style: TextStyle( fontWeight: FontWeight.bold, color: isSelected ? Colors.white : Colors.grey, ), ), title: Row( children: [ if (isSelected) Container( padding: const EdgeInsets.symmetric( horizontal: 4, vertical: 0, ), margin: const EdgeInsets.only(right: 4), decoration: BoxDecoration( color: const Color( 0x28303F9F, ), // Blue with alpha 40 borderRadius: BorderRadius.circular(4), ), child: Text( 'TESTING', style: TextStyle( color: Colors.blue.shade200, fontSize: AppThemeExtension.of( context, ).textSizeSmall, fontWeight: FontWeight.bold, ), ), ), if (isChecked) Container( padding: const EdgeInsets.symmetric( horizontal: 4, vertical: 0, ), margin: const EdgeInsets.only(right: 4), decoration: BoxDecoration( color: const Color( 0x1E2E7D32, ), // Green with alpha 30 borderRadius: BorderRadius.circular(4), ), child: Text( 'GOOD', style: TextStyle( color: Colors.green.shade200, fontSize: AppThemeExtension.of( context, ).textSizeSmall, fontWeight: FontWeight.bold, ), ), ), if (isProblem) Container( padding: const EdgeInsets.symmetric( horizontal: 4, vertical: 0, ), margin: const EdgeInsets.only(right: 4), decoration: BoxDecoration( color: const Color( 0x1EC62828, ), // Red with alpha 30 borderRadius: BorderRadius.circular(4), ), child: Text( 'PROBLEM', style: TextStyle( color: Colors.red.shade200, fontSize: AppThemeExtension.of( context, ).textSizeSmall, fontWeight: FontWeight.bold, ), ), ), Expanded( child: Text( mod.name, style: TextStyle( fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, ), ), ), ], ), subtitle: Text( modId, style: TextStyle( fontSize: AppThemeExtension.of(context).textSizeSmall, ), ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ // Display mod characteristics if (mod.isBaseGame) Tooltip( message: 'Base Game', child: Icon( Icons.home, color: AppThemeExtension.of(context).baseGameColor, size: AppThemeExtension.of(context).iconSizeSmall, ), ), if (mod.isExpansion) Tooltip( message: 'Expansion', child: Icon( Icons.star, color: AppThemeExtension.of( context, ).expansionColor, size: AppThemeExtension.of(context).iconSizeSmall, ), ), if (mod.dependencies.isNotEmpty) Tooltip( message: 'Dependencies:\n${mod.dependencies.join('\n')}', child: Icon( Icons.link, color: AppThemeExtension.of(context).linkColor, size: AppThemeExtension.of(context).iconSizeSmall, ), ), // Display status icon if (isChecked) Tooltip( message: 'Marked as working correctly', child: Icon( Icons.check_circle, color: Colors.green.shade300, ), ) else if (isProblem) Tooltip( message: 'Marked as problematic', child: Icon( Icons.error, color: Colors.red.shade300, ), ), const SizedBox(width: 4), // Show navigation indicators if (isInNextForward) Container( padding: const EdgeInsets.symmetric( horizontal: 4, vertical: 2, ), decoration: BoxDecoration( color: const Color( 0x0A2196F3, ), // Blue with alpha 10 borderRadius: BorderRadius.circular(4), ), child: Tooltip( message: 'Click to move Forward (test this mod)', child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.arrow_forward, color: Colors.blue.shade300, size: AppThemeExtension.of( context, ).iconSizeSmall, ), const SizedBox(width: 2), Text( 'Forward', style: TextStyle( color: Colors.blue.shade300, fontSize: AppThemeExtension.of( context, ).textSizeSmall, ), ), ], ), ), ), if (isInNextBackward) Container( padding: const EdgeInsets.symmetric( horizontal: 4, vertical: 2, ), decoration: BoxDecoration( color: const Color( 0x0A9C27B0, ), // Purple with alpha 10 borderRadius: BorderRadius.circular(4), ), child: Tooltip( message: 'Click to move Backward (test this mod)', child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.arrow_back, color: Colors.purple.shade300, size: AppThemeExtension.of( context, ).iconSizeSmall, ), const SizedBox(width: 2), Text( 'Back', style: TextStyle( color: Colors.purple.shade300, fontSize: AppThemeExtension.of( context, ).textSizeSmall, ), ), ], ), ), ), ], ), ), ), ); }, ), ), ], ), ); } Color _getModCardColor({ required bool isSelected, required bool isChecked, required bool isProblem, required bool isInNextForward, required bool isInNextBackward, }) { // Priority: 1. Selected, 2. Navigation areas, 3. Status if (isSelected) { return const Color(0x80303F9F); } else if (isInNextForward && isInNextBackward) { return const Color(0x50673AB7); } else if (isInNextForward) { return const Color(0x402196F3); } else if (isInNextBackward) { return const Color(0x409C27B0); } else if (isChecked) { return const Color(0x802E7D32); } else if (isProblem) { return const Color(0x80C62828); } return Colors.transparent; } }