Compare commits

...

4 Commits

3 changed files with 1202 additions and 110 deletions

View File

@@ -4,6 +4,176 @@ import 'package:flutter/material.dart';
import 'package:rimworld_modman/logger.dart';
import 'package:rimworld_modman/mod.dart';
import 'package:rimworld_modman/mod_list.dart';
import 'package:rimworld_modman/mod_troubleshooter_widget.dart';
// Theme extension to store app-specific constants
class AppThemeExtension extends ThemeExtension<AppThemeExtension> {
final double iconSizeSmall;
final double iconSizeRegular;
final double iconSizeLarge;
final double textSizeSmall;
final double textSizeRegular;
final double textSizeLarge;
final EdgeInsets paddingSmall;
final EdgeInsets paddingRegular;
final EdgeInsets paddingLarge;
final Color enabledModColor;
final Color enabledModBackgroundColor;
final Color errorBackgroundColor;
final Color warningColor;
final Color errorColor;
final Color baseGameColor;
final Color expansionColor;
final Color linkColor;
final Color loadAfterColor;
final Color loadBeforeColor;
AppThemeExtension({
required this.iconSizeSmall,
required this.iconSizeRegular,
required this.iconSizeLarge,
required this.textSizeSmall,
required this.textSizeRegular,
required this.textSizeLarge,
required this.paddingSmall,
required this.paddingRegular,
required this.paddingLarge,
required this.enabledModColor,
required this.enabledModBackgroundColor,
required this.errorBackgroundColor,
required this.warningColor,
required this.errorColor,
required this.baseGameColor,
required this.expansionColor,
required this.linkColor,
required this.loadAfterColor,
required this.loadBeforeColor,
});
static AppThemeExtension of(BuildContext context) {
return Theme.of(context).extension<AppThemeExtension>()!;
}
@override
ThemeExtension<AppThemeExtension> copyWith({
double? iconSizeSmall,
double? iconSizeRegular,
double? iconSizeLarge,
double? textSizeSmall,
double? textSizeRegular,
double? textSizeLarge,
EdgeInsets? paddingSmall,
EdgeInsets? paddingRegular,
EdgeInsets? paddingLarge,
Color? enabledModColor,
Color? enabledModBackgroundColor,
Color? errorBackgroundColor,
Color? warningColor,
Color? errorColor,
Color? baseGameColor,
Color? expansionColor,
Color? linkColor,
Color? loadAfterColor,
Color? loadBeforeColor,
}) {
return AppThemeExtension(
iconSizeSmall: iconSizeSmall ?? this.iconSizeSmall,
iconSizeRegular: iconSizeRegular ?? this.iconSizeRegular,
iconSizeLarge: iconSizeLarge ?? this.iconSizeLarge,
textSizeSmall: textSizeSmall ?? this.textSizeSmall,
textSizeRegular: textSizeRegular ?? this.textSizeRegular,
textSizeLarge: textSizeLarge ?? this.textSizeLarge,
paddingSmall: paddingSmall ?? this.paddingSmall,
paddingRegular: paddingRegular ?? this.paddingRegular,
paddingLarge: paddingLarge ?? this.paddingLarge,
enabledModColor: enabledModColor ?? this.enabledModColor,
enabledModBackgroundColor:
enabledModBackgroundColor ?? this.enabledModBackgroundColor,
errorBackgroundColor: errorBackgroundColor ?? this.errorBackgroundColor,
warningColor: warningColor ?? this.warningColor,
errorColor: errorColor ?? this.errorColor,
baseGameColor: baseGameColor ?? this.baseGameColor,
expansionColor: expansionColor ?? this.expansionColor,
linkColor: linkColor ?? this.linkColor,
loadAfterColor: loadAfterColor ?? this.loadAfterColor,
loadBeforeColor: loadBeforeColor ?? this.loadBeforeColor,
);
}
@override
ThemeExtension<AppThemeExtension> lerp(
covariant ThemeExtension<AppThemeExtension>? other,
double t,
) {
if (other is! AppThemeExtension) {
return this;
}
return AppThemeExtension(
iconSizeSmall: lerpDouble(iconSizeSmall, other.iconSizeSmall, t),
iconSizeRegular: lerpDouble(iconSizeRegular, other.iconSizeRegular, t),
iconSizeLarge: lerpDouble(iconSizeLarge, other.iconSizeLarge, t),
textSizeSmall: lerpDouble(textSizeSmall, other.textSizeSmall, t),
textSizeRegular: lerpDouble(textSizeRegular, other.textSizeRegular, t),
textSizeLarge: lerpDouble(textSizeLarge, other.textSizeLarge, t),
paddingSmall: EdgeInsets.lerp(paddingSmall, other.paddingSmall, t)!,
paddingRegular: EdgeInsets.lerp(paddingRegular, other.paddingRegular, t)!,
paddingLarge: EdgeInsets.lerp(paddingLarge, other.paddingLarge, t)!,
enabledModColor: Color.lerp(enabledModColor, other.enabledModColor, t)!,
enabledModBackgroundColor:
Color.lerp(
enabledModBackgroundColor,
other.enabledModBackgroundColor,
t,
)!,
errorBackgroundColor:
Color.lerp(errorBackgroundColor, other.errorBackgroundColor, t)!,
warningColor: Color.lerp(warningColor, other.warningColor, t)!,
errorColor: Color.lerp(errorColor, other.errorColor, t)!,
baseGameColor: Color.lerp(baseGameColor, other.baseGameColor, t)!,
expansionColor: Color.lerp(expansionColor, other.expansionColor, t)!,
linkColor: Color.lerp(linkColor, other.linkColor, t)!,
loadAfterColor: Color.lerp(loadAfterColor, other.loadAfterColor, t)!,
loadBeforeColor: Color.lerp(loadBeforeColor, other.loadBeforeColor, t)!,
);
}
static AppThemeExtension light() {
return AppThemeExtension(
iconSizeSmall: 16,
iconSizeRegular: 24,
iconSizeLarge: 32,
textSizeSmall: 10,
textSizeRegular: 14,
textSizeLarge: 18,
paddingSmall: const EdgeInsets.all(4.0),
paddingRegular: const EdgeInsets.all(8.0),
paddingLarge: const EdgeInsets.all(16.0),
enabledModColor: Colors.green,
enabledModBackgroundColor: const Color.fromRGBO(0, 128, 0, 0.1),
errorBackgroundColor: Color.fromRGBO(
Colors.red.shade900.red,
Colors.red.shade900.green,
Colors.red.shade900.blue,
0.3,
),
warningColor: Colors.orange,
errorColor: Colors.red,
baseGameColor: Colors.blue,
expansionColor: Colors.yellow,
linkColor: Colors.orange,
loadAfterColor: Colors.blue,
loadBeforeColor: Colors.green,
);
}
static AppThemeExtension dark() {
return light(); // For now, we use the same values for both light and dark
}
}
double lerpDouble(double a, double b, double t) {
return a + (b - a) * t;
}
// Constants for file paths
final String root =
@@ -53,6 +223,7 @@ class RimWorldModManager extends StatelessWidget {
backgroundColor: Color(0xFF2A3440),
foregroundColor: Colors.white,
),
extensions: [AppThemeExtension.dark()],
),
home: const ModManagerHomePage(),
);
@@ -255,7 +426,7 @@ class _ModManagerPageState extends State<ModManagerPage> {
return Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
padding: AppThemeExtension.of(context).paddingRegular,
child: Row(
children: [
// Search field
@@ -336,15 +507,14 @@ class _ModManagerPageState extends State<ModManagerPage> {
_incompatibleMods.isNotEmpty ||
(_loadOrderErrors?.isNotEmpty ?? false))
Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
padding: const EdgeInsets.all(8.0),
margin: EdgeInsets.symmetric(
horizontal:
AppThemeExtension.of(context).paddingRegular.horizontal,
vertical: AppThemeExtension.of(context).paddingSmall.vertical,
),
padding: AppThemeExtension.of(context).paddingRegular,
decoration: BoxDecoration(
color: Color.fromRGBO(
Colors.red.shade900.red,
Colors.red.shade900.green,
Colors.red.shade900.blue,
0.3
),
color: AppThemeExtension.of(context).errorBackgroundColor,
borderRadius: BorderRadius.circular(4.0),
border: Border.all(color: Colors.red.shade800),
),
@@ -357,12 +527,18 @@ class _ModManagerPageState extends State<ModManagerPage> {
padding: const EdgeInsets.only(bottom: 4.0),
child: Row(
children: [
const Icon(Icons.loop, color: Colors.orange, size: 16),
Icon(
Icons.loop,
color: AppThemeExtension.of(context).warningColor,
size: AppThemeExtension.of(context).iconSizeSmall,
),
const SizedBox(width: 4),
Expanded(
child: Text(
'Dependency cycle detected: ${_cycleInfo!.join(" -> ")}',
style: const TextStyle(color: Colors.orange),
style: TextStyle(
color: AppThemeExtension.of(context).warningColor,
),
),
),
],
@@ -376,10 +552,10 @@ class _ModManagerPageState extends State<ModManagerPage> {
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(
Icon(
Icons.warning,
color: Colors.orange,
size: 16,
color: AppThemeExtension.of(context).warningColor,
size: AppThemeExtension.of(context).iconSizeSmall,
),
const SizedBox(width: 4),
Expanded(
@@ -388,7 +564,12 @@ class _ModManagerPageState extends State<ModManagerPage> {
children: [
Text(
'${_incompatibleMods.length} incompatible mod pairs:',
style: const TextStyle(color: Colors.orange),
style: TextStyle(
color:
AppThemeExtension.of(
context,
).warningColor,
),
),
if (_incompatibleMods.length <= 3)
...List.generate(_incompatibleMods.length, (
@@ -408,7 +589,10 @@ class _ModManagerPageState extends State<ModManagerPage> {
'$mod1$mod2',
style: TextStyle(
color: Colors.orange.shade300,
fontSize: 12,
fontSize:
AppThemeExtension.of(
context,
).textSizeSmall,
),
),
);
@@ -425,19 +609,21 @@ class _ModManagerPageState extends State<ModManagerPage> {
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(
Icon(
Icons.error_outline,
color: Colors.red,
size: 16,
color: AppThemeExtension.of(context).errorColor,
size: AppThemeExtension.of(context).iconSizeSmall,
),
const SizedBox(width: 4),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
Text(
'Dependency errors:',
style: TextStyle(color: Colors.red),
style: TextStyle(
color: AppThemeExtension.of(context).errorColor,
),
),
...List.generate(
_loadOrderErrors!.length > 5
@@ -452,7 +638,10 @@ class _ModManagerPageState extends State<ModManagerPage> {
'${_loadOrderErrors![index]}',
style: TextStyle(
color: Colors.red.shade300,
fontSize: 12,
fontSize:
AppThemeExtension.of(
context,
).textSizeSmall,
),
),
),
@@ -467,7 +656,10 @@ class _ModManagerPageState extends State<ModManagerPage> {
'(${_loadOrderErrors!.length - 5} more errors...)',
style: TextStyle(
color: Colors.red.shade300,
fontSize: 12,
fontSize:
AppThemeExtension.of(
context,
).textSizeSmall,
fontStyle: FontStyle.italic,
),
),
@@ -488,7 +680,7 @@ class _ModManagerPageState extends State<ModManagerPage> {
// LEFT PANEL - All available mods (alphabetical)
Expanded(
child: Card(
margin: const EdgeInsets.all(8.0),
margin: AppThemeExtension.of(context).paddingRegular,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
@@ -503,11 +695,12 @@ class _ModManagerPageState extends State<ModManagerPage> {
),
if (_searchQuery.isNotEmpty)
Padding(
padding: const EdgeInsets.all(4.0),
padding: AppThemeExtension.of(context).paddingSmall,
child: Text(
'Searching: "$_searchQuery"',
style: TextStyle(
fontSize: 12,
fontSize:
AppThemeExtension.of(context).textSizeSmall,
fontStyle: FontStyle.italic,
color: Colors.grey.shade400,
),
@@ -528,32 +721,54 @@ class _ModManagerPageState extends State<ModManagerPage> {
mod.name,
style: TextStyle(
color:
isActive ? Colors.green : Colors.white,
isActive
? AppThemeExtension.of(
context,
).enabledModColor
: Colors.white,
),
),
subtitle: Text(
'ID: ${mod.id}\nSize: ${mod.size} files',
style: Theme.of(context).textTheme.bodySmall,
),
tileColor:
isActive
? AppThemeExtension.of(
context,
).enabledModBackgroundColor
: null,
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (mod.isBaseGame)
const Tooltip(
Tooltip(
message: 'Base Game',
child: Icon(
Icons.home,
color: Colors.blue,
size: 24,
color:
AppThemeExtension.of(
context,
).baseGameColor,
size:
AppThemeExtension.of(
context,
).iconSizeRegular,
),
),
if (mod.isExpansion)
const Tooltip(
Tooltip(
message: 'Expansion',
child: Icon(
Icons.star,
color: Colors.yellow,
size: 24,
color:
AppThemeExtension.of(
context,
).expansionColor,
size:
AppThemeExtension.of(
context,
).iconSizeRegular,
),
),
const SizedBox(width: 4),
@@ -561,30 +776,48 @@ class _ModManagerPageState extends State<ModManagerPage> {
Tooltip(
message:
'Dependencies:\n${mod.dependencies.join('\n')}',
child: const Icon(
child: Icon(
Icons.link,
color: Colors.orange,
size: 24,
color:
AppThemeExtension.of(
context,
).linkColor,
size:
AppThemeExtension.of(
context,
).iconSizeRegular,
),
),
if (mod.loadAfter.isNotEmpty)
Tooltip(
message:
'Loads after:\n${mod.loadAfter.join('\n')}',
child: const Icon(
child: Icon(
Icons.arrow_downward,
color: Colors.blue,
size: 24,
color:
AppThemeExtension.of(
context,
).loadAfterColor,
size:
AppThemeExtension.of(
context,
).iconSizeRegular,
),
),
if (mod.loadBefore.isNotEmpty)
Tooltip(
message:
'Loads before:\n${mod.loadBefore.join('\n')}',
child: const Icon(
child: Icon(
Icons.arrow_upward,
color: Colors.green,
size: 24,
color:
AppThemeExtension.of(
context,
).loadBeforeColor,
size:
AppThemeExtension.of(
context,
).iconSizeRegular,
),
),
],
@@ -605,7 +838,7 @@ class _ModManagerPageState extends State<ModManagerPage> {
// RIGHT PANEL - Active mods (load order)
Expanded(
child: Card(
margin: const EdgeInsets.all(8.0),
margin: AppThemeExtension.of(context).paddingRegular,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
@@ -619,13 +852,14 @@ class _ModManagerPageState extends State<ModManagerPage> {
),
),
Padding(
padding: const EdgeInsets.all(8.0),
padding: AppThemeExtension.of(context).paddingRegular,
child: Text(
_searchQuery.isNotEmpty
? 'Searching: "$_searchQuery"'
: 'Larger mods are prioritized during auto-sorting.',
style: TextStyle(
fontSize: 12,
fontSize:
AppThemeExtension.of(context).textSizeSmall,
color: Colors.grey.shade400,
fontStyle: FontStyle.italic,
),
@@ -659,10 +893,10 @@ class _ModManagerPageState extends State<ModManagerPage> {
}
},
child: Card(
margin: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 4.0,
),
margin:
AppThemeExtension.of(
context,
).paddingRegular,
child: ListTile(
leading: SizedBox(
width: 50,
@@ -680,7 +914,12 @@ class _ModManagerPageState extends State<ModManagerPage> {
decoration: BoxDecoration(
color:
_searchQuery.isNotEmpty
? const Color.fromRGBO(0, 0, 255, 0.2)
? const Color.fromRGBO(
0,
0,
255,
0.2,
)
: null,
borderRadius:
BorderRadius.circular(4),
@@ -692,7 +931,10 @@ class _ModManagerPageState extends State<ModManagerPage> {
'$actualLoadOrderPosition',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
fontSize:
AppThemeExtension.of(
context,
).textSizeSmall,
color:
_searchQuery.isNotEmpty
? Colors.blue.shade300
@@ -712,8 +954,11 @@ class _ModManagerPageState extends State<ModManagerPage> {
'This mod contains ${mod.size} files.',
child: Text(
'${mod.size}',
style: const TextStyle(
fontSize: 10,
style: TextStyle(
fontSize:
AppThemeExtension.of(
context,
).textSizeSmall,
color: Colors.grey,
),
),
@@ -727,37 +972,60 @@ class _ModManagerPageState extends State<ModManagerPage> {
title: Text(mod.name),
subtitle: Text(
mod.id,
style: const TextStyle(fontSize: 12),
style: TextStyle(
fontSize:
AppThemeExtension.of(
context,
).textSizeSmall,
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (mod.isBaseGame)
const Tooltip(
Tooltip(
message: 'Base Game',
child: Icon(
Icons.home,
color: Colors.blue,
size: 24,
color:
AppThemeExtension.of(
context,
).baseGameColor,
size:
AppThemeExtension.of(
context,
).iconSizeRegular,
),
),
if (mod.isExpansion)
const Tooltip(
Tooltip(
message: 'Expansion',
child: Icon(
Icons.star,
color: Colors.yellow,
size: 24,
color:
AppThemeExtension.of(
context,
).expansionColor,
size:
AppThemeExtension.of(
context,
).iconSizeRegular,
),
),
if (mod.dependencies.isNotEmpty)
Tooltip(
message:
'Dependencies:\n${mod.dependencies.join('\n')}',
child: const Icon(
child: Icon(
Icons.link,
color: Colors.orange,
size: 24,
color:
AppThemeExtension.of(
context,
).linkColor,
size:
AppThemeExtension.of(
context,
).iconSizeRegular,
),
),
const SizedBox(width: 4),
@@ -765,10 +1033,16 @@ class _ModManagerPageState extends State<ModManagerPage> {
Tooltip(
message:
'Loads after other mods:\n${mod.loadAfter.join('\n')}',
child: const Icon(
child: Icon(
Icons.arrow_downward,
color: Colors.blue,
size: 24,
color:
AppThemeExtension.of(
context,
).loadAfterColor,
size:
AppThemeExtension.of(
context,
).iconSizeRegular,
),
),
const SizedBox(width: 4),
@@ -776,10 +1050,16 @@ class _ModManagerPageState extends State<ModManagerPage> {
Tooltip(
message:
'Loads before other mods:\n${mod.loadBefore.join('\n')}',
child: const Icon(
child: Icon(
Icons.arrow_upward,
color: Colors.green,
size: 24,
color:
AppThemeExtension.of(
context,
).loadBeforeColor,
size:
AppThemeExtension.of(
context,
).iconSizeRegular,
),
),
],
@@ -1272,34 +1552,6 @@ class TroubleshootingPage extends StatelessWidget {
@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'),
),
],
),
);
return const ModTroubleshooterWidget();
}
}

View File

@@ -240,18 +240,6 @@ class ModList {
}
}
//LoadOrder loadRequired([LoadOrder? loadOrder]) {
// loadOrder ??= LoadOrder();
// final toEnable = <String>[];
// for (final modid in activeMods.keys) {
// loadDependencies(modid, loadOrder, toEnable);
// }
// for (final modid in toEnable) {
// setEnabled(modid, true);
// }
// return generateLoadOrder(loadOrder);
//}
LoadOrder generateLoadOrder([LoadOrder? loadOrder]) {
loadOrder ??= LoadOrder();
final logger = Logger.instance;
@@ -574,6 +562,27 @@ class ModList {
}
return generateLoadOrder(loadOrder);
}
LoadOrder loadRequiredBaseGame([LoadOrder? loadOrder]) {
loadOrder ??= LoadOrder();
final baseGameMods =
mods.values.where((mod) => mod.isBaseGame || mod.isExpansion).toList();
// You would probably want to load these too if you had them
final specialMods =
mods.values
.where(
(mod) =>
mod.id.contains("harmony") ||
mod.id.contains("prepatcher") ||
mod.id.contains("betterlog"),
)
.toList();
enableMods(baseGameMods.map((mod) => mod.id).toList());
enableMods(specialMods.map((mod) => mod.id).toList());
return loadRequired(loadOrder);
}
}
String _expansionNameFromId(String id) {

View File

@@ -0,0 +1,831 @@
import 'package:flutter/material.dart';
import 'package:rimworld_modman/mod_list.dart';
import 'package:rimworld_modman/mod_list_troubleshooter.dart';
import 'main.dart';
/// A widget that provides a user interface for the mod troubleshooter functionality.
///
/// This allows users to:
/// - Toggle between binary and linear search modes
/// - Navigate forward and backward through mod sets
/// - Adjust step size for linear navigation
/// - Mark mods as checked/good or problematic
/// - Find specific mods causing issues in their load order
class ModTroubleshooterWidget extends StatefulWidget {
const ModTroubleshooterWidget({super.key});
@override
State<ModTroubleshooterWidget> createState() =>
_ModTroubleshooterWidgetState();
}
class _ModTroubleshooterWidgetState extends State<ModTroubleshooterWidget> {
late ModListTroubleshooter _troubleshooter;
bool _isInitialized = false;
bool _isBinaryMode = false;
int _stepSize = 10;
// Set of mod IDs that have been checked and confirmed to be good
final Set<String> _checkedMods = {};
// Set of mod IDs that are suspected to cause issues
final Set<String> _problemMods = {};
// The currently selected mod IDs (for highlighting)
List<String> _selectedMods = [];
// The next potential set of mods (from move calculation)
Move? _nextForwardMove;
Move? _nextBackwardMove;
// Controller for step size input
late TextEditingController _stepSizeController;
@override
void initState() {
super.initState();
_stepSizeController = TextEditingController(text: _stepSize.toString());
}
@override
void dispose() {
_stepSizeController.dispose();
super.dispose();
}
void _initialize() {
if (_isInitialized) return;
// Initialize the troubleshooter with the global mod manager
_troubleshooter = ModListTroubleshooter(modManager);
// Set initial active mods for highlighting
if (modManager.activeMods.isNotEmpty) {
// Initially select all active mods
_selectedMods = List.from(modManager.activeMods.keys);
}
// Calculate initial moves
_updateNextMoves();
setState(() {
_isInitialized = true;
});
}
void _updateNextMoves() {
if (_isBinaryMode) {
_nextForwardMove = _troubleshooter.binaryForwardMove();
_nextBackwardMove = _troubleshooter.binaryBackwardMove();
} else {
_nextForwardMove = _troubleshooter.linearForwardMove(stepSize: _stepSize);
_nextBackwardMove = _troubleshooter.linearBackwardMove(
stepSize: _stepSize,
);
}
}
void _navigateForward() {
ModList result;
if (_isBinaryMode) {
result = _troubleshooter.binaryForward();
} else {
result = _troubleshooter.linearForward(stepSize: _stepSize);
}
// Load all required dependencies for the selected mods
final loadOrder = result.loadRequiredBaseGame();
// Use the mods from the load order result
setState(() {
_selectedMods = loadOrder.loadOrder;
_updateNextMoves();
});
}
void _navigateBackward() {
ModList result;
if (_isBinaryMode) {
result = _troubleshooter.binaryBackward();
} else {
result = _troubleshooter.linearBackward(stepSize: _stepSize);
}
// Load all required dependencies for the selected mods
final loadOrder = result.loadRequiredBaseGame();
// Use the mods from the load order result
setState(() {
_selectedMods = loadOrder.loadOrder;
_updateNextMoves();
});
}
void _markAsGood(String modId) {
setState(() {
_checkedMods.add(modId);
_problemMods.remove(modId);
});
}
void _markAsProblem(String modId) {
setState(() {
_problemMods.add(modId);
_checkedMods.remove(modId);
});
}
void _clearMarks(String modId) {
setState(() {
_checkedMods.remove(modId);
_problemMods.remove(modId);
});
}
void _resetTroubleshooter() {
setState(() {
_checkedMods.clear();
_problemMods.clear();
_isInitialized = false;
});
_initialize();
}
void _saveTroubleshootingConfig() {
// Only save if we have a valid selection
if (_selectedMods.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('No mods selected to save'),
duration: Duration(seconds: 2),
),
);
return;
}
// First disable all mods
modManager.disableAll();
// Then enable only the selected mods
modManager.enableMods(_selectedMods);
// Save the configuration (we don't have direct access to save method, so show a message)
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'${_selectedMods.length} mods prepared for testing. Please use Save button in the Mods tab to save config.',
),
backgroundColor: Colors.orange,
duration: const Duration(seconds: 4),
),
);
}
void _markSelectedAsGood() {
if (_selectedMods.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('No mods selected to mark'),
duration: Duration(seconds: 2),
),
);
return;
}
setState(() {
for (final modId in _selectedMods) {
_checkedMods.add(modId);
_problemMods.remove(modId);
}
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Marked ${_selectedMods.length} mods as good'),
backgroundColor: Colors.green,
duration: const Duration(seconds: 2),
),
);
}
void _markSelectedAsProblem() {
if (_selectedMods.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('No mods selected to mark'),
duration: Duration(seconds: 2),
),
);
return;
}
setState(() {
for (final modId in _selectedMods) {
_problemMods.add(modId);
_checkedMods.remove(modId);
}
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Marked ${_selectedMods.length} mods as problematic'),
backgroundColor: Colors.orange,
duration: const Duration(seconds: 2),
),
);
}
@override
Widget build(BuildContext context) {
// Make sure we're initialized
if (!_isInitialized) {
_initialize();
}
if (!_isInitialized || modManager.mods.isEmpty) {
return _buildEmptyState();
}
return Column(
children: [_buildControlPanel(), Expanded(child: _buildModList())],
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.build,
size: AppThemeExtension.of(context).iconSizeLarge * 2,
),
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(
'Load mods first to use the troubleshooting tools.',
style: Theme.of(context).textTheme.bodyLarge,
textAlign: TextAlign.center,
),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
// No direct access to the ModManagerHomePage state, so just show a message
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please go to the Mods tab to load mods first'),
duration: Duration(seconds: 3),
),
);
},
child: const Text('Go to Mod Manager'),
),
],
),
);
}
Widget _buildControlPanel() {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
child: Padding(
padding: AppThemeExtension.of(context).paddingSmall,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
// Compact instruction
Expanded(
child: Text(
_selectedMods.isNotEmpty
? 'Testing ${_selectedMods.length} mods. Tap highlighted mods to navigate. Mark results below:'
: 'Click highlighted mods to begin testing. Blue→forward, purple←backward.',
style: TextStyle(
fontSize: AppThemeExtension.of(context).textSizeRegular,
fontStyle: FontStyle.italic,
),
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
// Binary/Linear mode toggle
Text('Mode:', style: Theme.of(context).textTheme.bodyMedium),
const SizedBox(width: 8),
ToggleButtons(
isSelected: [!_isBinaryMode, _isBinaryMode],
onPressed: (index) {
setState(() {
_isBinaryMode = index == 1;
_updateNextMoves();
});
},
children: const [
Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: Text('Linear'),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: Text('Binary'),
),
],
),
// Step size input field (only for linear mode)
if (!_isBinaryMode) ...[
const SizedBox(width: 16),
Text('Step:', style: Theme.of(context).textTheme.bodyMedium),
const SizedBox(width: 4),
SizedBox(
width: 60,
child: TextField(
keyboardType: TextInputType.number,
decoration: const InputDecoration(
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(
horizontal: 6,
vertical: 6,
),
),
controller: _stepSizeController,
onChanged: (value) {
final parsedValue = int.tryParse(value);
if (parsedValue != null && parsedValue > 0) {
setState(() {
_stepSize = parsedValue;
_updateNextMoves();
});
}
},
),
),
],
const Spacer(),
// Buttons to mark selected mods
if (_selectedMods.isNotEmpty) ...[
OutlinedButton.icon(
icon: Icon(
Icons.error,
color: Colors.red.shade300,
size: 16,
),
label: const Text('Problem'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 0,
),
),
onPressed: _markSelectedAsProblem,
),
const SizedBox(width: 4),
OutlinedButton.icon(
icon: Icon(
Icons.check_circle,
color: Colors.green.shade300,
size: 16,
),
label: const Text('Good'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 0,
),
),
onPressed: _markSelectedAsGood,
),
const SizedBox(width: 4),
],
// Reset button
OutlinedButton.icon(
icon: const Icon(Icons.refresh, size: 16),
label: const Text('Reset'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 0,
),
),
onPressed: _resetTroubleshooter,
),
if (_selectedMods.isNotEmpty) ...[
const SizedBox(width: 4),
// Save config button
OutlinedButton.icon(
icon: const Icon(Icons.save, size: 16),
label: const Text('Save'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 0,
),
),
onPressed: _saveTroubleshootingConfig,
),
],
],
),
],
),
),
);
}
Widget _buildModList() {
// Get the original mod order from mod manager
final fullModList = modManager.activeMods.keys.toList();
return Card(
margin: AppThemeExtension.of(context).paddingRegular,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
color: Theme.of(context).primaryColor,
padding: AppThemeExtension.of(context).paddingRegular,
child: Column(
children: [
Text(
'Active Mods (${fullModList.length})',
style: const TextStyle(fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
if (_nextForwardMove != null || _nextBackwardMove != null)
Text(
'Click ↓blue areas to move forward, ↑purple to move backward',
style: TextStyle(
fontSize: AppThemeExtension.of(context).textSizeSmall,
fontStyle: FontStyle.italic,
color: Colors.grey.shade300,
),
textAlign: TextAlign.center,
),
],
),
),
Expanded(
child: ListView.builder(
itemCount: fullModList.length,
itemBuilder: (context, index) {
final modId = fullModList[index];
final mod = modManager.mods[modId];
if (mod == null) return const SizedBox.shrink();
// Determine if this mod is in the selection range for highlighted navigation
final bool isSelected = _selectedMods.contains(modId);
// Check if this mod would be included in the next Forward/Backward move
bool isInNextForward = false;
bool isInNextBackward = false;
if (_nextForwardMove != null &&
index >= _nextForwardMove!.startIndex &&
index < _nextForwardMove!.endIndex) {
isInNextForward = true;
}
if (_nextBackwardMove != null &&
index >= _nextBackwardMove!.startIndex &&
index < _nextBackwardMove!.endIndex) {
isInNextBackward = true;
}
// Determine mod status for coloring
final bool isChecked = _checkedMods.contains(modId);
final bool isProblem = _problemMods.contains(modId);
return GestureDetector(
onTap: () {
// Navigation takes precedence if this mod is in a navigation range
if (isInNextForward) {
_navigateForward();
} else if (isInNextBackward) {
_navigateBackward();
}
// Otherwise toggle the status of this mod
else if (isChecked) {
_markAsProblem(modId);
} else if (isProblem) {
_clearMarks(modId);
} else {
_markAsGood(modId);
}
},
child: Card(
margin: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 4.0,
),
color: _getModCardColor(
isSelected: isSelected,
isChecked: isChecked,
isProblem: isProblem,
isInNextForward: isInNextForward,
isInNextBackward: isInNextBackward,
),
child: ListTile(
leading: Text(
'${index + 1}',
style: TextStyle(
fontWeight: FontWeight.bold,
color: isSelected ? Colors.white : Colors.grey,
),
),
title: Row(
children: [
if (isSelected)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 0,
),
margin: const EdgeInsets.only(right: 4),
decoration: BoxDecoration(
color: const Color(
0x28303F9F,
), // Blue with alpha 40
borderRadius: BorderRadius.circular(4),
),
child: Text(
'TESTING',
style: TextStyle(
color: Colors.blue.shade200,
fontSize:
AppThemeExtension.of(
context,
).textSizeSmall,
fontWeight: FontWeight.bold,
),
),
),
if (isChecked)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 0,
),
margin: const EdgeInsets.only(right: 4),
decoration: BoxDecoration(
color: const Color(
0x1E2E7D32,
), // Green with alpha 30
borderRadius: BorderRadius.circular(4),
),
child: Text(
'GOOD',
style: TextStyle(
color: Colors.green.shade200,
fontSize:
AppThemeExtension.of(
context,
).textSizeSmall,
fontWeight: FontWeight.bold,
),
),
),
if (isProblem)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 0,
),
margin: const EdgeInsets.only(right: 4),
decoration: BoxDecoration(
color: const Color(
0x1EC62828,
), // Red with alpha 30
borderRadius: BorderRadius.circular(4),
),
child: Text(
'PROBLEM',
style: TextStyle(
color: Colors.red.shade200,
fontSize:
AppThemeExtension.of(
context,
).textSizeSmall,
fontWeight: FontWeight.bold,
),
),
),
Expanded(
child: Text(
mod.name,
style: TextStyle(
fontWeight:
isSelected
? FontWeight.bold
: FontWeight.normal,
),
),
),
],
),
subtitle: Text(
modId,
style: TextStyle(
fontSize: AppThemeExtension.of(context).textSizeSmall,
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
// Display mod characteristics
if (mod.isBaseGame)
Tooltip(
message: 'Base Game',
child: Icon(
Icons.home,
color:
AppThemeExtension.of(context).baseGameColor,
size:
AppThemeExtension.of(context).iconSizeSmall,
),
),
if (mod.isExpansion)
Tooltip(
message: 'Expansion',
child: Icon(
Icons.star,
color:
AppThemeExtension.of(
context,
).expansionColor,
size:
AppThemeExtension.of(context).iconSizeSmall,
),
),
if (mod.dependencies.isNotEmpty)
Tooltip(
message:
'Dependencies:\n${mod.dependencies.join('\n')}',
child: Icon(
Icons.link,
color: AppThemeExtension.of(context).linkColor,
size:
AppThemeExtension.of(context).iconSizeSmall,
),
),
// Display status icon
if (isChecked)
Tooltip(
message: 'Marked as working correctly',
child: Icon(
Icons.check_circle,
color: Colors.green.shade300,
),
)
else if (isProblem)
Tooltip(
message: 'Marked as problematic',
child: Icon(
Icons.error,
color: Colors.red.shade300,
),
),
const SizedBox(width: 4),
// Show navigation indicators
if (isInNextForward)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 2,
),
decoration: BoxDecoration(
color: const Color(
0x0A2196F3,
), // Blue with alpha 10
borderRadius: BorderRadius.circular(4),
),
child: Tooltip(
message:
'Click to move Forward (test this mod)',
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.arrow_forward,
color: Colors.blue.shade300,
size:
AppThemeExtension.of(
context,
).iconSizeSmall,
),
const SizedBox(width: 2),
Text(
'Forward',
style: TextStyle(
color: Colors.blue.shade300,
fontSize:
AppThemeExtension.of(
context,
).textSizeSmall,
),
),
],
),
),
),
if (isInNextBackward)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 2,
),
decoration: BoxDecoration(
color: const Color(
0x0A9C27B0,
), // Purple with alpha 10
borderRadius: BorderRadius.circular(4),
),
child: Tooltip(
message:
'Click to move Backward (test this mod)',
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.arrow_back,
color: Colors.purple.shade300,
size:
AppThemeExtension.of(
context,
).iconSizeSmall,
),
const SizedBox(width: 2),
Text(
'Back',
style: TextStyle(
color: Colors.purple.shade300,
fontSize:
AppThemeExtension.of(
context,
).textSizeSmall,
),
),
],
),
),
),
],
),
),
),
);
},
),
),
],
),
);
}
Color _getModCardColor({
required bool isSelected,
required bool isChecked,
required bool isProblem,
required bool isInNextForward,
required bool isInNextBackward,
}) {
// Priority: 1. Selected, 2. Navigation areas, 3. Status
if (isSelected) {
return const Color(0x80303F9F);
} else if (isInNextForward && isInNextBackward) {
return const Color(0x50673AB7);
} else if (isInNextForward) {
return const Color(0x402196F3);
} else if (isInNextBackward) {
return const Color(0x409C27B0);
} else if (isChecked) {
return const Color(0x802E7D32);
} else if (isProblem) {
return const Color(0x80C62828);
}
return Colors.transparent;
}
}