import 'dart:io'; import 'package:flutter/material.dart'; import 'package:rimworld_modman/logger.dart'; import 'package:rimworld_modman/mod.dart'; import 'package:rimworld_modman/mod_list.dart'; import 'package:rimworld_modman/mod_troubleshooter_widget.dart'; // Theme extension to store app-specific constants class AppThemeExtension extends ThemeExtension { final double iconSizeSmall; final double iconSizeRegular; final double iconSizeLarge; final double textSizeSmall; final double textSizeRegular; final double textSizeLarge; final EdgeInsets paddingSmall; final EdgeInsets paddingRegular; final EdgeInsets paddingLarge; final Color enabledModColor; final Color enabledModBackgroundColor; final Color errorBackgroundColor; final Color warningColor; final Color errorColor; final Color baseGameColor; final Color expansionColor; final Color linkColor; final Color loadAfterColor; final Color loadBeforeColor; AppThemeExtension({ required this.iconSizeSmall, required this.iconSizeRegular, required this.iconSizeLarge, required this.textSizeSmall, required this.textSizeRegular, required this.textSizeLarge, required this.paddingSmall, required this.paddingRegular, required this.paddingLarge, required this.enabledModColor, required this.enabledModBackgroundColor, required this.errorBackgroundColor, required this.warningColor, required this.errorColor, required this.baseGameColor, required this.expansionColor, required this.linkColor, required this.loadAfterColor, required this.loadBeforeColor, }); static AppThemeExtension of(BuildContext context) { return Theme.of(context).extension()!; } @override ThemeExtension copyWith({ double? iconSizeSmall, double? iconSizeRegular, double? iconSizeLarge, double? textSizeSmall, double? textSizeRegular, double? textSizeLarge, EdgeInsets? paddingSmall, EdgeInsets? paddingRegular, EdgeInsets? paddingLarge, Color? enabledModColor, Color? enabledModBackgroundColor, Color? errorBackgroundColor, Color? warningColor, Color? errorColor, Color? baseGameColor, Color? expansionColor, Color? linkColor, Color? loadAfterColor, Color? loadBeforeColor, }) { return AppThemeExtension( iconSizeSmall: iconSizeSmall ?? this.iconSizeSmall, iconSizeRegular: iconSizeRegular ?? this.iconSizeRegular, iconSizeLarge: iconSizeLarge ?? this.iconSizeLarge, textSizeSmall: textSizeSmall ?? this.textSizeSmall, textSizeRegular: textSizeRegular ?? this.textSizeRegular, textSizeLarge: textSizeLarge ?? this.textSizeLarge, paddingSmall: paddingSmall ?? this.paddingSmall, paddingRegular: paddingRegular ?? this.paddingRegular, paddingLarge: paddingLarge ?? this.paddingLarge, enabledModColor: enabledModColor ?? this.enabledModColor, enabledModBackgroundColor: enabledModBackgroundColor ?? this.enabledModBackgroundColor, errorBackgroundColor: errorBackgroundColor ?? this.errorBackgroundColor, warningColor: warningColor ?? this.warningColor, errorColor: errorColor ?? this.errorColor, baseGameColor: baseGameColor ?? this.baseGameColor, expansionColor: expansionColor ?? this.expansionColor, linkColor: linkColor ?? this.linkColor, loadAfterColor: loadAfterColor ?? this.loadAfterColor, loadBeforeColor: loadBeforeColor ?? this.loadBeforeColor, ); } @override ThemeExtension lerp( covariant ThemeExtension? other, double t, ) { if (other is! AppThemeExtension) { return this; } return AppThemeExtension( iconSizeSmall: lerpDouble(iconSizeSmall, other.iconSizeSmall, t), iconSizeRegular: lerpDouble(iconSizeRegular, other.iconSizeRegular, t), iconSizeLarge: lerpDouble(iconSizeLarge, other.iconSizeLarge, t), textSizeSmall: lerpDouble(textSizeSmall, other.textSizeSmall, t), textSizeRegular: lerpDouble(textSizeRegular, other.textSizeRegular, t), textSizeLarge: lerpDouble(textSizeLarge, other.textSizeLarge, t), paddingSmall: EdgeInsets.lerp(paddingSmall, other.paddingSmall, t)!, paddingRegular: EdgeInsets.lerp(paddingRegular, other.paddingRegular, t)!, paddingLarge: EdgeInsets.lerp(paddingLarge, other.paddingLarge, t)!, enabledModColor: Color.lerp(enabledModColor, other.enabledModColor, t)!, enabledModBackgroundColor: Color.lerp( enabledModBackgroundColor, other.enabledModBackgroundColor, t, )!, errorBackgroundColor: Color.lerp(errorBackgroundColor, other.errorBackgroundColor, t)!, warningColor: Color.lerp(warningColor, other.warningColor, t)!, errorColor: Color.lerp(errorColor, other.errorColor, t)!, baseGameColor: Color.lerp(baseGameColor, other.baseGameColor, t)!, expansionColor: Color.lerp(expansionColor, other.expansionColor, t)!, linkColor: Color.lerp(linkColor, other.linkColor, t)!, loadAfterColor: Color.lerp(loadAfterColor, other.loadAfterColor, t)!, loadBeforeColor: Color.lerp(loadBeforeColor, other.loadBeforeColor, t)!, ); } static AppThemeExtension light() { return AppThemeExtension( iconSizeSmall: 16, iconSizeRegular: 24, iconSizeLarge: 32, textSizeSmall: 10, textSizeRegular: 14, textSizeLarge: 18, paddingSmall: const EdgeInsets.all(4.0), paddingRegular: const EdgeInsets.all(8.0), paddingLarge: const EdgeInsets.all(16.0), enabledModColor: Colors.green, enabledModBackgroundColor: const Color.fromRGBO(0, 128, 0, 0.1), errorBackgroundColor: Color.fromRGBO( Colors.red.shade900.red, Colors.red.shade900.green, Colors.red.shade900.blue, 0.3, ), warningColor: Colors.orange, errorColor: Colors.red, baseGameColor: Colors.blue, expansionColor: Colors.yellow, linkColor: Colors.orange, loadAfterColor: Colors.blue, loadBeforeColor: Colors.green, ); } static AppThemeExtension dark() { return light(); // For now, we use the same values for both light and dark } } double lerpDouble(double a, double b, double t) { return a + (b - a) * t; } // Constants for file paths final String root = Platform.isWindows ? r'C:/Users/Administrator/Seafile/Games-RimWorld' : '~/Library/Application Support/RimWorld'; final String modsRoot = Platform.isWindows ? '$root/294100' : '$root/Mods'; final String configRoot = Platform.isWindows ? '$root/AppData/RimWorld by Ludeon Studios/Config' : '$root/Config'; final String configPath = '$configRoot/ModsConfig.xml'; final String logsPath = '$root/ModManager'; 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(modsPath: modsRoot, configPath: configPath); // Start the app runApp(const RimWorldModManager()); } class RimWorldModManager extends StatelessWidget { const RimWorldModManager({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'RimWorld Mod Manager', theme: ThemeData.dark().copyWith( primaryColor: const Color(0xFF3D4A59), colorScheme: ColorScheme.fromSeed( seedColor: const Color(0xFF3D4A59), brightness: Brightness.dark, ), scaffoldBackgroundColor: const Color(0xFF1E262F), cardColor: const Color(0xFF2A3440), appBarTheme: const AppBarTheme( backgroundColor: Color(0xFF2A3440), foregroundColor: Colors.white, ), extensions: [AppThemeExtension.dark()], ), home: const ModManagerHomePage(), ); } } class ModManagerHomePage extends StatefulWidget { const ModManagerHomePage({super.key}); @override State createState() => _ModManagerHomePageState(); } class _ModManagerHomePageState extends State { int _selectedIndex = 0; final List _pages = [ const ModManagerPage(), const TroubleshootingPage(), ]; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('RimWorld Mod Manager')), body: _pages[_selectedIndex], bottomNavigationBar: BottomNavigationBar( currentIndex: _selectedIndex, onTap: (index) { setState(() { _selectedIndex = index; }); }, items: const [ BottomNavigationBarItem(icon: Icon(Icons.extension), label: 'Mods'), BottomNavigationBarItem( icon: Icon(Icons.build), label: 'Troubleshoot', ), ], ), ); } } // Combined page for mod management with two-panel layout class ModManagerPage extends StatefulWidget { const ModManagerPage({super.key}); @override State createState() => _ModManagerPageState(); } class _ModManagerPageState extends State { // For all available mods (left panel) List _availableMods = []; // For active mods (right panel) List _activeMods = []; bool _isLoading = false; String _statusMessage = ''; bool _skipFileCount = false; bool _hasCycles = false; List? _cycleInfo; List> _incompatibleMods = []; List? _loadOrderErrors; final TextEditingController _searchController = TextEditingController(); String _searchQuery = ''; @override void initState() { super.initState(); // Check if mods are already loaded if (modManager.mods.isNotEmpty) { _loadModsFromGlobalState(); } _searchController.addListener(() { setState(() { _searchQuery = _searchController.text.toLowerCase(); }); }); } @override void dispose() { _searchController.dispose(); super.dispose(); } void _loadModsFromGlobalState() { setState(() { // 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; _statusMessage = 'Loaded ${_availableMods.length} mods'; }); } @override Widget build(BuildContext context) { return Scaffold( body: _isLoading && _availableMods.isEmpty ? _buildLoadingView() : _availableMods.isEmpty ? _buildEmptyState() : _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, ), ], ), ); } Widget _buildEmptyState() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.extension, size: 64), const SizedBox(height: 16), Text( 'Mod Manager', style: Theme.of(context).textTheme.headlineMedium, ), const SizedBox(height: 16), Text( 'Ready to scan for RimWorld mods.', style: Theme.of(context).textTheme.bodyLarge, ), const SizedBox(height: 12), Row( mainAxisSize: MainAxisSize.min, children: [ Checkbox( value: _skipFileCount, onChanged: (value) { setState(() { _skipFileCount = value ?? true; }); }, ), const Text('Skip file counting (faster loading)'), ], ), const SizedBox(height: 24), ElevatedButton( onPressed: _startLoadingMods, child: const Text('Scan for Mods'), ), ], ), ); } 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: [ Padding( padding: AppThemeExtension.of(context).paddingRegular, 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), // Load Dependencies button Tooltip( message: 'Automatically load missing dependencies for active mods', child: ElevatedButton.icon( icon: const Icon(Icons.download), label: const Text('Load Deps'), onPressed: _loadRequiredDependencies, ), ), 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, ), ], ), ), // 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, ), ), ), // Error display section if (_hasCycles || _incompatibleMods.isNotEmpty || (_loadOrderErrors?.isNotEmpty ?? false)) Container( margin: EdgeInsets.symmetric( horizontal: AppThemeExtension.of(context).paddingRegular.horizontal, vertical: AppThemeExtension.of(context).paddingSmall.vertical, ), padding: AppThemeExtension.of(context).paddingRegular, decoration: BoxDecoration( color: AppThemeExtension.of(context).errorBackgroundColor, borderRadius: BorderRadius.circular(4.0), border: Border.all(color: Colors.red.shade800), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Cycle warnings if (_hasCycles && _cycleInfo != null) Padding( padding: const EdgeInsets.only(bottom: 4.0), child: Row( children: [ Icon( Icons.loop, color: AppThemeExtension.of(context).warningColor, size: AppThemeExtension.of(context).iconSizeSmall, ), const SizedBox(width: 4), Expanded( child: Text( 'Dependency cycle detected: ${_cycleInfo!.join(" -> ")}', style: TextStyle( color: AppThemeExtension.of(context).warningColor, ), ), ), ], ), ), // Incompatible mod warnings if (_incompatibleMods.isNotEmpty) Padding( padding: const EdgeInsets.only(bottom: 4.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( Icons.warning, color: AppThemeExtension.of(context).warningColor, size: AppThemeExtension.of(context).iconSizeSmall, ), const SizedBox(width: 4), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${_incompatibleMods.length} incompatible mod pairs:', style: TextStyle( color: AppThemeExtension.of( context, ).warningColor, ), ), if (_incompatibleMods.length <= 3) ...List.generate(_incompatibleMods.length, ( index, ) { final pair = _incompatibleMods[index]; final mod1 = modManager.mods[pair[0]]?.name ?? pair[0]; final mod2 = modManager.mods[pair[1]]?.name ?? pair[1]; return Padding( padding: const EdgeInsets.only( left: 12.0, top: 2.0, ), child: Text( '• $mod1 ↔ $mod2', style: TextStyle( color: Colors.orange.shade300, fontSize: AppThemeExtension.of( context, ).textSizeSmall, ), ), ); }), ], ), ), ], ), ), // Other errors (missing dependencies, etc.) if (_loadOrderErrors?.isNotEmpty ?? false) Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( Icons.error_outline, color: AppThemeExtension.of(context).errorColor, size: AppThemeExtension.of(context).iconSizeSmall, ), const SizedBox(width: 4), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Dependency errors:', style: TextStyle( color: AppThemeExtension.of(context).errorColor, ), ), ...List.generate( _loadOrderErrors!.length > 5 ? 5 : _loadOrderErrors!.length, (index) => Padding( padding: const EdgeInsets.only( left: 12.0, top: 2.0, ), child: Text( '• ${_loadOrderErrors![index]}', style: TextStyle( color: Colors.red.shade300, fontSize: AppThemeExtension.of( context, ).textSizeRegular, ), ), ), ), if (_loadOrderErrors!.length > 5) Padding( padding: const EdgeInsets.only( left: 12.0, top: 4.0, ), child: Text( '(${_loadOrderErrors!.length - 5} more errors...)', style: TextStyle( color: Colors.red.shade300, fontSize: AppThemeExtension.of( context, ).textSizeSmall, fontStyle: FontStyle.italic, ), ), ), ], ), ), ], ), ], ), ), // Main split view Expanded( child: Row( children: [ // LEFT PANEL - All available mods (alphabetical) Expanded( child: Card( margin: AppThemeExtension.of(context).paddingRegular, 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}' : ''})', style: const TextStyle(fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), ), if (_searchQuery.isNotEmpty) Padding( padding: AppThemeExtension.of(context).paddingSmall, child: Text( 'Searching: "$_searchQuery"', style: TextStyle( fontSize: AppThemeExtension.of(context).textSizeSmall, 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 ? AppThemeExtension.of( context, ).enabledModColor : Colors.white, ), ), subtitle: Text( 'ID: ${mod.id}\nSize: ${mod.size} files', style: Theme.of(context).textTheme.bodySmall, ), tileColor: isActive ? AppThemeExtension.of( context, ).enabledModBackgroundColor : null, trailing: Row( mainAxisSize: MainAxisSize.min, children: [ if (mod.isBaseGame) Tooltip( message: 'Base Game', child: Icon( Icons.home, color: AppThemeExtension.of( context, ).baseGameColor, size: AppThemeExtension.of( context, ).iconSizeRegular, ), ), if (mod.isExpansion) Tooltip( message: 'Expansion', child: Icon( Icons.star, color: AppThemeExtension.of( context, ).expansionColor, size: AppThemeExtension.of( context, ).iconSizeRegular, ), ), const SizedBox(width: 4), 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, ).iconSizeRegular, ), ), if (mod.loadAfter.isNotEmpty) Tooltip( message: 'Loads after:\n${mod.loadAfter.join('\n')}', child: Icon( Icons.arrow_downward, color: AppThemeExtension.of( context, ).loadAfterColor, size: AppThemeExtension.of( context, ).iconSizeRegular, ), ), if (mod.loadBefore.isNotEmpty) Tooltip( message: 'Loads before:\n${mod.loadBefore.join('\n')}', child: Icon( Icons.arrow_upward, color: AppThemeExtension.of( context, ).loadBeforeColor, size: AppThemeExtension.of( context, ).iconSizeRegular, ), ), ], ), onTap: () { // Show mod details in future }, ), ); }, ), ), ], ), ), ), // RIGHT PANEL - Active mods (load order) Expanded( child: Card( margin: AppThemeExtension.of(context).paddingRegular, 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}' : ''})', style: const TextStyle(fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), ), Padding( padding: AppThemeExtension.of(context).paddingRegular, child: Text( _searchQuery.isNotEmpty ? 'Searching: "$_searchQuery"' : 'Larger mods are prioritized during auto-sorting.', style: TextStyle( fontSize: AppThemeExtension.of(context).textSizeSmall, 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: AppThemeExtension.of( context, ).paddingRegular, 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 ? const Color.fromRGBO( 0, 0, 255, 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: AppThemeExtension.of( context, ).textSizeSmall, 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: TextStyle( fontSize: AppThemeExtension.of( context, ).textSizeSmall, color: Colors.grey, ), ), ) : const SizedBox(), ), ), ], ), ), title: Text(mod.name), subtitle: Text( mod.id, style: TextStyle( fontSize: AppThemeExtension.of( context, ).textSizeSmall, ), ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ if (mod.isBaseGame) Tooltip( message: 'Base Game', child: Icon( Icons.home, color: AppThemeExtension.of( context, ).baseGameColor, size: AppThemeExtension.of( context, ).iconSizeRegular, ), ), if (mod.isExpansion) Tooltip( message: 'Expansion', child: Icon( Icons.star, color: AppThemeExtension.of( context, ).expansionColor, size: AppThemeExtension.of( context, ).iconSizeRegular, ), ), 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, ).iconSizeRegular, ), ), 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: AppThemeExtension.of( context, ).loadAfterColor, size: AppThemeExtension.of( context, ).iconSizeRegular, ), ), 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: AppThemeExtension.of( context, ).loadBeforeColor, size: AppThemeExtension.of( context, ).iconSizeRegular, ), ), ], ), onTap: () { // Show mod details in future }, ), ), ); }, ), ), ], ), ), ), ], ), ), ], ); } void _startLoadingMods() { setState(() { _availableMods.clear(); _activeMods.clear(); _isLoading = true; _statusMessage = 'Scanning for mods...'; _hasCycles = false; _cycleInfo = null; _incompatibleMods = []; }); // Create an async function to load mods Future loadMods() async { try { // First load available mods await for (final mod in modManager.loadAvailable()) { // Update UI for each mod loaded if (mounted) { setState(() { _statusMessage = 'Loaded mod: ${mod.name}'; }); } } // Then load active mods from config await for (final mod in modManager.loadActive()) { // Update UI as active mods are loaded if (mounted) { setState(() { _statusMessage = 'Loading active mod: ${mod.name}'; }); } } // Update the UI with all loaded mods if (mounted) { _loadModsFromGlobalState(); setState(() { _statusMessage = 'Loaded ${_availableMods.length} mods, ${_activeMods.length} active'; }); } } catch (error) { if (mounted) { setState(() { _isLoading = false; _statusMessage = 'Error loading mods: $error'; }); } } } // Start the loading process loadMods(); } 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; } // Get the current state before toggling final bool wasEnabled = mod.enabled; // Toggle the mod in the global mod manager modManager.setEnabled(mod.id, !wasEnabled); Logger.instance.info( 'Toggled mod ${mod.name} (${mod.id}) from ${wasEnabled ? 'enabled' : 'disabled'} to ${!wasEnabled ? 'enabled' : 'disabled'}', ); // Update the UI setState(() { // Update in the available mods list final index = _availableMods.indexWhere((m) => m.id == mod.id); if (index >= 0) { _availableMods[index] = modManager.mods[mod.id]!; } // Update the active mods list _activeMods = modManager.mods.values.where((m) => m.enabled).toList(); _statusMessage = 'Mod "${mod.name}" ${!wasEnabled ? 'activated' : 'deactivated'}'; }); } void _sortActiveMods() async { if (_activeMods.isEmpty) { setState(() { _statusMessage = 'No active mods to sort'; }); return; } setState(() { _isLoading = true; _statusMessage = 'Sorting mods based on dependencies...'; _hasCycles = false; _cycleInfo = null; _incompatibleMods = []; _loadOrderErrors = null; }); // Use a Future.delayed to allow the UI to update await Future.delayed(Duration.zero); try { final logger = Logger.instance; logger.info('Starting auto-sort of ${_activeMods.length} active mods'); // Generate a load order for active mods final loadOrder = modManager.generateLoadOrder(); // Store all errors if (loadOrder.hasErrors) { setState(() { _loadOrderErrors = List.from(loadOrder.errors); }); logger.warning( 'Found ${loadOrder.errors.length} errors during sorting', ); for (final error in loadOrder.errors) { logger.warning(' - $error'); } setState(() { _hasCycles = loadOrder.errors.any( (e) => e.contains('Cyclic dependency'), ); if (_hasCycles) { // Extract cycle info from error message final cycleError = loadOrder.errors.firstWhere( (e) => e.contains('Cyclic dependency'), orElse: () => '', ); logger.warning('Detected dependency cycle: $cycleError'); if (cycleError.isNotEmpty) { // Extract cycle path from error message final startIndex = cycleError.indexOf(':'); if (startIndex != -1) { final pathStr = cycleError.substring(startIndex + 1).trim(); _cycleInfo = pathStr.split(' -> '); logger.info( 'Extracted cycle path: ${_cycleInfo!.join(" -> ")}', ); } } } }); } else { _loadOrderErrors = null; } // Check for incompatibilities _incompatibleMods = modManager.checkIncompatibilities( modManager.activeMods.keys.toList(), ); if (_incompatibleMods.isNotEmpty) { logger.warning( 'Found ${_incompatibleMods.length} incompatible mod pairs:', ); for (final pair in _incompatibleMods) { final mod1 = modManager.mods[pair[0]]?.name ?? pair[0]; final mod2 = modManager.mods[pair[1]]?.name ?? pair[1]; logger.warning(' - $mod1 is incompatible with $mod2'); } } // Get sorted mods from the load order final List sortedMods = []; for (final modId in loadOrder.loadOrder) { if (modManager.mods.containsKey(modId)) { sortedMods.add(modManager.mods[modId]!); } } logger.info( 'Sorting complete. Arranged ${sortedMods.length} mods in load order', ); if (sortedMods.isNotEmpty) { logger.info( 'First 5 mods in order: ${sortedMods.take(5).map((m) => m.name).join(', ')}', ); if (sortedMods.length > 5) { logger.info('... (${sortedMods.length - 5} more) ...'); logger.info( 'Last 3 mods in order: ${sortedMods.reversed.take(3).map((m) => m.name).toList().reversed.join(', ')}', ); } } setState(() { _activeMods = sortedMods; _isLoading = false; _statusMessage = 'Sorting complete! ${sortedMods.length} mods sorted.'; if (_hasCycles) { _statusMessage += ' Warning: Dependency cycles were found and fixed.'; } if (_incompatibleMods.isNotEmpty) { _statusMessage += ' Warning: ${_incompatibleMods.length} incompatible mod pairs found.'; } }); } catch (e) { Logger.instance.error('Error during auto-sort: $e'); setState(() { _isLoading = false; _statusMessage = 'Error sorting mods: $e'; }); } } void _saveModOrder() async { if (_activeMods.isEmpty) { setState(() { _statusMessage = 'No active mods to save'; }); return; } setState(() { _isLoading = true; _statusMessage = 'Saving mod load order...'; }); try { final logger = Logger.instance; logger.info( 'Saving mod load order for ${_activeMods.length} active mods to $configPath', ); modManager.saveToConfig(LoadOrder(_activeMods)); setState(() { _isLoading = false; _statusMessage = 'Mod load order saved successfully!'; }); // Show success message if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Mod order saved to config. ${_activeMods.length} mods enabled.', ), backgroundColor: Colors.green, ), ); } } catch (e) { final logger = Logger.instance; logger.error('Error saving mod load order: $e'); setState(() { _isLoading = false; _statusMessage = 'Error saving mod load order: $e'; }); // Show error message if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error saving mod order: $e'), backgroundColor: Colors.red, ), ); } } } // Load all required dependencies for active mods void _loadRequiredDependencies() async { if (_activeMods.isEmpty) { setState(() { _statusMessage = 'No active mods to load dependencies for'; }); return; } setState(() { _isLoading = true; _statusMessage = 'Loading required dependencies...'; _hasCycles = false; _cycleInfo = null; _incompatibleMods = []; _loadOrderErrors = null; }); // Use a Future.delayed to allow the UI to update await Future.delayed(Duration.zero); try { final logger = Logger.instance; logger.info( 'Starting dependency resolution for ${_activeMods.length} active mods', ); // Get current active mod count for comparison final initialActiveCount = modManager.activeMods.length; // Load required dependencies and get the load order final loadOrder = modManager.loadRequired(); // Store any errors if (loadOrder.hasErrors) { setState(() { _loadOrderErrors = List.from(loadOrder.errors); }); logger.warning( 'Found ${loadOrder.errors.length} errors during dependency loading', ); for (final error in loadOrder.errors) { logger.warning(' - $error'); } // Check for cycles setState(() { _hasCycles = loadOrder.errors.any( (e) => e.contains('Cyclic dependency'), ); if (_hasCycles) { // Extract cycle info from error message final cycleError = loadOrder.errors.firstWhere( (e) => e.contains('Cyclic dependency'), orElse: () => '', ); logger.warning('Detected dependency cycle: $cycleError'); if (cycleError.isNotEmpty) { // Extract cycle path from error message final startIndex = cycleError.indexOf(':'); if (startIndex != -1) { final pathStr = cycleError.substring(startIndex + 1).trim(); _cycleInfo = pathStr.split(' -> '); logger.info( 'Extracted cycle path: ${_cycleInfo!.join(" -> ")}', ); } } } }); } else { _loadOrderErrors = null; } // Check for incompatibilities _incompatibleMods = modManager.checkIncompatibilities( modManager.activeMods.keys.toList(), ); if (_incompatibleMods.isNotEmpty) { logger.warning( 'Found ${_incompatibleMods.length} incompatible mod pairs:', ); for (final pair in _incompatibleMods) { final mod1 = modManager.mods[pair[0]]?.name ?? pair[0]; final mod2 = modManager.mods[pair[1]]?.name ?? pair[1]; logger.warning(' - $mod1 is incompatible with $mod2'); } } // Get sorted mods from the load order final List sortedMods = []; for (final modId in loadOrder.loadOrder) { if (modManager.mods.containsKey(modId)) { sortedMods.add(modManager.mods[modId]!); } } // Calculate how many dependencies were added final newModsCount = modManager.activeMods.length - initialActiveCount; logger.info( 'Dependency loading complete. Enabled $newModsCount new dependencies.', ); if (newModsCount > 0) { logger.info('Newly enabled dependencies:'); final activeModIds = modManager.activeMods.keys.toList(); for (int i = 0; i < newModsCount; i++) { final modId = activeModIds[initialActiveCount + i]; logger.info(' - ${modManager.mods[modId]?.name ?? modId} ($modId)'); } } setState(() { _activeMods = sortedMods; _isLoading = false; if (newModsCount > 0) { _statusMessage = 'Added $newModsCount required dependencies!'; } else { _statusMessage = 'All dependencies are already loaded.'; } if (_hasCycles) { _statusMessage += ' Warning: Dependency cycles were found and fixed.'; } if (_incompatibleMods.isNotEmpty) { _statusMessage += ' Warning: ${_incompatibleMods.length} incompatible mod pairs found.'; } }); } catch (e) { Logger.instance.error('Error during dependency loading: $e'); setState(() { _isLoading = false; _statusMessage = 'Error loading dependencies: $e'; }); } } } // Page for troubleshooting problematic mods class TroubleshootingPage extends StatelessWidget { const TroubleshootingPage({super.key}); @override Widget build(BuildContext context) { return const ModTroubleshooterWidget(); } }