diff --git a/lib/main.dart b/lib/main.dart index 0d50f42..9b668a5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,893 +1,892 @@ -import 'package:flutter/material.dart'; -import 'package:rimworld_modman/logger.dart'; -import 'package:rimworld_modman/mod.dart'; -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 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'), - ), - ], - ), - ); - } -} +//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 diff --git a/test/widget_test.dart b/test/widget_test.dart index fd64220..567afb3 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -5,26 +5,27 @@ // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:rimworld_modman/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} +//import 'package:flutter/material.dart'; +//import 'package:flutter_test/flutter_test.dart'; +// +//import 'package:rimworld_modman/main.dart'; +// +//void main() { +// testWidgets('Counter increments smoke test', (WidgetTester tester) async { +// // Build our app and trigger a frame. +// await tester.pumpWidget(const MyApp()); +// +// // Verify that our counter starts at 0. +// expect(find.text('0'), findsOneWidget); +// expect(find.text('1'), findsNothing); +// +// // Tap the '+' icon and trigger a frame. +// await tester.tap(find.byIcon(Icons.add)); +// await tester.pump(); +// +// // Verify that our counter has incremented. +// expect(find.text('0'), findsNothing); +// expect(find.text('1'), findsOneWidget); +// }); +//} +// \ No newline at end of file