diff --git a/lib/main.dart b/lib/main.dart index 9b668a5..d1f7a2b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,892 +1,1301 @@ -//import 'package:flutter/material.dart'; -//import 'package:rimworld_modman/logger.dart'; -//import 'package:rimworld_modman/mod.dart'; -// -//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 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 = ''; -// int _totalModsFound = 0; -// bool _skipFileCount = false; -// bool _hasCycles = false; -// List? _cycleInfo; -// List> _incompatibleMods = []; -// -// final TextEditingController _searchController = TextEditingController(); -// String _searchQuery = ''; -// -// @override -// void initState() { -// super.initState(); -// // Check if mods are already loaded in the global modManager -// if (modManager.modsLoaded) { -// _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 = modManager.loadingStatus; -// _totalModsFound = modManager.totalModsFound; -// }); -// } -// -// @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: 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, -// ), -// ], -// ), -// ), -// -// // 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, -// ), -// ), -// ), -// -// // 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}' : ''})', -// 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: 24, -// ), -// ), -// if (mod.isExpansion) -// const Tooltip( -// message: 'Expansion', -// child: Icon( -// Icons.star, -// color: Colors.yellow, -// size: 24, -// ), -// ), -// const SizedBox(width: 4), -// if (mod.dependencies.isNotEmpty) -// Tooltip( -// message: -// 'Dependencies:\n${mod.dependencies.join('\n')}', -// child: const Icon( -// Icons.link, -// color: Colors.orange, -// size: 24, -// ), -// ), -// if (mod.loadAfter.isNotEmpty) -// Tooltip( -// message: -// 'Loads after:\n${mod.loadAfter.join('\n')}', -// child: const Icon( -// Icons.arrow_downward, -// color: Colors.blue, -// size: 24, -// ), -// ), -// if (mod.loadBefore.isNotEmpty) -// Tooltip( -// message: -// 'Loads before:\n${mod.loadBefore.join('\n')}', -// child: const Icon( -// Icons.arrow_upward, -// color: Colors.green, -// size: 24, -// ), -// ), -// ], -// ), -// onTap: () { -// // Show mod details in future -// }, -// ), -// ); -// }, -// ), -// ), -// ], -// ), -// ), -// ), -// -// // 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}' : ''})', -// 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.dependencies.isNotEmpty) -// Tooltip( -// message: -// 'Dependencies:\n${mod.dependencies.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 -// }, -// ), -// ), -// ); -// }, -// ), -// ), -// ], -// ), -// ), -// ), -// ], -// ), -// ), -// ], -// ); -// } -// -// 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, -// dependencies: mod.dependencies, -// loadAfter: mod.loadAfter, -// loadBefore: mod.loadBefore, -// incompatibilities: mod.incompatibilities, -// 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 active mods to sort'; -// }); -// 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(() { -// _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) { -// 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 { -// // Create a ConfigFile instance -// final configFile = ConfigFile(path: configPath); -// -// // Load the current config -// await configFile.load(); -// -// // Replace the mods with our active mods list -// configFile.mods.clear(); -// for (final mod in _activeMods) { -// configFile.mods.add(mod); -// } -// -// // Save the updated config -// configFile.save(); -// -// setState(() { -// _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, -// ), -// ); -// } -// } -//} -// -//// 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'), -// ), -// ], -// ), -// ); -// } -//} -// \ No newline at end of file +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'; + +// 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, + ), + ), + 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 = ''; + int _totalModsFound = 0; + 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'; + _totalModsFound = _availableMods.length; + }); + } + + @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: 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), + // 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: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + color: Colors.red.shade900.withOpacity(0.3), + 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: [ + const Icon(Icons.loop, color: Colors.orange, size: 16), + const SizedBox(width: 4), + Expanded( + child: Text( + 'Dependency cycle detected: ${_cycleInfo!.join(" -> ")}', + style: const TextStyle(color: Colors.orange), + ), + ), + ], + ), + ), + + // Incompatible mod warnings + if (_incompatibleMods.isNotEmpty) + Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon( + Icons.warning, + color: Colors.orange, + size: 16, + ), + const SizedBox(width: 4), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${_incompatibleMods.length} incompatible mod pairs:', + style: const TextStyle(color: Colors.orange), + ), + 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: 12, + ), + ), + ); + }), + ], + ), + ), + ], + ), + ), + + // Other errors (missing dependencies, etc.) + if (_loadOrderErrors?.isNotEmpty ?? false) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon( + Icons.error_outline, + color: Colors.red, + size: 16, + ), + const SizedBox(width: 4), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Dependency errors:', + style: TextStyle(color: Colors.red), + ), + ...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: 12, + ), + ), + ), + ), + 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: 12, + fontStyle: FontStyle.italic, + ), + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + + // 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}' : ''})', + 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: 24, + ), + ), + if (mod.isExpansion) + const Tooltip( + message: 'Expansion', + child: Icon( + Icons.star, + color: Colors.yellow, + size: 24, + ), + ), + const SizedBox(width: 4), + if (mod.dependencies.isNotEmpty) + Tooltip( + message: + 'Dependencies:\n${mod.dependencies.join('\n')}', + child: const Icon( + Icons.link, + color: Colors.orange, + size: 24, + ), + ), + if (mod.loadAfter.isNotEmpty) + Tooltip( + message: + 'Loads after:\n${mod.loadAfter.join('\n')}', + child: const Icon( + Icons.arrow_downward, + color: Colors.blue, + size: 24, + ), + ), + if (mod.loadBefore.isNotEmpty) + Tooltip( + message: + 'Loads before:\n${mod.loadBefore.join('\n')}', + child: const Icon( + Icons.arrow_upward, + color: Colors.green, + size: 24, + ), + ), + ], + ), + onTap: () { + // Show mod details in future + }, + ), + ); + }, + ), + ), + ], + ), + ), + ), + + // 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}' : ''})', + 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.dependencies.isNotEmpty) + Tooltip( + message: + 'Dependencies:\n${mod.dependencies.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 + }, + ), + ), + ); + }, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ], + ); + } + + 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}'; + _totalModsFound = modManager.mods.length; + }); + } + } + + // 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', + ); + + // Save the mod list to the XML config file + final file = File(configPath); + final buffer = StringBuffer(); + + buffer.writeln(''); + buffer.writeln(''); + buffer.writeln(' 1'); + + // Write active mods + buffer.writeln(' '); + for (final mod in _activeMods) { + buffer.writeln('
  • ${mod.id}
  • '); + logger.info(' - Adding mod to config: ${mod.name} (${mod.id})'); + } + buffer.writeln('
    '); + + // Count expansions + final expansions = _availableMods.where((m) => m.isExpansion).toList(); + logger.info('Found ${expansions.length} expansions to include in config'); + + // Add known expansions + buffer.writeln(' '); + for (final mod in expansions) { + buffer.writeln('
  • ${mod.id}
  • '); + logger.info(' - Adding expansion to config: ${mod.name} (${mod.id})'); + } + buffer.writeln('
    '); + + buffer.writeln('
    '); + + // Ensure directory exists + final directory = Directory(configRoot); + if (!directory.existsSync()) { + logger.info('Creating config directory: $configRoot'); + directory.createSync(recursive: true); + } + + // Write to file + logger.info('Writing config file to $configPath'); + await file.writeAsString(buffer.toString()); + logger.info('Successfully saved mod configuration'); + + setState(() { + _isLoading = false; + _statusMessage = 'Mod load order saved successfully!'; + }); + + // Show success message + 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 + 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 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'), + ), + ], + ), + ); + } +} diff --git a/lib/mod_list.dart b/lib/mod_list.dart index 89caf08..cc38f0b 100644 --- a/lib/mod_list.dart +++ b/lib/mod_list.dart @@ -34,6 +34,7 @@ class ModList { } final List entities = directory.listSync(); + // TODO: Count only the latest version of each mod and not all versions final List modDirectories = entities.whereType().map((dir) => dir.path).toList(); @@ -85,6 +86,7 @@ class ModList { logger.info( 'Loaded mod from directory: ${mod.name} (ID: ${mod.id}) in $modTime ms', ); + yield mod; } catch (e) { logger.error('Error loading mod from directory: $modDir'); logger.error('Error: $e'); @@ -171,7 +173,7 @@ class ModList { logger.warning('Mod $modId already exists in mods list, overwriting'); } mods[modId] = mod; - setEnabled(modId, mod.enabled); + setEnabled(modId, true); yield mod; } @@ -522,10 +524,10 @@ class ModList { toEnable ??= []; seen ??= {}; cyclePath ??= []; - + // Add current mod to cycle path cyclePath.add(modId); - + for (final dep in mod.dependencies) { if (!mods.containsKey(dep)) { loadOrder.errors.add( @@ -540,7 +542,9 @@ class ModList { if (cycleStart >= 0) { // Extract the cycle part List cycleIds = [...cyclePath.sublist(cycleStart), modId]; - loadOrder.errors.add('Cyclic dependency detected: ${cycleIds.join(' -> ')}'); + loadOrder.errors.add( + 'Cyclic dependency detected: ${cycleIds.join(' -> ')}', + ); } else { loadOrder.errors.add('Cyclic dependency detected: $modId -> $dep'); } @@ -548,9 +552,15 @@ class ModList { } seen[dep] = true; toEnable.add(depMod.id); - loadDependencies(depMod.id, loadOrder, toEnable, seen, List.from(cyclePath)); + loadDependencies( + depMod.id, + loadOrder, + toEnable, + seen, + List.from(cyclePath), + ); } - + return loadOrder; }