diff --git a/lib/main.dart b/lib/main.dart index 05c2d8e..a4122c9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,6 @@ import 'dart:io'; // TODO: Fix "load dependencies", it causes fake errors between expansions and base game -// TODO: Add an icon for incompatibilities on the mod list -// TODO: Get rid of the demo button WHAT THE FUCK IS THE DEMO BUTTON?? import 'package:flutter/material.dart'; import 'package:rimworld_modman/logger.dart'; import 'package:rimworld_modman/components/html_tooltip.dart'; @@ -31,6 +29,7 @@ class AppThemeExtension extends ThemeExtension { final Color linkColor; final Color loadAfterColor; final Color loadBeforeColor; + final Color incompatibleColor; AppThemeExtension({ required this.iconSizeSmall, @@ -52,6 +51,7 @@ class AppThemeExtension extends ThemeExtension { required this.linkColor, required this.loadAfterColor, required this.loadBeforeColor, + required this.incompatibleColor, }); static AppThemeExtension of(BuildContext context) { @@ -79,6 +79,7 @@ class AppThemeExtension extends ThemeExtension { Color? linkColor, Color? loadAfterColor, Color? loadBeforeColor, + Color? incompatibleColor, }) { return AppThemeExtension( iconSizeSmall: iconSizeSmall ?? this.iconSizeSmall, @@ -101,6 +102,7 @@ class AppThemeExtension extends ThemeExtension { linkColor: linkColor ?? this.linkColor, loadAfterColor: loadAfterColor ?? this.loadAfterColor, loadBeforeColor: loadBeforeColor ?? this.loadBeforeColor, + incompatibleColor: incompatibleColor ?? this.incompatibleColor, ); } @@ -138,6 +140,7 @@ class AppThemeExtension extends ThemeExtension { linkColor: Color.lerp(linkColor, other.linkColor, t)!, loadAfterColor: Color.lerp(loadAfterColor, other.loadAfterColor, t)!, loadBeforeColor: Color.lerp(loadBeforeColor, other.loadBeforeColor, t)!, + incompatibleColor: Color.lerp(incompatibleColor, other.incompatibleColor, t)!, ); } @@ -167,6 +170,7 @@ class AppThemeExtension extends ThemeExtension { linkColor: Colors.orange, loadAfterColor: Colors.blue, loadBeforeColor: Colors.green, + incompatibleColor: Colors.red.shade400, ); } @@ -267,10 +271,6 @@ class _ModManagerHomePageState extends State { icon: Icon(Icons.build), label: 'Troubleshoot', ), - BottomNavigationBarItem( - icon: Icon(Icons.format_paint), - label: 'Demo', - ), ], ), ); @@ -302,6 +302,8 @@ class _ModManagerPageState extends State { final TextEditingController _searchController = TextEditingController(); String _searchQuery = ''; + bool _useRegex = false; + RegExp? _searchRegex; @override void initState() { @@ -310,12 +312,6 @@ class _ModManagerPageState extends State { if (modManager.mods.isNotEmpty) { _loadModsFromGlobalState(); } - - _searchController.addListener(() { - setState(() { - _searchQuery = _searchController.text.toLowerCase(); - }); - }); } @override @@ -409,27 +405,49 @@ class _ModManagerPageState extends State { 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(); + List filteredAvailableMods; + List filteredActiveMods; - final filteredActiveMods = - _searchQuery.isEmpty - ? _activeMods - : _activeMods - .where( - (mod) => - mod.name.toLowerCase().contains(_searchQuery) || - mod.id.toLowerCase().contains(_searchQuery), - ) - .toList(); + if (_searchQuery.isEmpty) { + filteredAvailableMods = _availableMods; + filteredActiveMods = _activeMods; + } else { + if (_useRegex && _searchRegex != null) { + // Use regex pattern for filtering + filteredAvailableMods = _availableMods + .where( + (mod) => + _searchRegex!.hasMatch(mod.name.toLowerCase()) || + _searchRegex!.hasMatch(mod.id.toLowerCase()), + ) + .toList(); + + filteredActiveMods = _activeMods + .where( + (mod) => + _searchRegex!.hasMatch(mod.name.toLowerCase()) || + _searchRegex!.hasMatch(mod.id.toLowerCase()), + ) + .toList(); + } else { + // Use simple string contains for filtering + filteredAvailableMods = _availableMods + .where( + (mod) => + mod.name.toLowerCase().contains(_searchQuery) || + mod.id.toLowerCase().contains(_searchQuery), + ) + .toList(); + + filteredActiveMods = _activeMods + .where( + (mod) => + mod.name.toLowerCase().contains(_searchQuery) || + mod.id.toLowerCase().contains(_searchQuery), + ) + .toList(); + } + } return Column( children: [ @@ -457,6 +475,51 @@ class _ModManagerPageState extends State { ) : null, ), + onChanged: (value) { + setState(() { + _searchQuery = value.toLowerCase(); + + // Try to compile regex if regex mode is enabled + if (_useRegex && _searchQuery.isNotEmpty) { + try { + _searchRegex = RegExp(_searchQuery, caseSensitive: false); + } catch (e) { + // If regex is invalid, fallback to normal search + _searchRegex = null; + } + } + }); + }, + ), + ), + const SizedBox(width: 8), + // Regex toggle + Tooltip( + message: 'Use regex pattern matching', + child: Row( + children: [ + Checkbox( + value: _useRegex, + onChanged: (value) { + setState(() { + _useRegex = value ?? false; + + // Try to compile regex if toggled on + if (_useRegex && _searchQuery.isNotEmpty) { + try { + _searchRegex = RegExp(_searchQuery, caseSensitive: false); + } catch (e) { + // If regex fails, keep checkbox on but disable regex internally + _searchRegex = null; + } + } else { + _searchRegex = null; + } + }); + }, + ), + const Text('Regex'), + ], ), ), const SizedBox(width: 8), @@ -841,6 +904,22 @@ class _ModManagerPageState extends State { ).iconSizeRegular, ), ), + if (mod.incompatibilities.isNotEmpty) + Tooltip( + message: + 'Incompatible with:\n${mod.incompatibilities.join('\n')}', + child: Icon( + Icons.warning_amber_rounded, + color: + AppThemeExtension.of( + context, + ).incompatibleColor, + size: + AppThemeExtension.of( + context, + ).iconSizeRegular, + ), + ), ], ), onTap: () { @@ -1097,6 +1176,22 @@ class _ModManagerPageState extends State { ).iconSizeRegular, ), ), + if (mod.incompatibilities.isNotEmpty) + Tooltip( + message: + 'Incompatible with:\n${mod.incompatibilities.join('\n')}', + child: Icon( + Icons.warning_amber_rounded, + color: + AppThemeExtension.of( + context, + ).incompatibleColor, + size: + AppThemeExtension.of( + context, + ).iconSizeRegular, + ), + ), ], ), onTap: () { diff --git a/lib/mod_list.dart b/lib/mod_list.dart index 9a2553f..34e058f 100644 --- a/lib/mod_list.dart +++ b/lib/mod_list.dart @@ -702,13 +702,76 @@ class ModList { LoadOrder loadRequired([LoadOrder? loadOrder]) { loadOrder ??= LoadOrder(); final toEnable = []; + final logger = Logger.instance; + + // First, identify all base game and expansion mods + final baseGameIds = {}; + final expansionIds = {}; + + for (final entry in mods.entries) { + if (entry.value.isBaseGame) { + baseGameIds.add(entry.key); + } else if (entry.value.isExpansion) { + expansionIds.add(entry.key); + } + } + + logger.info("Base game mods: ${baseGameIds.join(', ')}"); + logger.info("Expansion mods: ${expansionIds.join(', ')}"); + + // Load dependencies for all active mods for (final modid in activeMods.keys) { loadDependencies(modid, loadOrder, toEnable); } + + // Enable all required dependencies for (final modid in toEnable) { setEnabled(modid, true); } - return generateLoadOrder(loadOrder); + + // Generate the load order + final newLoadOrder = generateLoadOrder(loadOrder); + + // Filter out any error messages related to incompatibilities between base game and expansions + if (newLoadOrder.hasErrors) { + final filteredErrors = []; + + for (final error in newLoadOrder.errors) { + // Check if the error is about incompatibility + if (error.contains('Incompatibility detected:')) { + // Extract the mod IDs from the error message + final parts = error.split(' is incompatible with '); + if (parts.length == 2) { + final firstModId = parts[0].replaceAll('Incompatibility detected: ', ''); + final secondModId = parts[1]; + + // Check if either mod is a base game or expansion + final isBaseGameOrExpansion = + baseGameIds.contains(firstModId) || baseGameIds.contains(secondModId) || + expansionIds.contains(firstModId) || expansionIds.contains(secondModId); + + // Only keep the error if it's not between base game/expansions + if (!isBaseGameOrExpansion) { + filteredErrors.add(error); + } else { + logger.info("Ignoring incompatibility between base game or expansion mods: $error"); + } + } else { + // If we can't parse the error, keep it + filteredErrors.add(error); + } + } else { + // Keep non-incompatibility errors + filteredErrors.add(error); + } + } + + // Replace the errors with the filtered list + newLoadOrder.errors.clear(); + newLoadOrder.errors.addAll(filteredErrors); + } + + return newLoadOrder; } LoadOrder loadRequiredBaseGame([LoadOrder? loadOrder]) {