From 164e95fa54f8a7bf80ef7528ebf0e4d035cfffee Mon Sep 17 00:00:00 2001 From: PhatPhuckDave Date: Tue, 18 Mar 2025 21:40:34 +0100 Subject: [PATCH] Refactor a large part of main into theme --- lib/main.dart | 417 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 348 insertions(+), 69 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index d1f4555..f1194f2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,175 @@ import 'package:rimworld_modman/logger.dart'; import 'package:rimworld_modman/mod.dart'; import 'package:rimworld_modman/mod_list.dart'; +// Theme extension to store app-specific constants +class AppThemeExtension extends ThemeExtension { + 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()!; + } + + @override + ThemeExtension 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 lerp( + covariant ThemeExtension? 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 = Platform.isWindows @@ -53,6 +222,7 @@ class RimWorldModManager extends StatelessWidget { backgroundColor: Color(0xFF2A3440), foregroundColor: Colors.white, ), + extensions: [AppThemeExtension.dark()], ), home: const ModManagerHomePage(), ); @@ -255,7 +425,7 @@ class _ModManagerPageState extends State { return Column( children: [ Padding( - padding: const EdgeInsets.all(8.0), + padding: AppThemeExtension.of(context).paddingRegular, child: Row( children: [ // Search field @@ -336,15 +506,14 @@ class _ModManagerPageState extends State { _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 +526,18 @@ class _ModManagerPageState extends State { 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 +551,10 @@ class _ModManagerPageState extends State { 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 +563,12 @@ class _ModManagerPageState extends State { 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 +588,10 @@ class _ModManagerPageState extends State { '• $mod1 ↔ $mod2', style: TextStyle( color: Colors.orange.shade300, - fontSize: 12, + fontSize: + AppThemeExtension.of( + context, + ).textSizeSmall, ), ), ); @@ -425,19 +608,21 @@ class _ModManagerPageState extends State { 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 +637,10 @@ class _ModManagerPageState extends State { '• ${_loadOrderErrors![index]}', style: TextStyle( color: Colors.red.shade300, - fontSize: 12, + fontSize: + AppThemeExtension.of( + context, + ).textSizeSmall, ), ), ), @@ -467,7 +655,10 @@ class _ModManagerPageState extends State { '(${_loadOrderErrors!.length - 5} more errors...)', style: TextStyle( color: Colors.red.shade300, - fontSize: 12, + fontSize: + AppThemeExtension.of( + context, + ).textSizeSmall, fontStyle: FontStyle.italic, ), ), @@ -488,7 +679,7 @@ class _ModManagerPageState extends State { // 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 +694,12 @@ class _ModManagerPageState extends State { ), 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 +720,54 @@ class _ModManagerPageState extends State { 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 +775,48 @@ class _ModManagerPageState extends State { 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 +837,7 @@ class _ModManagerPageState extends State { // 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 +851,14 @@ class _ModManagerPageState extends State { ), ), 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 +892,10 @@ class _ModManagerPageState extends State { } }, 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 +913,12 @@ class _ModManagerPageState extends State { 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 +930,10 @@ class _ModManagerPageState extends State { '$actualLoadOrderPosition', style: TextStyle( fontWeight: FontWeight.bold, - fontSize: 12, + fontSize: + AppThemeExtension.of( + context, + ).textSizeSmall, color: _searchQuery.isNotEmpty ? Colors.blue.shade300 @@ -712,8 +953,11 @@ class _ModManagerPageState extends State { '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 +971,60 @@ class _ModManagerPageState extends State { 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 +1032,16 @@ class _ModManagerPageState extends State { 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 +1049,16 @@ class _ModManagerPageState extends State { 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, ), ), ],