import 'package:flutter/material.dart'; import 'dart:io'; import 'package:rimworld_modman/modloader.dart'; // Global variable to store loaded mods for access across the app 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); // 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, ), ), 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 ModListPage(), const LoadOrderPage(), 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.reorder), label: 'Load Order', ), BottomNavigationBarItem( icon: Icon(Icons.build), label: 'Troubleshoot', ), ], ), ); } } // Page to display all installed mods with enable/disable toggles class ModListPage extends StatefulWidget { const ModListPage({super.key}); @override State createState() => _ModListPageState(); } class _ModListPageState extends State { List _loadedMods = []; bool _isLoading = false; String _loadingStatus = ''; int _totalModsFound = 0; bool _skipFileCount = false; // Skip file counting by default for faster loading @override void initState() { super.initState(); // Check if mods are already loaded in the global modManager if (modManager.modsLoaded) { _loadModsFromGlobalState(); } } void _loadModsFromGlobalState() { setState(() { _loadedMods = modManager.mods.values.toList(); _loadedMods.sort((a, b) => a.name.compareTo(b.name)); _isLoading = false; _loadingStatus = modManager.loadingStatus; _totalModsFound = modManager.totalModsFound; }); } @override Widget build(BuildContext context) { return Scaffold( body: _loadedMods.isEmpty && !_isLoading ? _buildEmptyState() : _buildModList(), ); } Widget _buildEmptyState() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.extension, size: 64), const SizedBox(height: 16), Text('Mod List', 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 _buildModList() { 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, ), ], ), ), if (!_isLoading && _loadedMods.isEmpty) Center( child: Padding( padding: const EdgeInsets.all(16.0), child: Text( 'No mods found. Try reloading.', textAlign: TextAlign.center, ), ), ), 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) 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() { 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( 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( 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_back, 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_forward, color: Colors.green, size: 24, ), ), ], ), ), ); }, ), ), ], ), ), ); } void _sortMods() async { if (modManager.mods.isEmpty) { setState(() { _statusMessage = 'No mods have been loaded yet. Please go to the Mods tab and load mods first.'; }); return; } setState(() { _isLoading = true; _statusMessage = 'Sorting mods based on dependencies...'; _hasCycles = false; _cycleInfo = null; _incompatibleMods = []; }); // Use a Future.delayed to allow the UI to update await Future.delayed(Duration.zero); try { // Check for cycles first final hardGraph = modManager.buildDependencyGraph(); final cycle = modManager.detectCycle(hardGraph); if (cycle != null) { setState(() { _hasCycles = true; _cycleInfo = cycle; }); } // Get incompatibilities _incompatibleMods = modManager.findIncompatibilities(); // Get the sorted mods final sortedMods = modManager.getModsInLoadOrder(); setState(() { _sortedMods = 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) { setState(() { _isLoading = false; _statusMessage = 'Error sorting mods: $e'; }); } } void _saveModOrder() async { if (_sortedMods.isEmpty) return; setState(() { _isLoading = true; _statusMessage = 'Saving mod load order...'; }); try { // Create a ConfigFile instance final configFile = ConfigFile(path: configPath); // Load the current config configFile.load(); // Replace the mods with our sorted list // We need to convert our Mods to the format expected by the config file configFile.mods.clear(); for (final mod in _sortedMods) { configFile.mods.add(mod); } // Save the updated config configFile.save(); setState(() { _isLoading = false; _statusMessage = 'Mod load order saved successfully!'; }); } catch (e) { setState(() { _isLoading = false; _statusMessage = 'Error saving mod load order: $e'; }); } } } // Page for troubleshooting problematic mods class TroubleshootingPage extends StatelessWidget { const TroubleshootingPage({super.key}); @override Widget build(BuildContext context) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.build, size: 64), 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( 'Find problematic mods with smart batch testing.', style: Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.center, ), ), const SizedBox(height: 24), ElevatedButton( onPressed: () { // TODO: Implement troubleshooting wizard }, child: const Text('Start Troubleshooting'), ), ], ), ); } }