Compare commits
	
		
			21 Commits
		
	
	
		
			02cfe01ae0
			...
			v1.0.0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 07d81eca71 | |||
| 2e6bfb84de | |||
| 0384e8012e | |||
| 1bb8ed9084 | |||
| 573ad05514 | |||
| 9a8b7fd2d3 | |||
| d00c20397f | |||
| 40d251f400 | |||
| 09b7fe539e | |||
| 5f20368fe2 | |||
| 9eb71e94c1 | |||
| f90371109c | |||
| 7f4b944101 | |||
| 8f466420f2 | |||
| 160488849f | |||
| 6826b272aa | |||
| 1c6af27c7e | |||
| 71ad392fb6 | |||
| a4ee202971 | |||
| a37b67873e | |||
| 164e95fa54 | 
							
								
								
									
										345
									
								
								lib/components/html_tooltip.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										345
									
								
								lib/components/html_tooltip.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,345 @@
 | 
				
			|||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:flutter_html/flutter_html.dart';
 | 
				
			||||||
 | 
					import 'package:url_launcher/url_launcher.dart';
 | 
				
			||||||
 | 
					import '../format_converter.dart';
 | 
				
			||||||
 | 
					import 'dart:math' as math;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class HtmlTooltip extends StatefulWidget {
 | 
				
			||||||
 | 
					  final Widget child;
 | 
				
			||||||
 | 
					  final String content;
 | 
				
			||||||
 | 
					  final double maxWidth;
 | 
				
			||||||
 | 
					  final double maxHeight;
 | 
				
			||||||
 | 
					  final EdgeInsets padding;
 | 
				
			||||||
 | 
					  final Duration showDuration;
 | 
				
			||||||
 | 
					  final Duration fadeDuration;
 | 
				
			||||||
 | 
					  final Color backgroundColor;
 | 
				
			||||||
 | 
					  final Color textColor;
 | 
				
			||||||
 | 
					  final BorderRadius borderRadius;
 | 
				
			||||||
 | 
					  final bool preferBelow;
 | 
				
			||||||
 | 
					  final String? title;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const HtmlTooltip({
 | 
				
			||||||
 | 
					    super.key,
 | 
				
			||||||
 | 
					    required this.child,
 | 
				
			||||||
 | 
					    required this.content,
 | 
				
			||||||
 | 
					    this.maxWidth = 800.0,
 | 
				
			||||||
 | 
					    this.maxHeight = 800.0,
 | 
				
			||||||
 | 
					    this.padding = const EdgeInsets.all(8.0),
 | 
				
			||||||
 | 
					    this.showDuration = const Duration(milliseconds: 0),
 | 
				
			||||||
 | 
					    this.fadeDuration = const Duration(milliseconds: 200),
 | 
				
			||||||
 | 
					    this.backgroundColor = const Color(0xFF232323),
 | 
				
			||||||
 | 
					    this.textColor = Colors.white,
 | 
				
			||||||
 | 
					    this.borderRadius = const BorderRadius.all(Radius.circular(4.0)),
 | 
				
			||||||
 | 
					    this.preferBelow = true,
 | 
				
			||||||
 | 
					    this.title = 'Mod Description',
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  State<HtmlTooltip> createState() => _HtmlTooltipState();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _HtmlTooltipState extends State<HtmlTooltip> {
 | 
				
			||||||
 | 
					  final LayerLink _layerLink = LayerLink();
 | 
				
			||||||
 | 
					  OverlayEntry? _overlayEntry;
 | 
				
			||||||
 | 
					  bool _isTooltipVisible = false;
 | 
				
			||||||
 | 
					  bool _isMouseInside = false;
 | 
				
			||||||
 | 
					  bool _isMouseInsideTooltip = false;
 | 
				
			||||||
 | 
					  final ScrollController _scrollController = ScrollController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void dispose() {
 | 
				
			||||||
 | 
					    _scrollController.dispose();
 | 
				
			||||||
 | 
					    _hideTooltip();
 | 
				
			||||||
 | 
					    super.dispose();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Launch a URL
 | 
				
			||||||
 | 
					  Future<void> _launchUrl(String? urlString) async {
 | 
				
			||||||
 | 
					    if (urlString == null || urlString.isEmpty) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final Uri url = Uri.parse(urlString);
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      if (await canLaunchUrl(url)) {
 | 
				
			||||||
 | 
					        await launchUrl(url, mode: LaunchMode.externalApplication);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      debugPrint('Error launching URL: $e');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _showTooltip(BuildContext context) {
 | 
				
			||||||
 | 
					    if (_overlayEntry != null) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Get render box of the trigger widget
 | 
				
			||||||
 | 
					    final RenderBox box = context.findRenderObject() as RenderBox;
 | 
				
			||||||
 | 
					    final Size childSize = box.size;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Use the specified maxWidth without adjusting based on content length
 | 
				
			||||||
 | 
					    final double tooltipWidth = widget.maxWidth;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _overlayEntry = OverlayEntry(
 | 
				
			||||||
 | 
					      builder: (context) {
 | 
				
			||||||
 | 
					        return Positioned(
 | 
				
			||||||
 | 
					          width: tooltipWidth,
 | 
				
			||||||
 | 
					          child: CompositedTransformFollower(
 | 
				
			||||||
 | 
					            link: _layerLink,
 | 
				
			||||||
 | 
					            showWhenUnlinked: false,
 | 
				
			||||||
 | 
					            offset: Offset(
 | 
				
			||||||
 | 
					              (childSize.width / 2) - (tooltipWidth / 2),
 | 
				
			||||||
 | 
					              widget.preferBelow ? childSize.height + 5 : -5,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            child: Material(
 | 
				
			||||||
 | 
					              color: Colors.transparent,
 | 
				
			||||||
 | 
					              child: MouseRegion(
 | 
				
			||||||
 | 
					                onEnter: (_) {
 | 
				
			||||||
 | 
					                  setState(() {
 | 
				
			||||||
 | 
					                    _isMouseInsideTooltip = true;
 | 
				
			||||||
 | 
					                  });
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                onExit: (_) {
 | 
				
			||||||
 | 
					                  setState(() {
 | 
				
			||||||
 | 
					                    _isMouseInsideTooltip = false;
 | 
				
			||||||
 | 
					                    // Slight delay to prevent flickering
 | 
				
			||||||
 | 
					                    Future.delayed(const Duration(milliseconds: 50), () {
 | 
				
			||||||
 | 
					                      if (!_isMouseInside && !_isMouseInsideTooltip) {
 | 
				
			||||||
 | 
					                        _hideTooltip();
 | 
				
			||||||
 | 
					                      }
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                  });
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                child: FadeTransition(
 | 
				
			||||||
 | 
					                  opacity: const AlwaysStoppedAnimation(1.0),
 | 
				
			||||||
 | 
					                  child: Container(
 | 
				
			||||||
 | 
					                    constraints: BoxConstraints(
 | 
				
			||||||
 | 
					                      maxWidth: tooltipWidth,
 | 
				
			||||||
 | 
					                      maxHeight: widget.maxHeight,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    decoration: BoxDecoration(
 | 
				
			||||||
 | 
					                      color: widget.backgroundColor,
 | 
				
			||||||
 | 
					                      borderRadius: widget.borderRadius,
 | 
				
			||||||
 | 
					                      boxShadow: [
 | 
				
			||||||
 | 
					                        BoxShadow(
 | 
				
			||||||
 | 
					                          color: Colors.black.withAlpha(
 | 
				
			||||||
 | 
					                            77,
 | 
				
			||||||
 | 
					                          ), // Equivalent to 0.3 opacity
 | 
				
			||||||
 | 
					                          blurRadius: 10.0,
 | 
				
			||||||
 | 
					                          spreadRadius: 0.0,
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ],
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    child: ClipRRect(
 | 
				
			||||||
 | 
					                      borderRadius: widget.borderRadius,
 | 
				
			||||||
 | 
					                      child: Column(
 | 
				
			||||||
 | 
					                        mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
 | 
					                        crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					                        children: [
 | 
				
			||||||
 | 
					                          // Header
 | 
				
			||||||
 | 
					                          Container(
 | 
				
			||||||
 | 
					                            color: const Color(0xFF3D4A59),
 | 
				
			||||||
 | 
					                            padding: const EdgeInsets.symmetric(
 | 
				
			||||||
 | 
					                              horizontal: 16.0,
 | 
				
			||||||
 | 
					                              vertical: 8.0,
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                            child: Row(
 | 
				
			||||||
 | 
					                              mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
				
			||||||
 | 
					                              children: [
 | 
				
			||||||
 | 
					                                Text(
 | 
				
			||||||
 | 
					                                  widget.title ?? 'Description',
 | 
				
			||||||
 | 
					                                  style: const TextStyle(
 | 
				
			||||||
 | 
					                                    color: Colors.white,
 | 
				
			||||||
 | 
					                                    fontWeight: FontWeight.bold,
 | 
				
			||||||
 | 
					                                    fontSize: 14.0,
 | 
				
			||||||
 | 
					                                  ),
 | 
				
			||||||
 | 
					                                ),
 | 
				
			||||||
 | 
					                                Row(
 | 
				
			||||||
 | 
					                                  mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
 | 
					                                  children: [
 | 
				
			||||||
 | 
					                                    // Scroll to top button
 | 
				
			||||||
 | 
					                                    InkWell(
 | 
				
			||||||
 | 
					                                      onTap: () {
 | 
				
			||||||
 | 
					                                        if (_scrollController.hasClients) {
 | 
				
			||||||
 | 
					                                          _scrollController.animateTo(
 | 
				
			||||||
 | 
					                                            0.0,
 | 
				
			||||||
 | 
					                                            duration: const Duration(
 | 
				
			||||||
 | 
					                                              milliseconds: 300,
 | 
				
			||||||
 | 
					                                            ),
 | 
				
			||||||
 | 
					                                            curve: Curves.easeOut,
 | 
				
			||||||
 | 
					                                          );
 | 
				
			||||||
 | 
					                                        }
 | 
				
			||||||
 | 
					                                      },
 | 
				
			||||||
 | 
					                                      child: const Padding(
 | 
				
			||||||
 | 
					                                        padding: EdgeInsets.all(2.0),
 | 
				
			||||||
 | 
					                                        child: Icon(
 | 
				
			||||||
 | 
					                                          Icons.arrow_upward,
 | 
				
			||||||
 | 
					                                          color: Colors.white,
 | 
				
			||||||
 | 
					                                          size: 16.0,
 | 
				
			||||||
 | 
					                                        ),
 | 
				
			||||||
 | 
					                                      ),
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
 | 
					                                    const SizedBox(width: 4.0),
 | 
				
			||||||
 | 
					                                    // Close button
 | 
				
			||||||
 | 
					                                    InkWell(
 | 
				
			||||||
 | 
					                                      onTap: () {
 | 
				
			||||||
 | 
					                                        _hideTooltip();
 | 
				
			||||||
 | 
					                                      },
 | 
				
			||||||
 | 
					                                      child: const Padding(
 | 
				
			||||||
 | 
					                                        padding: EdgeInsets.all(2.0),
 | 
				
			||||||
 | 
					                                        child: Icon(
 | 
				
			||||||
 | 
					                                          Icons.close,
 | 
				
			||||||
 | 
					                                          color: Colors.white,
 | 
				
			||||||
 | 
					                                          size: 16.0,
 | 
				
			||||||
 | 
					                                        ),
 | 
				
			||||||
 | 
					                                      ),
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
 | 
					                                  ],
 | 
				
			||||||
 | 
					                                ),
 | 
				
			||||||
 | 
					                              ],
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                          // Content
 | 
				
			||||||
 | 
					                          Flexible(
 | 
				
			||||||
 | 
					                            child: SingleChildScrollView(
 | 
				
			||||||
 | 
					                              controller: _scrollController,
 | 
				
			||||||
 | 
					                              child: Padding(
 | 
				
			||||||
 | 
					                                padding: widget.padding,
 | 
				
			||||||
 | 
					                                child: Html(
 | 
				
			||||||
 | 
					                                  data: FormatConverter.toHtml(widget.content),
 | 
				
			||||||
 | 
					                                  style: {
 | 
				
			||||||
 | 
					                                    "body": Style(
 | 
				
			||||||
 | 
					                                      color: widget.textColor,
 | 
				
			||||||
 | 
					                                      margin: Margins.zero,
 | 
				
			||||||
 | 
					                                      padding: HtmlPaddings.zero,
 | 
				
			||||||
 | 
					                                      fontSize: FontSize(14.0),
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
 | 
					                                    "a": Style(
 | 
				
			||||||
 | 
					                                      color: Colors.lightBlue,
 | 
				
			||||||
 | 
					                                      textDecoration: TextDecoration.underline,
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
 | 
					                                    "blockquote": Style(
 | 
				
			||||||
 | 
					                                      backgroundColor: Colors.grey.withAlpha(
 | 
				
			||||||
 | 
					                                        26,
 | 
				
			||||||
 | 
					                                      ), // Approx 0.1 opacity
 | 
				
			||||||
 | 
					                                      border: Border(
 | 
				
			||||||
 | 
					                                        left: BorderSide(
 | 
				
			||||||
 | 
					                                          color: Colors.grey.withAlpha(
 | 
				
			||||||
 | 
					                                            128,
 | 
				
			||||||
 | 
					                                          ), // Approx 0.5 opacity
 | 
				
			||||||
 | 
					                                          width: 4.0,
 | 
				
			||||||
 | 
					                                        ),
 | 
				
			||||||
 | 
					                                      ),
 | 
				
			||||||
 | 
					                                      padding: HtmlPaddings.all(8.0),
 | 
				
			||||||
 | 
					                                      margin: Margins.only(left: 0, right: 0),
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
 | 
					                                    "code": Style(
 | 
				
			||||||
 | 
					                                      backgroundColor: Colors.grey.withAlpha(
 | 
				
			||||||
 | 
					                                        51,
 | 
				
			||||||
 | 
					                                      ), // Approx 0.2 opacity
 | 
				
			||||||
 | 
					                                      padding: HtmlPaddings.all(2.0),
 | 
				
			||||||
 | 
					                                      fontFamily: 'monospace',
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
 | 
					                                    "pre": Style(
 | 
				
			||||||
 | 
					                                      backgroundColor: Colors.grey.withAlpha(
 | 
				
			||||||
 | 
					                                        51,
 | 
				
			||||||
 | 
					                                      ), // Approx 0.2 opacity
 | 
				
			||||||
 | 
					                                      padding: HtmlPaddings.all(8.0),
 | 
				
			||||||
 | 
					                                      fontFamily: 'monospace',
 | 
				
			||||||
 | 
					                                      margin: Margins.only(
 | 
				
			||||||
 | 
					                                        bottom: 8.0,
 | 
				
			||||||
 | 
					                                        top: 8.0,
 | 
				
			||||||
 | 
					                                      ),
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
 | 
					                                    "table": Style(
 | 
				
			||||||
 | 
					                                      border: Border.all(color: Colors.grey),
 | 
				
			||||||
 | 
					                                      backgroundColor: Colors.transparent,
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
 | 
					                                    "td": Style(
 | 
				
			||||||
 | 
					                                      border: Border.all(
 | 
				
			||||||
 | 
					                                        color: Colors.grey.withAlpha(128),
 | 
				
			||||||
 | 
					                                      ), // Approx 0.5 opacity
 | 
				
			||||||
 | 
					                                      padding: HtmlPaddings.all(4.0),
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
 | 
					                                    "th": Style(
 | 
				
			||||||
 | 
					                                      border: Border.all(
 | 
				
			||||||
 | 
					                                        color: Colors.grey.withAlpha(128),
 | 
				
			||||||
 | 
					                                      ), // Approx 0.5 opacity
 | 
				
			||||||
 | 
					                                      padding: HtmlPaddings.all(4.0),
 | 
				
			||||||
 | 
					                                      backgroundColor: Colors.grey.withAlpha(
 | 
				
			||||||
 | 
					                                        51,
 | 
				
			||||||
 | 
					                                      ), // Approx 0.2 opacity
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
 | 
					                                  },
 | 
				
			||||||
 | 
					                                  onAnchorTap: (url, _, __) {
 | 
				
			||||||
 | 
					                                    _launchUrl(url);
 | 
				
			||||||
 | 
					                                  },
 | 
				
			||||||
 | 
					                                ),
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Use a check for mounted before inserting the overlay
 | 
				
			||||||
 | 
					    if (mounted) {
 | 
				
			||||||
 | 
					      Overlay.of(context).insert(_overlayEntry!);
 | 
				
			||||||
 | 
					      _isTooltipVisible = true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _hideTooltip() {
 | 
				
			||||||
 | 
					    _overlayEntry?.remove();
 | 
				
			||||||
 | 
					    _overlayEntry = null;
 | 
				
			||||||
 | 
					    _isTooltipVisible = false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return CompositedTransformTarget(
 | 
				
			||||||
 | 
					      link: _layerLink,
 | 
				
			||||||
 | 
					      child: MouseRegion(
 | 
				
			||||||
 | 
					        onEnter: (_) {
 | 
				
			||||||
 | 
					          setState(() {
 | 
				
			||||||
 | 
					            _isMouseInside = true;
 | 
				
			||||||
 | 
					            // Show tooltip after a brief delay to prevent accidental triggers
 | 
				
			||||||
 | 
					            Future.delayed(const Duration(milliseconds: 50), () {
 | 
				
			||||||
 | 
					              if (mounted && _isMouseInside && !_isTooltipVisible) {
 | 
				
			||||||
 | 
					                _showTooltip(context);
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        onExit: (_) {
 | 
				
			||||||
 | 
					          setState(() {
 | 
				
			||||||
 | 
					            _isMouseInside = false;
 | 
				
			||||||
 | 
					            // Slight delay to prevent flickering
 | 
				
			||||||
 | 
					            Future.delayed(const Duration(milliseconds: 50), () {
 | 
				
			||||||
 | 
					              if (mounted && !_isMouseInside && !_isMouseInsideTooltip) {
 | 
				
			||||||
 | 
					                _hideTooltip();
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        child: GestureDetector(
 | 
				
			||||||
 | 
					          onTap: () {
 | 
				
			||||||
 | 
					            // Toggle tooltip for touch devices
 | 
				
			||||||
 | 
					            if (_isTooltipVisible) {
 | 
				
			||||||
 | 
					              _hideTooltip();
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              _showTooltip(context);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          child: widget.child,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										308
									
								
								lib/format_converter.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										308
									
								
								lib/format_converter.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,308 @@
 | 
				
			|||||||
 | 
					import 'dart:math' as math;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Utility class to convert mixed format content (BBCode, Markdown, and HTML) to HTML
 | 
				
			||||||
 | 
					class FormatConverter {
 | 
				
			||||||
 | 
					  /// Converts mixed format text (BBCode, Markdown, HTML) to pure HTML
 | 
				
			||||||
 | 
					  static String toHtml(String content) {
 | 
				
			||||||
 | 
					    if (content.isEmpty) return '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // First, normalize line endings and escape any literal backslashes that aren't already escaped
 | 
				
			||||||
 | 
					    String result = content.replaceAll('\r\n', '\n');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Handle BBCode format
 | 
				
			||||||
 | 
					    result = _convertBBCodeToHtml(result);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Handle Markdown format
 | 
				
			||||||
 | 
					    result = _convertMarkdownToHtml(result);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Sanitize HTML
 | 
				
			||||||
 | 
					    result = _sanitizeHtml(result);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Wrap the final content in a container with styles
 | 
				
			||||||
 | 
					    result =
 | 
				
			||||||
 | 
					        '<div style="line-height: 1.5; word-wrap: break-word;">$result</div>';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return result;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Converts BBCode to HTML
 | 
				
			||||||
 | 
					  static String _convertBBCodeToHtml(String bbcode) {
 | 
				
			||||||
 | 
					    String result = bbcode;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Fix unclosed tags - RimWorld descriptions often have unclosed BBCode tags
 | 
				
			||||||
 | 
					    final List<String> tagTypes = [
 | 
				
			||||||
 | 
					      'b',
 | 
				
			||||||
 | 
					      'i',
 | 
				
			||||||
 | 
					      'color',
 | 
				
			||||||
 | 
					      'size',
 | 
				
			||||||
 | 
					      'url',
 | 
				
			||||||
 | 
					      'code',
 | 
				
			||||||
 | 
					      'quote',
 | 
				
			||||||
 | 
					      'list',
 | 
				
			||||||
 | 
					      'table',
 | 
				
			||||||
 | 
					      'tr',
 | 
				
			||||||
 | 
					      'td',
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					    for (final tag in tagTypes) {
 | 
				
			||||||
 | 
					      final openCount = '[${tag}'.allMatches(result).length;
 | 
				
			||||||
 | 
					      final closeCount = '[/$tag]'.allMatches(result).length;
 | 
				
			||||||
 | 
					      if (openCount > closeCount) {
 | 
				
			||||||
 | 
					        result = result + '[/$tag]' * (openCount - closeCount);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // URLs
 | 
				
			||||||
 | 
					    // [url=http://example.com]text[/url] -> <a href="http://example.com">text</a>
 | 
				
			||||||
 | 
					    result = RegExp(
 | 
				
			||||||
 | 
					      r'\[url=([^\]]+)\](.*?)\[/url\]',
 | 
				
			||||||
 | 
					      dotAll: true,
 | 
				
			||||||
 | 
					    ).allMatches(result).fold(result, (prev, match) {
 | 
				
			||||||
 | 
					      final url = match.group(1);
 | 
				
			||||||
 | 
					      final text = match.group(2);
 | 
				
			||||||
 | 
					      return prev.replaceFirst(
 | 
				
			||||||
 | 
					        match.group(0)!,
 | 
				
			||||||
 | 
					        '<a href="$url" target="_blank">$text</a>',
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Simple URL [url]http://example.com[/url] -> <a href="http://example.com">http://example.com</a>
 | 
				
			||||||
 | 
					    result = result.replaceAllMapped(
 | 
				
			||||||
 | 
					      RegExp(r'\[url\](.*?)\[/url\]', dotAll: true),
 | 
				
			||||||
 | 
					      (match) =>
 | 
				
			||||||
 | 
					          '<a href="${match.group(1)}" target="_blank">${match.group(1)}</a>',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Bold
 | 
				
			||||||
 | 
					    result = result
 | 
				
			||||||
 | 
					        .replaceAll('[b]', '<strong>')
 | 
				
			||||||
 | 
					        .replaceAll('[/b]', '</strong>');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Italic
 | 
				
			||||||
 | 
					    result = result.replaceAll('[i]', '<em>').replaceAll('[/i]', '</em>');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Headers
 | 
				
			||||||
 | 
					    result = result.replaceAllMapped(
 | 
				
			||||||
 | 
					      RegExp(r'\[h1\](.*?)\[/h1\]', dotAll: true),
 | 
				
			||||||
 | 
					      (match) =>
 | 
				
			||||||
 | 
					          '<h1 style="margin-top: 16px; margin-bottom: 8px;">${match.group(1)?.trim()}</h1>',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    result = result.replaceAllMapped(
 | 
				
			||||||
 | 
					      RegExp(r'\[h2\](.*?)\[/h2\]', dotAll: true),
 | 
				
			||||||
 | 
					      (match) =>
 | 
				
			||||||
 | 
					          '<h2 style="margin-top: 12px; margin-bottom: 6px;">${match.group(1)?.trim()}</h2>',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    result = result.replaceAllMapped(
 | 
				
			||||||
 | 
					      RegExp(r'\[h3\](.*?)\[/h3\]', dotAll: true),
 | 
				
			||||||
 | 
					      (match) =>
 | 
				
			||||||
 | 
					          '<h3 style="margin-top: 10px; margin-bottom: 4px;">${match.group(1)?.trim()}</h3>',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Lists
 | 
				
			||||||
 | 
					    result = result
 | 
				
			||||||
 | 
					        .replaceAll(
 | 
				
			||||||
 | 
					          '[list]',
 | 
				
			||||||
 | 
					          '<ul style="padding-left: 20px; margin-top: 8px; margin-bottom: 8px;">',
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .replaceAll('[/list]', '</ul>');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // List items
 | 
				
			||||||
 | 
					    result = result.replaceAllMapped(
 | 
				
			||||||
 | 
					      RegExp(r'\[\*\](.*?)(?=\[\*\]|\[/list\]|$)', dotAll: true),
 | 
				
			||||||
 | 
					      (match) {
 | 
				
			||||||
 | 
					        final content = match.group(1)?.trim() ?? '';
 | 
				
			||||||
 | 
					        return '<li style="margin-bottom: 4px;">$content</li>';
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Color
 | 
				
			||||||
 | 
					    result = result.replaceAllMapped(
 | 
				
			||||||
 | 
					      RegExp(r'\[color=([^\]]+)\](.*?)\[/color\]', dotAll: true),
 | 
				
			||||||
 | 
					      (match) {
 | 
				
			||||||
 | 
					        final color = match.group(1) ?? '';
 | 
				
			||||||
 | 
					        final content = match.group(2) ?? '';
 | 
				
			||||||
 | 
					        if (content.trim().isEmpty) return '';
 | 
				
			||||||
 | 
					        return '<span style="color:$color">$content</span>';
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Images
 | 
				
			||||||
 | 
					    result = result.replaceAllMapped(
 | 
				
			||||||
 | 
					      RegExp(r'\[img\](.*?)\[/img\]', dotAll: true),
 | 
				
			||||||
 | 
					      (match) =>
 | 
				
			||||||
 | 
					          '<img src="${match.group(1)}" alt="Image" style="max-width: 100%;" />',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Image with size
 | 
				
			||||||
 | 
					    result = result.replaceAllMapped(
 | 
				
			||||||
 | 
					      RegExp(r'\[img[^\]]*width=(\d+)[^\]]*\](.*?)\[/img\]', dotAll: true),
 | 
				
			||||||
 | 
					      (match) {
 | 
				
			||||||
 | 
					        final width = match.group(1) ?? '';
 | 
				
			||||||
 | 
					        final url = match.group(2) ?? '';
 | 
				
			||||||
 | 
					        return '<img src="$url" alt="Image" width="$width" style="max-width: 100%;" />';
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Tables
 | 
				
			||||||
 | 
					    result = result
 | 
				
			||||||
 | 
					        .replaceAll(
 | 
				
			||||||
 | 
					          '[table]',
 | 
				
			||||||
 | 
					          '<table border="1" style="border-collapse: collapse; width: 100%; margin: 10px 0;">',
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .replaceAll('[/table]', '</table>');
 | 
				
			||||||
 | 
					    result = result.replaceAll('[tr]', '<tr>').replaceAll('[/tr]', '</tr>');
 | 
				
			||||||
 | 
					    result = result
 | 
				
			||||||
 | 
					        .replaceAll('[td]', '<td style="padding: 8px;">')
 | 
				
			||||||
 | 
					        .replaceAll('[/td]', '</td>');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Size
 | 
				
			||||||
 | 
					    result = result.replaceAllMapped(
 | 
				
			||||||
 | 
					      RegExp(r'\[size=([^\]]+)\](.*?)\[/size\]', dotAll: true),
 | 
				
			||||||
 | 
					      (match) {
 | 
				
			||||||
 | 
					        final size = match.group(1) ?? '';
 | 
				
			||||||
 | 
					        final content = match.group(2) ?? '';
 | 
				
			||||||
 | 
					        return '<span style="font-size:${size}px">$content</span>';
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Code
 | 
				
			||||||
 | 
					    result = result
 | 
				
			||||||
 | 
					        .replaceAll(
 | 
				
			||||||
 | 
					          '[code]',
 | 
				
			||||||
 | 
					          '<pre style="background-color: rgba(0,0,0,0.1); padding: 8px; border-radius: 4px; overflow-x: auto;"><code>',
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .replaceAll('[/code]', '</code></pre>');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Quote
 | 
				
			||||||
 | 
					    result = result.replaceAllMapped(
 | 
				
			||||||
 | 
					      RegExp(r'\[quote\](.*?)\[/quote\]', dotAll: true),
 | 
				
			||||||
 | 
					      (match) {
 | 
				
			||||||
 | 
					        final content = match.group(1)?.trim() ?? '';
 | 
				
			||||||
 | 
					        if (content.isEmpty) return '';
 | 
				
			||||||
 | 
					        return '<blockquote style="border-left: 4px solid rgba(128,128,128,0.5); padding-left: 10px; margin: 10px 0; color: rgba(255,255,255,0.8);">$content</blockquote>';
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Handle any remaining custom BBCode tags
 | 
				
			||||||
 | 
					    result = result.replaceAllMapped(
 | 
				
			||||||
 | 
					      RegExp(r'\[([a-zA-Z0-9_]+)(?:=[^\]]+)?\](.*?)\[/\1\]', dotAll: true),
 | 
				
			||||||
 | 
					      (match) => match.group(2) ?? '',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Handle RimWorld-specific patterns
 | 
				
			||||||
 | 
					    // [h1] without closing tag is common
 | 
				
			||||||
 | 
					    result = result.replaceAllMapped(
 | 
				
			||||||
 | 
					      RegExp(r'\[h1\]([^\[]+)'),
 | 
				
			||||||
 | 
					      (match) =>
 | 
				
			||||||
 | 
					          '<h1 style="margin-top: 16px; margin-bottom: 8px;">${match.group(1)?.trim()}</h1>',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return result;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Converts Markdown to HTML
 | 
				
			||||||
 | 
					  static String _convertMarkdownToHtml(String markdown) {
 | 
				
			||||||
 | 
					    String result = markdown;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Headers
 | 
				
			||||||
 | 
					    // Convert # Header to <h1>Header</h1>
 | 
				
			||||||
 | 
					    result = result.replaceAllMapped(
 | 
				
			||||||
 | 
					      RegExp(r'^#\s+(.*?)$', multiLine: true),
 | 
				
			||||||
 | 
					      (match) =>
 | 
				
			||||||
 | 
					          '<h1 style="margin-top: 16px; margin-bottom: 8px;">${match.group(1)?.trim()}</h1>',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Convert ## Header to <h2>Header</h2>
 | 
				
			||||||
 | 
					    result = result.replaceAllMapped(
 | 
				
			||||||
 | 
					      RegExp(r'^##\s+(.*?)$', multiLine: true),
 | 
				
			||||||
 | 
					      (match) =>
 | 
				
			||||||
 | 
					          '<h2 style="margin-top: 12px; margin-bottom: 6px;">${match.group(1)?.trim()}</h2>',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Convert ### Header to <h3>Header</h3>
 | 
				
			||||||
 | 
					    result = result.replaceAllMapped(
 | 
				
			||||||
 | 
					      RegExp(r'^###\s+(.*?)$', multiLine: true),
 | 
				
			||||||
 | 
					      (match) =>
 | 
				
			||||||
 | 
					          '<h3 style="margin-top: 10px; margin-bottom: 4px;">${match.group(1)?.trim()}</h3>',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Bold - **text** to <strong>text</strong>
 | 
				
			||||||
 | 
					    result = result.replaceAllMapped(
 | 
				
			||||||
 | 
					      RegExp(r'\*\*(.*?)\*\*'),
 | 
				
			||||||
 | 
					      (match) => '<strong>${match.group(1)}</strong>',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Italic - *text* or _text_ to <em>text</em>
 | 
				
			||||||
 | 
					    result = result.replaceAllMapped(
 | 
				
			||||||
 | 
					      RegExp(r'\*(.*?)\*|_(.*?)_'),
 | 
				
			||||||
 | 
					      (match) => '<em>${match.group(1) ?? match.group(2)}</em>',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Inline code - `code` to <code>code</code>
 | 
				
			||||||
 | 
					    result = result.replaceAllMapped(
 | 
				
			||||||
 | 
					      RegExp(r'`(.*?)`'),
 | 
				
			||||||
 | 
					      (match) =>
 | 
				
			||||||
 | 
					          '<code style="background-color: rgba(0,0,0,0.1); padding: 2px 4px; border-radius: 3px;">${match.group(1)}</code>',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Links - [text](url) to <a href="url">text</a>
 | 
				
			||||||
 | 
					    result = result.replaceAllMapped(
 | 
				
			||||||
 | 
					      RegExp(r'\[(.*?)\]\((.*?)\)'),
 | 
				
			||||||
 | 
					      (match) =>
 | 
				
			||||||
 | 
					          '<a href="${match.group(2)}" target="_blank">${match.group(1)}</a>',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Images -  to <img src="url" alt="alt" />
 | 
				
			||||||
 | 
					    result = result.replaceAllMapped(
 | 
				
			||||||
 | 
					      RegExp(r'!\[(.*?)\]\((.*?)\)'),
 | 
				
			||||||
 | 
					      (match) =>
 | 
				
			||||||
 | 
					          '<img src="${match.group(2)}" alt="${match.group(1)}" style="max-width: 100%;" />',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Lists - Convert Markdown bullet lists to HTML lists
 | 
				
			||||||
 | 
					    // This is a simple implementation and might not handle all cases
 | 
				
			||||||
 | 
					    result = result.replaceAllMapped(
 | 
				
			||||||
 | 
					      RegExp(r'^(\s*)\*\s+(.*?)$', multiLine: true),
 | 
				
			||||||
 | 
					      (match) => '<li style="margin-bottom: 4px;">${match.group(2)}</li>',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Wrap adjacent list items in <ul> tags (simple approach)
 | 
				
			||||||
 | 
					    result = result.replaceAll('</li>\n<li>', '</li><li>');
 | 
				
			||||||
 | 
					    result = result.replaceAll(
 | 
				
			||||||
 | 
					      '<li>',
 | 
				
			||||||
 | 
					      '<ul style="padding-left: 20px; margin-top: 8px; margin-bottom: 8px;"><li>',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    result = result.replaceAll('</li>', '</li></ul>');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Remove duplicated </ul><ul> tags
 | 
				
			||||||
 | 
					    result = result.replaceAll(
 | 
				
			||||||
 | 
					      '</ul><ul style="padding-left: 20px; margin-top: 8px; margin-bottom: 8px;">',
 | 
				
			||||||
 | 
					      '',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Paragraphs - Convert newlines to <br>, but skipping where tags already exist
 | 
				
			||||||
 | 
					    result = result.replaceAllMapped(
 | 
				
			||||||
 | 
					      RegExp(r'(?<!>)\n(?!<)'),
 | 
				
			||||||
 | 
					      (match) => '<br />',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return result;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Performs basic sanitization and fixes for the HTML
 | 
				
			||||||
 | 
					  static String _sanitizeHtml(String html) {
 | 
				
			||||||
 | 
					    // Remove potentially dangerous elements and attributes
 | 
				
			||||||
 | 
					    final String result = html
 | 
				
			||||||
 | 
					        // Remove any script tags
 | 
				
			||||||
 | 
					        .replaceAll(RegExp(r'<script.*?>.*?</script>', dotAll: true), '')
 | 
				
			||||||
 | 
					        // Remove on* event handlers
 | 
				
			||||||
 | 
					        .replaceAll(RegExp(r'\son\w+=".*?"'), '')
 | 
				
			||||||
 | 
					        // Ensure newlines are converted to <br /> if not already handled
 | 
				
			||||||
 | 
					        .replaceAll(RegExp(r'(?<!>)\n(?!<)'), '<br />');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Fix double paragraph or break issues
 | 
				
			||||||
 | 
					    return result
 | 
				
			||||||
 | 
					        .replaceAll('<br /><br />', '<br />')
 | 
				
			||||||
 | 
					        .replaceAll('<br></br>', '<br />')
 | 
				
			||||||
 | 
					        .replaceAll('<p></p>', '');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										748
									
								
								lib/main.dart
									
									
									
									
									
								
							
							
						
						
									
										748
									
								
								lib/main.dart
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										202
									
								
								lib/mod.dart
									
									
									
									
									
								
							
							
						
						
									
										202
									
								
								lib/mod.dart
									
									
									
									
									
								
							@@ -59,28 +59,28 @@ class Mod {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static Mod fromDirectory(String path, {bool skipFileCount = false}) {
 | 
					  static Mod fromDirectory(String path, {bool skipFileCount = false}) {
 | 
				
			||||||
    final logger = Logger.instance;
 | 
					    // final logger = Logger.instance;
 | 
				
			||||||
    final stopwatch = Stopwatch()..start();
 | 
					    // final stopwatch = Stopwatch()..start();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    logger.info('Attempting to load mod from directory: $path');
 | 
					    // logger.info('Attempting to load mod from directory: $path');
 | 
				
			||||||
    final aboutFile = File('$path/About/About.xml');
 | 
					    final aboutFile = File('$path/About/About.xml');
 | 
				
			||||||
    if (!aboutFile.existsSync()) {
 | 
					    if (!aboutFile.existsSync()) {
 | 
				
			||||||
      logger.error('About.xml file does not exist in $aboutFile');
 | 
					      // logger.error('About.xml file does not exist in $aboutFile');
 | 
				
			||||||
      throw Exception('About.xml file does not exist in $aboutFile');
 | 
					      throw Exception('About.xml file does not exist in $aboutFile');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    logger.info('Parsing About.xml file...');
 | 
					    // logger.info('Parsing About.xml file...');
 | 
				
			||||||
    final aboutXml = XmlDocument.parse(aboutFile.readAsStringSync());
 | 
					    final aboutXml = XmlDocument.parse(aboutFile.readAsStringSync());
 | 
				
			||||||
    final xmlTime = stopwatch.elapsedMilliseconds;
 | 
					    // final xmlTime = stopwatch.elapsedMilliseconds;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    late final XmlElement metadata;
 | 
					    late final XmlElement metadata;
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      metadata = findCaseInsensitiveDoc(aboutXml, 'ModMetaData');
 | 
					      metadata = findCaseInsensitiveDoc(aboutXml, 'ModMetaData');
 | 
				
			||||||
      logger.info('Successfully found ModMetaData in About.xml');
 | 
					      // logger.info('Successfully found ModMetaData in About.xml');
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      logger.error(
 | 
					      // logger.error(
 | 
				
			||||||
        'Error: ModMetaData element is missing in About.xml ($aboutFile). Original error: $e',
 | 
					      //   'Error: ModMetaData element is missing in About.xml ($aboutFile). Original error: $e',
 | 
				
			||||||
      );
 | 
					      // );
 | 
				
			||||||
      throw Exception(
 | 
					      throw Exception(
 | 
				
			||||||
        'Error: ModMetaData element is missing in About.xml ($aboutFile). Original error: $e',
 | 
					        'Error: ModMetaData element is missing in About.xml ($aboutFile). Original error: $e',
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
@@ -89,11 +89,11 @@ class Mod {
 | 
				
			|||||||
    late final String name;
 | 
					    late final String name;
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      name = metadata.findElements('name').first.innerText;
 | 
					      name = metadata.findElements('name').first.innerText;
 | 
				
			||||||
      logger.info('Mod name found: $name');
 | 
					      // logger.info('Mod name found: $name');
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      logger.error(
 | 
					      // logger.error(
 | 
				
			||||||
        'Error: name element is missing in ModMetaData ($aboutFile). Original error: $e',
 | 
					      //   'Error: name element is missing in ModMetaData ($aboutFile). Original error: $e',
 | 
				
			||||||
      );
 | 
					      // );
 | 
				
			||||||
      throw Exception(
 | 
					      throw Exception(
 | 
				
			||||||
        'Error: name element is missing in ModMetaData ($aboutFile). Original error: $e',
 | 
					        'Error: name element is missing in ModMetaData ($aboutFile). Original error: $e',
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
@@ -102,11 +102,11 @@ class Mod {
 | 
				
			|||||||
    late final String id;
 | 
					    late final String id;
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      id = metadata.findElements('packageId').first.innerText.toLowerCase();
 | 
					      id = metadata.findElements('packageId').first.innerText.toLowerCase();
 | 
				
			||||||
      logger.info('Mod ID found: $id');
 | 
					      // logger.info('Mod ID found: $id');
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      logger.error(
 | 
					      // logger.error(
 | 
				
			||||||
        'Error: packageId element is missing in ModMetaData ($aboutFile). Original error: $e',
 | 
					      //   'Error: packageId element is missing in ModMetaData ($aboutFile). Original error: $e',
 | 
				
			||||||
      );
 | 
					      // );
 | 
				
			||||||
      throw Exception(
 | 
					      throw Exception(
 | 
				
			||||||
        'Error: packageId element is missing in ModMetaData ($aboutFile). Original error: $e',
 | 
					        'Error: packageId element is missing in ModMetaData ($aboutFile). Original error: $e',
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
@@ -121,11 +121,11 @@ class Mod {
 | 
				
			|||||||
              .findElements('li')
 | 
					              .findElements('li')
 | 
				
			||||||
              .map((e) => e.innerText)
 | 
					              .map((e) => e.innerText)
 | 
				
			||||||
              .toList();
 | 
					              .toList();
 | 
				
			||||||
      logger.info('Supported versions found: ${versions.join(", ")}');
 | 
					      // logger.info('Supported versions found: ${versions.join(", ")}');
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      logger.error(
 | 
					      // logger.error(
 | 
				
			||||||
        'Error: supportedVersions or li elements are missing in ModMetaData ($aboutFile). Original error: $e',
 | 
					      //   'Error: supportedVersions or li elements are missing in ModMetaData ($aboutFile). Original error: $e',
 | 
				
			||||||
      );
 | 
					      // );
 | 
				
			||||||
      throw Exception(
 | 
					      throw Exception(
 | 
				
			||||||
        'Error: supportedVersions or li elements are missing in ModMetaData ($aboutFile). Original error: $e',
 | 
					        'Error: supportedVersions or li elements are missing in ModMetaData ($aboutFile). Original error: $e',
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
@@ -134,11 +134,11 @@ class Mod {
 | 
				
			|||||||
    String description = '';
 | 
					    String description = '';
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      description = metadata.findElements('description').first.innerText;
 | 
					      description = metadata.findElements('description').first.innerText;
 | 
				
			||||||
      logger.info('Mod description found: $description');
 | 
					      // logger.info('Mod description found: $description');
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      logger.warning(
 | 
					      // logger.warning(
 | 
				
			||||||
        'Description element is missing in ModMetaData ($aboutFile).',
 | 
					      //   'Description element is missing in ModMetaData ($aboutFile).',
 | 
				
			||||||
      );
 | 
					      // );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    List<String> dependencies = [];
 | 
					    List<String> dependencies = [];
 | 
				
			||||||
@@ -156,11 +156,11 @@ class Mod {
 | 
				
			|||||||
                    e.findElements("packageId").first.innerText.toLowerCase(),
 | 
					                    e.findElements("packageId").first.innerText.toLowerCase(),
 | 
				
			||||||
              )
 | 
					              )
 | 
				
			||||||
              .toList();
 | 
					              .toList();
 | 
				
			||||||
      logger.info('Dependencies found: ${dependencies.join(", ")}');
 | 
					      // logger.info('Dependencies found: ${dependencies.join(", ")}');
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      logger.warning(
 | 
					      // logger.warning(
 | 
				
			||||||
        'Dependencies element is missing in ModMetaData ($aboutFile).',
 | 
					      //   'Dependencies element is missing in ModMetaData ($aboutFile).',
 | 
				
			||||||
      );
 | 
					      // );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    List<String> loadAfter = [];
 | 
					    List<String> loadAfter = [];
 | 
				
			||||||
@@ -172,12 +172,32 @@ class Mod {
 | 
				
			|||||||
              .findElements('li')
 | 
					              .findElements('li')
 | 
				
			||||||
              .map((e) => e.innerText.toLowerCase())
 | 
					              .map((e) => e.innerText.toLowerCase())
 | 
				
			||||||
              .toList();
 | 
					              .toList();
 | 
				
			||||||
      logger.info('Load after dependencies found: ${loadAfter.join(", ")}');
 | 
					      // logger.info(
 | 
				
			||||||
 | 
					      //   'Load after dependencies found: ${loadAfter.isNotEmpty ? loadAfter.join(", ") : "none"}',
 | 
				
			||||||
 | 
					      // );
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      logger.warning(
 | 
					      // logger.warning(
 | 
				
			||||||
        'Load after element is missing in ModMetaData ($aboutFile).',
 | 
					      //   'Load after element is missing or empty in ModMetaData ($aboutFile). Original error: $e',
 | 
				
			||||||
      );
 | 
					      // );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    List<String> loadAfterForce = [];
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      loadAfterForce =
 | 
				
			||||||
 | 
					          metadata
 | 
				
			||||||
 | 
					              .findElements('forceLoadAfter')
 | 
				
			||||||
 | 
					              .first
 | 
				
			||||||
 | 
					              .findElements('li')
 | 
				
			||||||
 | 
					              .map((e) => e.innerText.toLowerCase())
 | 
				
			||||||
 | 
					              .toList();
 | 
				
			||||||
 | 
					      // logger.info(
 | 
				
			||||||
 | 
					      //   'Force load after dependencies found: ${loadAfterForce.isNotEmpty ? loadAfterForce.join(", ") : "none"}',
 | 
				
			||||||
 | 
					      // );
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      // logger.warning(
 | 
				
			||||||
 | 
					      //   'Force load after element is missing or empty in ModMetaData ($aboutFile). Original error: $e',
 | 
				
			||||||
 | 
					      // );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    dependencies.addAll(loadAfterForce);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    List<String> loadBefore = [];
 | 
					    List<String> loadBefore = [];
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
@@ -188,11 +208,13 @@ class Mod {
 | 
				
			|||||||
              .findElements('li')
 | 
					              .findElements('li')
 | 
				
			||||||
              .map((e) => e.innerText.toLowerCase())
 | 
					              .map((e) => e.innerText.toLowerCase())
 | 
				
			||||||
              .toList();
 | 
					              .toList();
 | 
				
			||||||
      logger.info('Load before dependencies found: ${loadBefore.join(", ")}');
 | 
					      // logger.info(
 | 
				
			||||||
 | 
					      //   'Load before dependencies found: ${loadBefore.isNotEmpty ? loadBefore.join(", ") : "none"}',
 | 
				
			||||||
 | 
					      // );
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      logger.warning(
 | 
					      // logger.warning(
 | 
				
			||||||
        'Load before element is missing in ModMetaData ($aboutFile).',
 | 
					      //   'Load before element is missing or empty in ModMetaData ($aboutFile). Original error: $e ',
 | 
				
			||||||
      );
 | 
					      // );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    List<String> incompatibilities = [];
 | 
					    List<String> incompatibilities = [];
 | 
				
			||||||
@@ -204,51 +226,92 @@ class Mod {
 | 
				
			|||||||
              .findElements('li')
 | 
					              .findElements('li')
 | 
				
			||||||
              .map((e) => e.innerText.toLowerCase())
 | 
					              .map((e) => e.innerText.toLowerCase())
 | 
				
			||||||
              .toList();
 | 
					              .toList();
 | 
				
			||||||
      logger.info('Incompatibilities found: ${incompatibilities.join(", ")}');
 | 
					      // logger.info('Incompatibilities found: ${incompatibilities.join(", ")}');
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      logger.warning(
 | 
					      // logger.warning(
 | 
				
			||||||
        'Incompatibilities element is missing in ModMetaData ($aboutFile).',
 | 
					      //   'Incompatibilities element is missing in ModMetaData ($aboutFile).',
 | 
				
			||||||
      );
 | 
					      // );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final metadataTime = stopwatch.elapsedMilliseconds - xmlTime;
 | 
					    // final metadataTime = stopwatch.elapsedMilliseconds - xmlTime;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    int size = 0;
 | 
					    int size = 0;
 | 
				
			||||||
    if (!skipFileCount) {
 | 
					    if (!skipFileCount) {
 | 
				
			||||||
      size =
 | 
					      // Find all directories matching version pattern (like "1.0", "1.4", etc.)
 | 
				
			||||||
 | 
					      final versionDirs =
 | 
				
			||||||
          Directory(path)
 | 
					          Directory(path)
 | 
				
			||||||
              .listSync(recursive: true)
 | 
					              .listSync(recursive: false)
 | 
				
			||||||
 | 
					              .whereType<Directory>()
 | 
				
			||||||
              .where(
 | 
					              .where(
 | 
				
			||||||
                (entity) =>
 | 
					                (dir) => RegExp(
 | 
				
			||||||
                    !entity.path
 | 
					                  r'^\d+\.\d+$',
 | 
				
			||||||
 | 
					                ).hasMatch(dir.path.split(Platform.pathSeparator).last),
 | 
				
			||||||
 | 
					              )
 | 
				
			||||||
 | 
					              .toList();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Find the latest version directory (if any)
 | 
				
			||||||
 | 
					      Directory? latestVersionDir;
 | 
				
			||||||
 | 
					      if (versionDirs.isNotEmpty) {
 | 
				
			||||||
 | 
					        // Sort by version number
 | 
				
			||||||
 | 
					        versionDirs.sort((a, b) {
 | 
				
			||||||
 | 
					          final List<int> vA =
 | 
				
			||||||
 | 
					              a.path
 | 
				
			||||||
                  .split(Platform.pathSeparator)
 | 
					                  .split(Platform.pathSeparator)
 | 
				
			||||||
                  .last
 | 
					                  .last
 | 
				
			||||||
                        .startsWith('.'),
 | 
					                  .split('.')
 | 
				
			||||||
              )
 | 
					                  .map(int.parse)
 | 
				
			||||||
              .length;
 | 
					                  .toList();
 | 
				
			||||||
      logger.info('File count in mod directory: $size');
 | 
					          final List<int> vB =
 | 
				
			||||||
 | 
					              b.path
 | 
				
			||||||
 | 
					                  .split(Platform.pathSeparator)
 | 
				
			||||||
 | 
					                  .last
 | 
				
			||||||
 | 
					                  .split('.')
 | 
				
			||||||
 | 
					                  .map(int.parse)
 | 
				
			||||||
 | 
					                  .toList();
 | 
				
			||||||
 | 
					          return vA[0] != vB[0]
 | 
				
			||||||
 | 
					              ? vA[0] - vB[0]
 | 
				
			||||||
 | 
					              : vA[1] - vB[1]; // Compare major, then minor version
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        latestVersionDir = versionDirs.last;
 | 
				
			||||||
 | 
					        // logger.info(
 | 
				
			||||||
 | 
					        //   'Latest version directory found: ${latestVersionDir.path.split(Platform.pathSeparator).last}',
 | 
				
			||||||
 | 
					        // );
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Check if this is RimWorld base game or expansion
 | 
					      // Count all files, excluding older version directories
 | 
				
			||||||
    bool isBaseGame = id == 'ludeon.rimworld';
 | 
					      size =
 | 
				
			||||||
    bool isExpansion = !isBaseGame && id.startsWith('ludeon.rimworld.');
 | 
					          Directory(path).listSync(recursive: true).where((entity) {
 | 
				
			||||||
 | 
					            if (entity is! File ||
 | 
				
			||||||
    // If this is an expansion, ensure it depends on the base game
 | 
					                entity.path
 | 
				
			||||||
    if (isExpansion && !loadAfter.contains('ludeon.rimworld')) {
 | 
					                    .split(Platform.pathSeparator)
 | 
				
			||||||
      loadAfter.add('ludeon.rimworld');
 | 
					                    .any((part) => part.startsWith('.'))) {
 | 
				
			||||||
      logger.info(
 | 
					              return false;
 | 
				
			||||||
        'Added base game dependency for expansion mod: ludeon.rimworld',
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final fileCountTime =
 | 
					            // Skip files in version directories except for the latest
 | 
				
			||||||
        stopwatch.elapsedMilliseconds - metadataTime - xmlTime;
 | 
					            for (final verDir in versionDirs) {
 | 
				
			||||||
    final totalTime = stopwatch.elapsedMilliseconds;
 | 
					              if (verDir != latestVersionDir &&
 | 
				
			||||||
 | 
					                  entity.path.startsWith(verDir.path)) {
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
 | 
					          }).length;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // logger.info(
 | 
				
			||||||
 | 
					      //   'File count in mod directory (with only latest version): $size',
 | 
				
			||||||
 | 
					      // );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // final fileCountTime =
 | 
				
			||||||
 | 
					    //     stopwatch.elapsedMilliseconds - metadataTime - xmlTime;
 | 
				
			||||||
 | 
					    // final totalTime = stopwatch.elapsedMilliseconds;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Log detailed timing information
 | 
					    // Log detailed timing information
 | 
				
			||||||
    logger.info(
 | 
					    // logger.info(
 | 
				
			||||||
      'Mod $name timing: XML=${xmlTime}ms, Metadata=${metadataTime}ms, FileCount=${fileCountTime}ms, Total=${totalTime}ms',
 | 
					    //   'Mod $name timing: XML=${xmlTime}ms, Metadata=${metadataTime}ms, FileCount=${fileCountTime}ms, Total=${totalTime}ms',
 | 
				
			||||||
    );
 | 
					    // );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Mod(
 | 
					    return Mod(
 | 
				
			||||||
      name: name,
 | 
					      name: name,
 | 
				
			||||||
@@ -261,8 +324,9 @@ class Mod {
 | 
				
			|||||||
      loadBefore: loadBefore,
 | 
					      loadBefore: loadBefore,
 | 
				
			||||||
      incompatibilities: incompatibilities,
 | 
					      incompatibilities: incompatibilities,
 | 
				
			||||||
      size: size,
 | 
					      size: size,
 | 
				
			||||||
      isBaseGame: isBaseGame,
 | 
					      // No mods loaded from workshop are ever base or expansion games
 | 
				
			||||||
      isExpansion: isExpansion,
 | 
					      isBaseGame: false,
 | 
				
			||||||
 | 
					      isExpansion: false,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,11 +13,99 @@ class LoadOrder {
 | 
				
			|||||||
    return order.map((mod) => mod.id).toList();
 | 
					    return order.map((mod) => mod.id).toList();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  LoadOrder();
 | 
					  LoadOrder([List<Mod>? order]) {
 | 
				
			||||||
 | 
					    this.order = order ?? [];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  bool get hasErrors => errors.isNotEmpty;
 | 
					  bool get hasErrors => errors.isNotEmpty;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var specialMods = {
 | 
				
			||||||
 | 
					  'ludeon.rimworld': Mod(
 | 
				
			||||||
 | 
					    id: 'ludeon.rimworld',
 | 
				
			||||||
 | 
					    name: 'RimWorld',
 | 
				
			||||||
 | 
					    path: '',
 | 
				
			||||||
 | 
					    versions: [],
 | 
				
			||||||
 | 
					    description: 'RimWorld base game',
 | 
				
			||||||
 | 
					    dependencies: [],
 | 
				
			||||||
 | 
					    loadAfter: [],
 | 
				
			||||||
 | 
					    loadBefore: [],
 | 
				
			||||||
 | 
					    incompatibilities: [],
 | 
				
			||||||
 | 
					    isBaseGame: true,
 | 
				
			||||||
 | 
					    size: 0,
 | 
				
			||||||
 | 
					    isExpansion: false,
 | 
				
			||||||
 | 
					    enabled: true,
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					  'ludeon.rimworld.royalty': Mod(
 | 
				
			||||||
 | 
					    id: 'ludeon.rimworld.royalty',
 | 
				
			||||||
 | 
					    name: 'Royalty',
 | 
				
			||||||
 | 
					    path: '',
 | 
				
			||||||
 | 
					    versions: [],
 | 
				
			||||||
 | 
					    description: 'RimWorld expansion - Royalty',
 | 
				
			||||||
 | 
					    dependencies: ['ludeon.rimworld'],
 | 
				
			||||||
 | 
					    loadAfter: [],
 | 
				
			||||||
 | 
					    loadBefore: [
 | 
				
			||||||
 | 
					      'ludeon.rimworld.anomaly',
 | 
				
			||||||
 | 
					      'ludeon.rimworld.biotech',
 | 
				
			||||||
 | 
					      'ludeon.rimworld.ideology',
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    incompatibilities: [],
 | 
				
			||||||
 | 
					    isBaseGame: false,
 | 
				
			||||||
 | 
					    size: 0,
 | 
				
			||||||
 | 
					    isExpansion: true,
 | 
				
			||||||
 | 
					    enabled: true,
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					  'ludeon.rimworld.ideology': Mod(
 | 
				
			||||||
 | 
					    id: 'ludeon.rimworld.ideology',
 | 
				
			||||||
 | 
					    name: 'Ideology',
 | 
				
			||||||
 | 
					    path: '',
 | 
				
			||||||
 | 
					    versions: [],
 | 
				
			||||||
 | 
					    description: 'RimWorld expansion - Ideology',
 | 
				
			||||||
 | 
					    dependencies: ['ludeon.rimworld'],
 | 
				
			||||||
 | 
					    loadAfter: ['ludeon.rimworld.royalty'],
 | 
				
			||||||
 | 
					    loadBefore: ['ludeon.rimworld.anomaly', 'ludeon.rimworld.biotech'],
 | 
				
			||||||
 | 
					    incompatibilities: [],
 | 
				
			||||||
 | 
					    isBaseGame: false,
 | 
				
			||||||
 | 
					    size: 0,
 | 
				
			||||||
 | 
					    isExpansion: true,
 | 
				
			||||||
 | 
					    enabled: true,
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					  'ludeon.rimworld.biotech': Mod(
 | 
				
			||||||
 | 
					    id: 'ludeon.rimworld.biotech',
 | 
				
			||||||
 | 
					    name: 'Biotech',
 | 
				
			||||||
 | 
					    path: '',
 | 
				
			||||||
 | 
					    versions: [],
 | 
				
			||||||
 | 
					    description: 'RimWorld expansion - Biotech',
 | 
				
			||||||
 | 
					    dependencies: ['ludeon.rimworld'],
 | 
				
			||||||
 | 
					    loadAfter: ['ludeon.rimworld.ideology', 'ludeon.rimworld.royalty'],
 | 
				
			||||||
 | 
					    loadBefore: ['ludeon.rimworld.anomaly'],
 | 
				
			||||||
 | 
					    incompatibilities: [],
 | 
				
			||||||
 | 
					    isBaseGame: false,
 | 
				
			||||||
 | 
					    size: 0,
 | 
				
			||||||
 | 
					    isExpansion: true,
 | 
				
			||||||
 | 
					    enabled: true,
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					  'ludeon.rimworld.anomaly': Mod(
 | 
				
			||||||
 | 
					    id: 'ludeon.rimworld.anomaly',
 | 
				
			||||||
 | 
					    name: 'Anomaly',
 | 
				
			||||||
 | 
					    path: '',
 | 
				
			||||||
 | 
					    versions: [],
 | 
				
			||||||
 | 
					    description: 'RimWorld expansion - Anomaly',
 | 
				
			||||||
 | 
					    dependencies: ['ludeon.rimworld'],
 | 
				
			||||||
 | 
					    loadAfter: [
 | 
				
			||||||
 | 
					      'ludeon.rimworld.biotech',
 | 
				
			||||||
 | 
					      'ludeon.rimworld.ideology',
 | 
				
			||||||
 | 
					      'ludeon.rimworld.royalty',
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    loadBefore: [],
 | 
				
			||||||
 | 
					    incompatibilities: [],
 | 
				
			||||||
 | 
					    isBaseGame: false,
 | 
				
			||||||
 | 
					    size: 0,
 | 
				
			||||||
 | 
					    isExpansion: true,
 | 
				
			||||||
 | 
					    enabled: true,
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ModList {
 | 
					class ModList {
 | 
				
			||||||
  String configPath = '';
 | 
					  String configPath = '';
 | 
				
			||||||
  String modsPath = '';
 | 
					  String modsPath = '';
 | 
				
			||||||
@@ -42,44 +130,43 @@ class ModList {
 | 
				
			|||||||
    return newModlist;
 | 
					    return newModlist;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Stream<Mod> loadAvailable() async* {
 | 
					  Stream<Mod> loadAvailable({bool skipExistingSizes = false}) async* {
 | 
				
			||||||
    final logger = Logger.instance;
 | 
					    // final logger = Logger.instance;
 | 
				
			||||||
    final stopwatch = Stopwatch()..start();
 | 
					    // final stopwatch = Stopwatch()..start();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final directory = Directory(modsPath);
 | 
					    final directory = Directory(modsPath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!directory.existsSync()) {
 | 
					    if (!directory.existsSync()) {
 | 
				
			||||||
      logger.error('Error: Mods root directory does not exist: $modsPath');
 | 
					      // logger.error('Error: Mods root directory does not exist: $modsPath');
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final List<FileSystemEntity> entities = directory.listSync();
 | 
					    final List<FileSystemEntity> entities = directory.listSync();
 | 
				
			||||||
    // TODO: Count only the latest version of each mod and not all versions
 | 
					 | 
				
			||||||
    final List<String> modDirectories =
 | 
					    final List<String> modDirectories =
 | 
				
			||||||
        entities.whereType<Directory>().map((dir) => dir.path).toList();
 | 
					        entities.whereType<Directory>().map((dir) => dir.path).toList();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    logger.info(
 | 
					    // logger.info(
 | 
				
			||||||
      'Found ${modDirectories.length} mod directories (${stopwatch.elapsedMilliseconds}ms)',
 | 
					    //   'Found ${modDirectories.length} mod directories (${stopwatch.elapsedMilliseconds}ms)',
 | 
				
			||||||
    );
 | 
					    // );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (final modDir in modDirectories) {
 | 
					    for (final modDir in modDirectories) {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        final modStart = stopwatch.elapsedMilliseconds;
 | 
					        // final modStart = stopwatch.elapsedMilliseconds;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Check if this directory contains a valid mod
 | 
					        // Check if this directory contains a valid mod
 | 
				
			||||||
        final aboutFile = File('$modDir/About/About.xml');
 | 
					        final aboutFile = File('$modDir/About/About.xml');
 | 
				
			||||||
        if (!aboutFile.existsSync()) {
 | 
					        if (!aboutFile.existsSync()) {
 | 
				
			||||||
          logger.warning('No About.xml found in directory: $modDir');
 | 
					          // logger.warning('No About.xml found in directory: $modDir');
 | 
				
			||||||
          continue;
 | 
					          continue;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        final mod = Mod.fromDirectory(modDir);
 | 
					        final mod = Mod.fromDirectory(modDir, skipFileCount: skipExistingSizes);
 | 
				
			||||||
        logger.info('Loaded mod from directory: ${mod.name} (ID: ${mod.id})');
 | 
					        // logger.info('Loaded mod from directory: ${mod.name} (ID: ${mod.id})');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (mods.containsKey(mod.id)) {
 | 
					        if (mods.containsKey(mod.id)) {
 | 
				
			||||||
          logger.warning(
 | 
					          // logger.warning(
 | 
				
			||||||
            'Mod $mod.id already exists in mods list, overwriting',
 | 
					          //   'Mod ${mod.id} already exists in mods list, overwriting',
 | 
				
			||||||
          );
 | 
					          // );
 | 
				
			||||||
          final existingMod = mods[mod.id]!;
 | 
					          final existingMod = mods[mod.id]!;
 | 
				
			||||||
          mods[mod.id] = Mod(
 | 
					          mods[mod.id] = Mod(
 | 
				
			||||||
            name: mod.name,
 | 
					            name: mod.name,
 | 
				
			||||||
@@ -96,20 +183,20 @@ class ModList {
 | 
				
			|||||||
            isBaseGame: existingMod.isBaseGame,
 | 
					            isBaseGame: existingMod.isBaseGame,
 | 
				
			||||||
            isExpansion: existingMod.isExpansion,
 | 
					            isExpansion: existingMod.isExpansion,
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
          logger.info('Updated existing mod: ${mod.name} (ID: ${mod.id})');
 | 
					          // logger.info('Updated existing mod: ${mod.name} (ID: ${mod.id})');
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          mods[mod.id] = mod;
 | 
					          mods[mod.id] = mod;
 | 
				
			||||||
          logger.info('Added new mod: ${mod.name} (ID: ${mod.id})');
 | 
					          // logger.info('Added new mod: ${mod.name} (ID: ${mod.id})');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        final modTime = stopwatch.elapsedMilliseconds - modStart;
 | 
					        // final modTime = stopwatch.elapsedMilliseconds - modStart;
 | 
				
			||||||
        logger.info(
 | 
					        // logger.info(
 | 
				
			||||||
          'Loaded mod from directory: ${mod.name} (ID: ${mod.id}) in $modTime ms',
 | 
					        //   'Loaded mod from directory: ${mod.name} (ID: ${mod.id}) in $modTime ms',
 | 
				
			||||||
        );
 | 
					        // );
 | 
				
			||||||
        yield mod;
 | 
					        yield mod;
 | 
				
			||||||
      } catch (e) {
 | 
					      } catch (e) {
 | 
				
			||||||
        logger.error('Error loading mod from directory: $modDir');
 | 
					        // logger.error('Error loading mod from directory: $modDir');
 | 
				
			||||||
        logger.error('Error: $e');
 | 
					        // logger.error('Error: $e');
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -152,42 +239,30 @@ class ModList {
 | 
				
			|||||||
      for (final modElement in modElements) {
 | 
					      for (final modElement in modElements) {
 | 
				
			||||||
        final modId = modElement.innerText.toLowerCase();
 | 
					        final modId = modElement.innerText.toLowerCase();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Check if this is a special Ludeon mod
 | 
					        if (specialMods.containsKey(modId)) {
 | 
				
			||||||
        final isBaseGame = modId == 'ludeon.rimworld';
 | 
					          logger.info('Loading special mod: $modId');
 | 
				
			||||||
        final isExpansion =
 | 
					          mods[modId] = specialMods[modId]!.copyWith();
 | 
				
			||||||
            !isBaseGame &&
 | 
					          setEnabled(modId, true);
 | 
				
			||||||
            modId.startsWith('ludeon.rimworld.') &&
 | 
					          logger.info('Enabled special mod: $modId');
 | 
				
			||||||
            knownExpansionIds.contains(modId);
 | 
					          yield mods[modId]!;
 | 
				
			||||||
 | 
					          continue;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        final existingMod = mods[modId];
 | 
					        final existingMod = mods[modId];
 | 
				
			||||||
        final mod = Mod(
 | 
					        final mod = Mod(
 | 
				
			||||||
          name:
 | 
					          name: existingMod?.name ?? modId,
 | 
				
			||||||
              existingMod?.name ??
 | 
					 | 
				
			||||||
              (isBaseGame
 | 
					 | 
				
			||||||
                  ? "RimWorld"
 | 
					 | 
				
			||||||
                  : isExpansion
 | 
					 | 
				
			||||||
                  ? "RimWorld ${_expansionNameFromId(modId)}"
 | 
					 | 
				
			||||||
                  : modId),
 | 
					 | 
				
			||||||
          id: existingMod?.id ?? modId,
 | 
					          id: existingMod?.id ?? modId,
 | 
				
			||||||
          path: existingMod?.path ?? '',
 | 
					          path: existingMod?.path ?? '',
 | 
				
			||||||
          versions: existingMod?.versions ?? [],
 | 
					          versions: existingMod?.versions ?? [],
 | 
				
			||||||
          description:
 | 
					          description: existingMod?.description ?? '',
 | 
				
			||||||
              existingMod?.description ??
 | 
					 | 
				
			||||||
              (isBaseGame
 | 
					 | 
				
			||||||
                  ? "RimWorld base game"
 | 
					 | 
				
			||||||
                  : isExpansion
 | 
					 | 
				
			||||||
                  ? "RimWorld expansion"
 | 
					 | 
				
			||||||
                  : ""),
 | 
					 | 
				
			||||||
          dependencies: existingMod?.dependencies ?? [],
 | 
					          dependencies: existingMod?.dependencies ?? [],
 | 
				
			||||||
          loadAfter:
 | 
					          loadAfter: existingMod?.loadAfter ?? [],
 | 
				
			||||||
              existingMod?.loadAfter ??
 | 
					 | 
				
			||||||
              (isExpansion ? ['ludeon.rimworld'] : []),
 | 
					 | 
				
			||||||
          loadBefore: existingMod?.loadBefore ?? [],
 | 
					          loadBefore: existingMod?.loadBefore ?? [],
 | 
				
			||||||
          incompatibilities: existingMod?.incompatibilities ?? [],
 | 
					          incompatibilities: existingMod?.incompatibilities ?? [],
 | 
				
			||||||
          enabled: existingMod?.enabled ?? false,
 | 
					          enabled: existingMod?.enabled ?? false,
 | 
				
			||||||
          size: existingMod?.size ?? 0,
 | 
					          size: existingMod?.size ?? 0,
 | 
				
			||||||
          isBaseGame: isBaseGame,
 | 
					          isBaseGame: false,
 | 
				
			||||||
          isExpansion: isExpansion,
 | 
					          isExpansion: false,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        if (mods.containsKey(modId)) {
 | 
					        if (mods.containsKey(modId)) {
 | 
				
			||||||
          logger.warning('Mod $modId already exists in mods list, overwriting');
 | 
					          logger.warning('Mod $modId already exists in mods list, overwriting');
 | 
				
			||||||
@@ -204,6 +279,73 @@ class ModList {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void saveToConfig(LoadOrder loadOrder) {
 | 
				
			||||||
 | 
					    final file = File(configPath);
 | 
				
			||||||
 | 
					    final logger = Logger.instance;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      // Create XML builder
 | 
				
			||||||
 | 
					      final builder = XmlBuilder();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Add XML declaration
 | 
				
			||||||
 | 
					      builder.declaration(encoding: 'utf-8');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Add root element
 | 
				
			||||||
 | 
					      builder.element(
 | 
				
			||||||
 | 
					        'ModsConfigData',
 | 
				
			||||||
 | 
					        nest: () {
 | 
				
			||||||
 | 
					          // Add version element
 | 
				
			||||||
 | 
					          builder.element('version', nest: '1.5.4297 rev994');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          // Add active mods element
 | 
				
			||||||
 | 
					          builder.element(
 | 
				
			||||||
 | 
					            'activeMods',
 | 
				
			||||||
 | 
					            nest: () {
 | 
				
			||||||
 | 
					              // Add each mod as a list item
 | 
				
			||||||
 | 
					              for (final mod in loadOrder.order) {
 | 
				
			||||||
 | 
					                builder.element('li', nest: mod.id);
 | 
				
			||||||
 | 
					                logger.info('Adding mod to config: ${mod.name} (${mod.id})');
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          // Add known expansions element
 | 
				
			||||||
 | 
					          final expansions = mods.values.where((m) => m.isExpansion).toList();
 | 
				
			||||||
 | 
					          if (expansions.isNotEmpty) {
 | 
				
			||||||
 | 
					            builder.element(
 | 
				
			||||||
 | 
					              'knownExpansions',
 | 
				
			||||||
 | 
					              nest: () {
 | 
				
			||||||
 | 
					                for (final mod in expansions) {
 | 
				
			||||||
 | 
					                  builder.element('li', nest: mod.id);
 | 
				
			||||||
 | 
					                  logger.info(
 | 
				
			||||||
 | 
					                    'Adding expansion to config: ${mod.name} (${mod.id})',
 | 
				
			||||||
 | 
					                  );
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Build the XML document
 | 
				
			||||||
 | 
					      final xmlDocument = builder.buildDocument();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Convert to string with 2-space indentation
 | 
				
			||||||
 | 
					      final prettyXml = xmlDocument.toXmlString(
 | 
				
			||||||
 | 
					        pretty: true,
 | 
				
			||||||
 | 
					        indent: '  ', // 2 spaces
 | 
				
			||||||
 | 
					        newLine: '\n',
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Write the formatted XML document to file
 | 
				
			||||||
 | 
					      file.writeAsStringSync(prettyXml);
 | 
				
			||||||
 | 
					      logger.info('Successfully saved mod configuration to: $configPath');
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      logger.error('Error saving configuration file: $e');
 | 
				
			||||||
 | 
					      throw Exception('Failed to save config file: $e');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void setEnabled(String modId, bool enabled) {
 | 
					  void setEnabled(String modId, bool enabled) {
 | 
				
			||||||
    if (mods.containsKey(modId)) {
 | 
					    if (mods.containsKey(modId)) {
 | 
				
			||||||
      final mod = mods[modId]!;
 | 
					      final mod = mods[modId]!;
 | 
				
			||||||
@@ -240,25 +382,19 @@ 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 generateLoadOrder([LoadOrder? loadOrder]) {
 | 
				
			||||||
    loadOrder ??= LoadOrder();
 | 
					    loadOrder ??= LoadOrder();
 | 
				
			||||||
    final logger = Logger.instance;
 | 
					    final logger = Logger.instance;
 | 
				
			||||||
    logger.info('Generating load order...');
 | 
					    logger.info('Generating load order...');
 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (final mod in activeMods.values) {
 | 
					    for (final mod in activeMods.values) {
 | 
				
			||||||
      logger.info('Checking mod: ${mod.id}');
 | 
					      logger.info('Checking mod: ${mod.id}');
 | 
				
			||||||
 | 
					      if (specialMods.containsKey(mod.id)) {
 | 
				
			||||||
 | 
					        logger.info('Special mod: ${mod.id}');
 | 
				
			||||||
 | 
					        // Replace our fake base game mod with the chad one
 | 
				
			||||||
 | 
					        // This is a bit of a hack, but it works
 | 
				
			||||||
 | 
					        activeMods[mod.id] = specialMods[mod.id]!.copyWith();
 | 
				
			||||||
 | 
					        mods[mod.id] = specialMods[mod.id]!.copyWith();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      logger.info('Mod details: ${mod.toString()}');
 | 
					      logger.info('Mod details: ${mod.toString()}');
 | 
				
			||||||
      for (final incomp in mod.incompatibilities) {
 | 
					      for (final incomp in mod.incompatibilities) {
 | 
				
			||||||
        if (activeMods.containsKey(incomp)) {
 | 
					        if (activeMods.containsKey(incomp)) {
 | 
				
			||||||
@@ -566,21 +702,96 @@ class ModList {
 | 
				
			|||||||
  LoadOrder loadRequired([LoadOrder? loadOrder]) {
 | 
					  LoadOrder loadRequired([LoadOrder? loadOrder]) {
 | 
				
			||||||
    loadOrder ??= LoadOrder();
 | 
					    loadOrder ??= LoadOrder();
 | 
				
			||||||
    final toEnable = <String>[];
 | 
					    final toEnable = <String>[];
 | 
				
			||||||
 | 
					    final logger = Logger.instance;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // First, identify all base game and expansion mods
 | 
				
			||||||
 | 
					    final baseGameIds = <String>{};
 | 
				
			||||||
 | 
					    final expansionIds = <String>{};
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    for (final entry in mods.entries) {
 | 
				
			||||||
 | 
					      if (entry.value.isBaseGame) {
 | 
				
			||||||
 | 
					        baseGameIds.add(entry.key);
 | 
				
			||||||
 | 
					      } else if (entry.value.isExpansion) {
 | 
				
			||||||
 | 
					        expansionIds.add(entry.key);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    logger.info("Base game mods: ${baseGameIds.join(', ')}");
 | 
				
			||||||
 | 
					    logger.info("Expansion mods: ${expansionIds.join(', ')}");
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Load dependencies for all active mods
 | 
				
			||||||
    for (final modid in activeMods.keys) {
 | 
					    for (final modid in activeMods.keys) {
 | 
				
			||||||
      loadDependencies(modid, loadOrder, toEnable);
 | 
					      loadDependencies(modid, loadOrder, toEnable);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Enable all required dependencies
 | 
				
			||||||
    for (final modid in toEnable) {
 | 
					    for (final modid in toEnable) {
 | 
				
			||||||
      setEnabled(modid, true);
 | 
					      setEnabled(modid, true);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return generateLoadOrder(loadOrder);
 | 
					    
 | 
				
			||||||
 | 
					    // Generate the load order
 | 
				
			||||||
 | 
					    final newLoadOrder = generateLoadOrder(loadOrder);
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Filter out any error messages related to incompatibilities between base game and expansions
 | 
				
			||||||
 | 
					    if (newLoadOrder.hasErrors) {
 | 
				
			||||||
 | 
					      final filteredErrors = <String>[];
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      for (final error in newLoadOrder.errors) {
 | 
				
			||||||
 | 
					        // Check if the error is about incompatibility
 | 
				
			||||||
 | 
					        if (error.contains('Incompatibility detected:')) {
 | 
				
			||||||
 | 
					          // Extract the mod IDs from the error message
 | 
				
			||||||
 | 
					          final parts = error.split(' is incompatible with ');
 | 
				
			||||||
 | 
					          if (parts.length == 2) {
 | 
				
			||||||
 | 
					            final firstModId = parts[0].replaceAll('Incompatibility detected: ', '');
 | 
				
			||||||
 | 
					            final secondModId = parts[1];
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Check if either mod is a base game or expansion
 | 
				
			||||||
 | 
					            final isBaseGameOrExpansion = 
 | 
				
			||||||
 | 
					                baseGameIds.contains(firstModId) || baseGameIds.contains(secondModId) ||
 | 
				
			||||||
 | 
					                expansionIds.contains(firstModId) || expansionIds.contains(secondModId);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					            // Only keep the error if it's not between base game/expansions
 | 
				
			||||||
 | 
					            if (!isBaseGameOrExpansion) {
 | 
				
			||||||
 | 
					              filteredErrors.add(error);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              logger.info("Ignoring incompatibility between base game or expansion mods: $error");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            // If we can't parse the error, keep it
 | 
				
			||||||
 | 
					            filteredErrors.add(error);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          // Keep non-incompatibility errors
 | 
				
			||||||
 | 
					          filteredErrors.add(error);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      // Replace the errors with the filtered list
 | 
				
			||||||
 | 
					      newLoadOrder.errors.clear();
 | 
				
			||||||
 | 
					      newLoadOrder.errors.addAll(filteredErrors);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return newLoadOrder;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  LoadOrder loadRequiredBaseGame([LoadOrder? loadOrder]) {
 | 
				
			||||||
 | 
					    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) {
 | 
					 | 
				
			||||||
  final parts = id.split('.');
 | 
					 | 
				
			||||||
  if (parts.length < 3) return id;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  final expansionPart = parts[2];
 | 
					 | 
				
			||||||
  return expansionPart.substring(0, 1).toUpperCase() +
 | 
					 | 
				
			||||||
      expansionPart.substring(1);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										829
									
								
								lib/mod_troubleshooter_widget.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										829
									
								
								lib/mod_troubleshooter_widget.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,829 @@
 | 
				
			|||||||
 | 
					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)
 | 
				
			||||||
 | 
					  LoadOrder _loadOrder = LoadOrder();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 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
 | 
				
			||||||
 | 
					      _loadOrder = LoadOrder(modManager.activeMods.values.toList());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 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(() {
 | 
				
			||||||
 | 
					      _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(() {
 | 
				
			||||||
 | 
					      _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 (_loadOrder.order.isEmpty) {
 | 
				
			||||||
 | 
					      ScaffoldMessenger.of(context).showSnackBar(
 | 
				
			||||||
 | 
					        const SnackBar(
 | 
				
			||||||
 | 
					          content: Text('No mods selected to save'),
 | 
				
			||||||
 | 
					          duration: Duration(seconds: 2),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    modManager.saveToConfig(_loadOrder);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Save the configuration (we don't have direct access to save method, so show a message)
 | 
				
			||||||
 | 
					    ScaffoldMessenger.of(context).showSnackBar(
 | 
				
			||||||
 | 
					      SnackBar(
 | 
				
			||||||
 | 
					        content: Text(
 | 
				
			||||||
 | 
					          '${_loadOrder.order.length} mods have been successfully saved to the configuration.',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        backgroundColor: Colors.green,
 | 
				
			||||||
 | 
					        duration: const Duration(seconds: 4),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _markSelectedAsGood() {
 | 
				
			||||||
 | 
					    if (_loadOrder.order.isEmpty) {
 | 
				
			||||||
 | 
					      ScaffoldMessenger.of(context).showSnackBar(
 | 
				
			||||||
 | 
					        const SnackBar(
 | 
				
			||||||
 | 
					          content: Text('No mods selected to mark'),
 | 
				
			||||||
 | 
					          duration: Duration(seconds: 2),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setState(() {
 | 
				
			||||||
 | 
					      for (final mod in _loadOrder.order) {
 | 
				
			||||||
 | 
					        _checkedMods.add(mod.id);
 | 
				
			||||||
 | 
					        _problemMods.remove(mod.id);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ScaffoldMessenger.of(context).showSnackBar(
 | 
				
			||||||
 | 
					      SnackBar(
 | 
				
			||||||
 | 
					        content: Text('Marked ${_loadOrder.order.length} mods as good'),
 | 
				
			||||||
 | 
					        backgroundColor: Colors.green,
 | 
				
			||||||
 | 
					        duration: const Duration(seconds: 2),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _markSelectedAsProblem() {
 | 
				
			||||||
 | 
					    if (_loadOrder.order.isEmpty) {
 | 
				
			||||||
 | 
					      ScaffoldMessenger.of(context).showSnackBar(
 | 
				
			||||||
 | 
					        const SnackBar(
 | 
				
			||||||
 | 
					          content: Text('No mods selected to mark'),
 | 
				
			||||||
 | 
					          duration: Duration(seconds: 2),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setState(() {
 | 
				
			||||||
 | 
					      for (final mod in _loadOrder.order) {
 | 
				
			||||||
 | 
					        _problemMods.add(mod.id);
 | 
				
			||||||
 | 
					        _checkedMods.remove(mod.id);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ScaffoldMessenger.of(context).showSnackBar(
 | 
				
			||||||
 | 
					      SnackBar(
 | 
				
			||||||
 | 
					        content: Text('Marked ${_loadOrder.order.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(
 | 
				
			||||||
 | 
					                    _loadOrder.order.isNotEmpty
 | 
				
			||||||
 | 
					                        ? 'Testing ${_loadOrder.order.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 (_loadOrder.order.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 (_loadOrder.order.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 = _loadOrder.order.any(
 | 
				
			||||||
 | 
					                  (m) => m.id == 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;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -6,6 +6,10 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#include "generated_plugin_registrant.h"
 | 
					#include "generated_plugin_registrant.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <url_launcher_linux/url_launcher_plugin.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void fl_register_plugins(FlPluginRegistry* registry) {
 | 
					void fl_register_plugins(FlPluginRegistry* registry) {
 | 
				
			||||||
 | 
					  g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
 | 
				
			||||||
 | 
					      fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
 | 
				
			||||||
 | 
					  url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,7 @@
 | 
				
			|||||||
#
 | 
					#
 | 
				
			||||||
 | 
					
 | 
				
			||||||
list(APPEND FLUTTER_PLUGIN_LIST
 | 
					list(APPEND FLUTTER_PLUGIN_LIST
 | 
				
			||||||
 | 
					  url_launcher_linux
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
 | 
					list(APPEND FLUTTER_FFI_PLUGIN_LIST
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,8 @@
 | 
				
			|||||||
import FlutterMacOS
 | 
					import FlutterMacOS
 | 
				
			||||||
import Foundation
 | 
					import Foundation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import url_launcher_macos
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
 | 
					func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
 | 
				
			||||||
 | 
					  UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										127
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										127
									
								
								pubspec.lock
									
									
									
									
									
								
							@@ -89,6 +89,14 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "3.0.6"
 | 
					    version: "3.0.6"
 | 
				
			||||||
 | 
					  csslib:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: csslib
 | 
				
			||||||
 | 
					      sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "1.0.2"
 | 
				
			||||||
  cupertino_icons:
 | 
					  cupertino_icons:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -118,6 +126,14 @@ packages:
 | 
				
			|||||||
    description: flutter
 | 
					    description: flutter
 | 
				
			||||||
    source: sdk
 | 
					    source: sdk
 | 
				
			||||||
    version: "0.0.0"
 | 
					    version: "0.0.0"
 | 
				
			||||||
 | 
					  flutter_html:
 | 
				
			||||||
 | 
					    dependency: "direct main"
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: flutter_html
 | 
				
			||||||
 | 
					      sha256: "38a2fd702ffdf3243fb7441ab58aa1bc7e6922d95a50db76534de8260638558d"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "3.0.0"
 | 
				
			||||||
  flutter_lints:
 | 
					  flutter_lints:
 | 
				
			||||||
    dependency: "direct dev"
 | 
					    dependency: "direct dev"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -126,11 +142,24 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "5.0.0"
 | 
					    version: "5.0.0"
 | 
				
			||||||
 | 
					  flutter_markdown:
 | 
				
			||||||
 | 
					    dependency: "direct main"
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: flutter_markdown
 | 
				
			||||||
 | 
					      sha256: "04c4722cc36ec5af38acc38ece70d22d3c2123c61305d555750a091517bbe504"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "0.6.23"
 | 
				
			||||||
  flutter_test:
 | 
					  flutter_test:
 | 
				
			||||||
    dependency: "direct dev"
 | 
					    dependency: "direct dev"
 | 
				
			||||||
    description: flutter
 | 
					    description: flutter
 | 
				
			||||||
    source: sdk
 | 
					    source: sdk
 | 
				
			||||||
    version: "0.0.0"
 | 
					    version: "0.0.0"
 | 
				
			||||||
 | 
					  flutter_web_plugins:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description: flutter
 | 
				
			||||||
 | 
					    source: sdk
 | 
				
			||||||
 | 
					    version: "0.0.0"
 | 
				
			||||||
  frontend_server_client:
 | 
					  frontend_server_client:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -147,6 +176,14 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.1.3"
 | 
					    version: "2.1.3"
 | 
				
			||||||
 | 
					  html:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: html
 | 
				
			||||||
 | 
					      sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "0.15.5"
 | 
				
			||||||
  http_multi_server:
 | 
					  http_multi_server:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -219,6 +256,14 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "5.1.1"
 | 
					    version: "5.1.1"
 | 
				
			||||||
 | 
					  list_counter:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: list_counter
 | 
				
			||||||
 | 
					      sha256: c447ae3dfcd1c55f0152867090e67e219d42fe6d4f2807db4bbe8b8d69912237
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "1.0.2"
 | 
				
			||||||
  logging:
 | 
					  logging:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -227,6 +272,14 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.3.0"
 | 
					    version: "1.3.0"
 | 
				
			||||||
 | 
					  markdown:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: markdown
 | 
				
			||||||
 | 
					      sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "7.3.0"
 | 
				
			||||||
  matcher:
 | 
					  matcher:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -291,6 +344,14 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "6.1.0"
 | 
					    version: "6.1.0"
 | 
				
			||||||
 | 
					  plugin_platform_interface:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: plugin_platform_interface
 | 
				
			||||||
 | 
					      sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "2.1.8"
 | 
				
			||||||
  pool:
 | 
					  pool:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -432,6 +493,70 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.4.0"
 | 
					    version: "1.4.0"
 | 
				
			||||||
 | 
					  url_launcher:
 | 
				
			||||||
 | 
					    dependency: "direct main"
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: url_launcher
 | 
				
			||||||
 | 
					      sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "6.3.1"
 | 
				
			||||||
 | 
					  url_launcher_android:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: url_launcher_android
 | 
				
			||||||
 | 
					      sha256: "1d0eae19bd7606ef60fe69ef3b312a437a16549476c42321d5dc1506c9ca3bf4"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "6.3.15"
 | 
				
			||||||
 | 
					  url_launcher_ios:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: url_launcher_ios
 | 
				
			||||||
 | 
					      sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "6.3.2"
 | 
				
			||||||
 | 
					  url_launcher_linux:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: url_launcher_linux
 | 
				
			||||||
 | 
					      sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "3.2.1"
 | 
				
			||||||
 | 
					  url_launcher_macos:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: url_launcher_macos
 | 
				
			||||||
 | 
					      sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "3.2.2"
 | 
				
			||||||
 | 
					  url_launcher_platform_interface:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: url_launcher_platform_interface
 | 
				
			||||||
 | 
					      sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "2.3.2"
 | 
				
			||||||
 | 
					  url_launcher_web:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: url_launcher_web
 | 
				
			||||||
 | 
					      sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "2.4.0"
 | 
				
			||||||
 | 
					  url_launcher_windows:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: url_launcher_windows
 | 
				
			||||||
 | 
					      sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "3.1.4"
 | 
				
			||||||
  vector_math:
 | 
					  vector_math:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -506,4 +631,4 @@ packages:
 | 
				
			|||||||
    version: "3.1.3"
 | 
					    version: "3.1.3"
 | 
				
			||||||
sdks:
 | 
					sdks:
 | 
				
			||||||
  dart: ">=3.7.2 <4.0.0"
 | 
					  dart: ">=3.7.2 <4.0.0"
 | 
				
			||||||
  flutter: ">=3.18.0-18.0.pre.54"
 | 
					  flutter: ">=3.27.0"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -37,6 +37,9 @@ dependencies:
 | 
				
			|||||||
  xml: ^6.5.0
 | 
					  xml: ^6.5.0
 | 
				
			||||||
  intl: ^0.20.2
 | 
					  intl: ^0.20.2
 | 
				
			||||||
  path: ^1.9.1
 | 
					  path: ^1.9.1
 | 
				
			||||||
 | 
					  flutter_markdown: ^0.6.20
 | 
				
			||||||
 | 
					  flutter_html: ^3.0.0-beta.2
 | 
				
			||||||
 | 
					  url_launcher: ^6.3.1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
dev_dependencies:
 | 
					dev_dependencies:
 | 
				
			||||||
  flutter_test:
 | 
					  flutter_test:
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										51
									
								
								release.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								release.sh
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
				
			|||||||
 | 
					#!/bin/bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					echo "Figuring out the tag..."
 | 
				
			||||||
 | 
					TAG=$(git describe --tags --exact-match 2>/dev/null || echo "")
 | 
				
			||||||
 | 
					if [ -z "$TAG" ]; then
 | 
				
			||||||
 | 
					  # Get the latest tag
 | 
				
			||||||
 | 
					  LATEST_TAG=$(git describe --tags $(git rev-list --tags --max-count=1))
 | 
				
			||||||
 | 
					  # Increment the patch version
 | 
				
			||||||
 | 
					  IFS='.' read -r -a VERSION_PARTS <<< "$LATEST_TAG"
 | 
				
			||||||
 | 
					  VERSION_PARTS[2]=$((VERSION_PARTS[2]+1))
 | 
				
			||||||
 | 
					  TAG="${VERSION_PARTS[0]}.${VERSION_PARTS[1]}.${VERSION_PARTS[2]}"
 | 
				
			||||||
 | 
					  # Create a new tag
 | 
				
			||||||
 | 
					  git tag $TAG
 | 
				
			||||||
 | 
					  git push origin $TAG
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					echo "Tag: $TAG"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					echo "Building the thing..."
 | 
				
			||||||
 | 
					flutter build windows --release
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					echo "Creating a release..."
 | 
				
			||||||
 | 
					TOKEN="$GITEA_API_KEY"
 | 
				
			||||||
 | 
					GITEA="https://git.site.quack-lab.dev"
 | 
				
			||||||
 | 
					REPO="dave/flutter-rimworld-modman"
 | 
				
			||||||
 | 
					ZIP="rimworld-modman-${TAG}.zip"
 | 
				
			||||||
 | 
					# Create a release
 | 
				
			||||||
 | 
					RELEASE_RESPONSE=$(curl -s -X POST \
 | 
				
			||||||
 | 
					  -H "Authorization: token $TOKEN" \
 | 
				
			||||||
 | 
					  -H "Accept: application/json" \
 | 
				
			||||||
 | 
					  -H "Content-Type: application/json" \
 | 
				
			||||||
 | 
					  -d '{
 | 
				
			||||||
 | 
					    "tag_name": "'"$TAG"'",
 | 
				
			||||||
 | 
					    "name": "'"$TAG"'",
 | 
				
			||||||
 | 
					    "draft": false,
 | 
				
			||||||
 | 
					    "prerelease": false
 | 
				
			||||||
 | 
					  }' \
 | 
				
			||||||
 | 
					  $GITEA/api/v1/repos/$REPO/releases)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Extract the release ID
 | 
				
			||||||
 | 
					echo $RELEASE_RESPONSE
 | 
				
			||||||
 | 
					RELEASE_ID=$(echo $RELEASE_RESPONSE | awk -F'"id":' '{print $2+0; exit}')
 | 
				
			||||||
 | 
					echo "Release ID: $RELEASE_ID"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					echo "Uploading the things..."
 | 
				
			||||||
 | 
					WINRELEASE="./build/windows/x64/runner/Release/"
 | 
				
			||||||
 | 
					7z a $WINRELEASE/$ZIP $WINRELEASE/*
 | 
				
			||||||
 | 
					curl -X POST \
 | 
				
			||||||
 | 
					  -H "Authorization: token $TOKEN" \
 | 
				
			||||||
 | 
					  -F "attachment=@$WINRELEASE/$ZIP" \
 | 
				
			||||||
 | 
					  "$GITEA/api/v1/repos/$REPO/releases/${RELEASE_ID}/assets?name=$ZIP"
 | 
				
			||||||
 | 
					rm $WINRELEASE/$ZIP
 | 
				
			||||||
@@ -200,10 +200,10 @@ void main() {
 | 
				
			|||||||
    final expected = [
 | 
					    final expected = [
 | 
				
			||||||
      'brrainz.harmony',
 | 
					      'brrainz.harmony',
 | 
				
			||||||
      'ludeon.rimworld',
 | 
					      'ludeon.rimworld',
 | 
				
			||||||
      'ludeon.rimworld.anomaly',
 | 
					 | 
				
			||||||
      'ludeon.rimworld.biotech',
 | 
					 | 
				
			||||||
      'ludeon.rimworld.ideology',
 | 
					 | 
				
			||||||
      'ludeon.rimworld.royalty',
 | 
					      'ludeon.rimworld.royalty',
 | 
				
			||||||
 | 
					      'ludeon.rimworld.ideology',
 | 
				
			||||||
 | 
					      'ludeon.rimworld.biotech',
 | 
				
			||||||
 | 
					      'ludeon.rimworld.anomaly',
 | 
				
			||||||
      'dubwise.rimatomics',
 | 
					      'dubwise.rimatomics',
 | 
				
			||||||
      'jecrell.doorsexpanded',
 | 
					      'jecrell.doorsexpanded',
 | 
				
			||||||
      'dubwise.rimefeller',
 | 
					      'dubwise.rimefeller',
 | 
				
			||||||
@@ -304,10 +304,10 @@ void main() {
 | 
				
			|||||||
      'brrainz.harmony',
 | 
					      'brrainz.harmony',
 | 
				
			||||||
      'ludeon.rimworld',
 | 
					      'ludeon.rimworld',
 | 
				
			||||||
      'bs.betterlog',
 | 
					      'bs.betterlog',
 | 
				
			||||||
      'ludeon.rimworld.anomaly',
 | 
					 | 
				
			||||||
      'ludeon.rimworld.royalty',
 | 
					      'ludeon.rimworld.royalty',
 | 
				
			||||||
      'ludeon.rimworld.ideology',
 | 
					      'ludeon.rimworld.ideology',
 | 
				
			||||||
      'ludeon.rimworld.biotech',
 | 
					      'ludeon.rimworld.biotech',
 | 
				
			||||||
 | 
					      'ludeon.rimworld.anomaly',
 | 
				
			||||||
    ];
 | 
					    ];
 | 
				
			||||||
    expect(order.loadOrder, equals(expected));
 | 
					    expect(order.loadOrder, equals(expected));
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
@@ -712,10 +712,10 @@ void main() {
 | 
				
			|||||||
      'brrainz.harmony',
 | 
					      'brrainz.harmony',
 | 
				
			||||||
      'ludeon.rimworld',
 | 
					      'ludeon.rimworld',
 | 
				
			||||||
      'bs.betterlog',
 | 
					      'bs.betterlog',
 | 
				
			||||||
      'ludeon.rimworld.anomaly',
 | 
					 | 
				
			||||||
      'ludeon.rimworld.biotech',
 | 
					 | 
				
			||||||
      'ludeon.rimworld.ideology',
 | 
					 | 
				
			||||||
      'ludeon.rimworld.royalty',
 | 
					      'ludeon.rimworld.royalty',
 | 
				
			||||||
 | 
					      'ludeon.rimworld.ideology',
 | 
				
			||||||
 | 
					      'ludeon.rimworld.biotech',
 | 
				
			||||||
 | 
					      'ludeon.rimworld.anomaly',
 | 
				
			||||||
      'rah.rbse',
 | 
					      'rah.rbse',
 | 
				
			||||||
      'mlie.usethisinstead',
 | 
					      'mlie.usethisinstead',
 | 
				
			||||||
      'dubwise.rimatomics',
 | 
					      'dubwise.rimatomics',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -182,6 +182,38 @@ void main() {
 | 
				
			|||||||
      expect(result.errors, isEmpty);
 | 
					      expect(result.errors, isEmpty);
 | 
				
			||||||
      expect(result.loadOrder, equals(expected));
 | 
					      expect(result.loadOrder, equals(expected));
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					    test('Expansions should load in the correct order', () {
 | 
				
			||||||
 | 
					      final list = ModList();
 | 
				
			||||||
 | 
					      // Intentionally left barren because that's how we get it out of the box
 | 
				
			||||||
 | 
					      // It is up to generateLoadOrder to fill in the details
 | 
				
			||||||
 | 
					      list.mods = {
 | 
				
			||||||
 | 
					        'ludeon.rimworld': makeDummy().copyWith(id: 'ludeon.rimworld'),
 | 
				
			||||||
 | 
					        'ludeon.rimworld.anomaly': makeDummy().copyWith(
 | 
				
			||||||
 | 
					          id: 'ludeon.rimworld.anomaly',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        'ludeon.rimworld.ideology': makeDummy().copyWith(
 | 
				
			||||||
 | 
					          id: 'ludeon.rimworld.ideology',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        'ludeon.rimworld.biotech': makeDummy().copyWith(
 | 
				
			||||||
 | 
					          id: 'ludeon.rimworld.biotech',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        'ludeon.rimworld.royalty': makeDummy().copyWith(
 | 
				
			||||||
 | 
					          id: 'ludeon.rimworld.royalty',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      list.enableAll();
 | 
				
			||||||
 | 
					      final result = list.generateLoadOrder();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      final expected = [
 | 
				
			||||||
 | 
					        'ludeon.rimworld',
 | 
				
			||||||
 | 
					        'ludeon.rimworld.royalty',
 | 
				
			||||||
 | 
					        'ludeon.rimworld.ideology',
 | 
				
			||||||
 | 
					        'ludeon.rimworld.biotech',
 | 
				
			||||||
 | 
					        'ludeon.rimworld.anomaly',
 | 
				
			||||||
 | 
					      ];
 | 
				
			||||||
 | 
					      expect(result.errors, isEmpty);
 | 
				
			||||||
 | 
					      expect(result.loadOrder, equals(expected));
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  group('Test loadRequired', () {
 | 
					  group('Test loadRequired', () {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,6 +6,9 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#include "generated_plugin_registrant.h"
 | 
					#include "generated_plugin_registrant.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <url_launcher_windows/url_launcher_windows.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
 | 
					void RegisterPlugins(flutter::PluginRegistry* registry) {
 | 
				
			||||||
 | 
					  UrlLauncherWindowsRegisterWithRegistrar(
 | 
				
			||||||
 | 
					      registry->GetRegistrarForPlugin("UrlLauncherWindows"));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,7 @@
 | 
				
			|||||||
#
 | 
					#
 | 
				
			||||||
 | 
					
 | 
				
			||||||
list(APPEND FLUTTER_PLUGIN_LIST
 | 
					list(APPEND FLUTTER_PLUGIN_LIST
 | 
				
			||||||
 | 
					  url_launcher_windows
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
 | 
					list(APPEND FLUTTER_FFI_PLUGIN_LIST
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,8 +25,9 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
 | 
				
			|||||||
  project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
 | 
					  project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  FlutterWindow window(project);
 | 
					  FlutterWindow window(project);
 | 
				
			||||||
  Win32Window::Point origin(10, 10);
 | 
					  Win32Window::Size size(1920, 1080);
 | 
				
			||||||
  Win32Window::Size size(1280, 720);
 | 
					  Win32Window::Point origin((GetSystemMetrics(SM_CXSCREEN) - size.width) / 2,
 | 
				
			||||||
 | 
					                            (GetSystemMetrics(SM_CYSCREEN) - size.height) / 2);
 | 
				
			||||||
  if (!window.Create(L"rimworld_modman", origin, size)) {
 | 
					  if (!window.Create(L"rimworld_modman", origin, size)) {
 | 
				
			||||||
    return EXIT_FAILURE;
 | 
					    return EXIT_FAILURE;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user