Compare commits
4 Commits
02cfe01ae0
...
71ad392fb6
Author | SHA1 | Date | |
---|---|---|---|
71ad392fb6 | |||
a4ee202971 | |||
a37b67873e | |||
164e95fa54 |
448
lib/main.dart
448
lib/main.dart
@@ -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();
|
||||
}
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
831
lib/mod_troubleshooter_widget.dart
Normal file
831
lib/mod_troubleshooter_widget.dart
Normal 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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user