Compare commits
24 Commits
efe74b404e
...
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 | |||
| 02cfe01ae0 | |||
| 1dabc804b4 | |||
| a6cfd3e16e |
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
215
lib/mod.dart
215
lib/mod.dart
@@ -47,29 +47,40 @@ class Mod {
|
|||||||
this.enabled = false,
|
this.enabled = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
static Mod fromDirectory(String path, {bool skipFileCount = false}) {
|
int get tier {
|
||||||
final logger = Logger.instance;
|
if (isBaseGame) return 0;
|
||||||
final stopwatch = Stopwatch()..start();
|
if (isExpansion) return 1;
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
logger.info('Attempting to load mod from directory: $path');
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'Mod{name: $name, id: $id, path: $path, dependencies: $dependencies, loadAfter: $loadAfter, loadBefore: $loadBefore, incompatibilities: $incompatibilities, size: $size, isBaseGame: $isBaseGame, isExpansion: $isExpansion}';
|
||||||
|
}
|
||||||
|
|
||||||
|
static Mod fromDirectory(String path, {bool skipFileCount = false}) {
|
||||||
|
// final logger = Logger.instance;
|
||||||
|
// final stopwatch = Stopwatch()..start();
|
||||||
|
|
||||||
|
// 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',
|
||||||
);
|
);
|
||||||
@@ -78,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',
|
||||||
);
|
);
|
||||||
@@ -91,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',
|
||||||
);
|
);
|
||||||
@@ -110,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',
|
||||||
);
|
);
|
||||||
@@ -123,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 = [];
|
||||||
@@ -145,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 = [];
|
||||||
@@ -161,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 {
|
||||||
@@ -177,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 = [];
|
||||||
@@ -193,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,
|
||||||
@@ -250,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,49 +382,154 @@ 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...');
|
||||||
for (final mod in activeMods.values) {
|
for (final mod in activeMods.values) {
|
||||||
|
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()}');
|
||||||
for (final incomp in mod.incompatibilities) {
|
for (final incomp in mod.incompatibilities) {
|
||||||
if (activeMods.containsKey(incomp)) {
|
if (activeMods.containsKey(incomp)) {
|
||||||
loadOrder.errors.add(
|
loadOrder.errors.add(
|
||||||
'Incompatibility detected: ${mod.id} is incompatible with $incomp',
|
'Incompatibility detected: ${mod.id} is incompatible with $incomp',
|
||||||
);
|
);
|
||||||
|
logger.warning(
|
||||||
|
'Incompatibility detected: ${mod.id} is incompatible with $incomp',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.info('No incompatibility found for: $incomp');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (final dep in mod.dependencies) {
|
||||||
|
if (!activeMods.containsKey(dep)) {
|
||||||
|
loadOrder.errors.add('Missing dependency: ${mod.id} requires $dep');
|
||||||
|
logger.warning('Missing dependency: ${mod.id} requires $dep');
|
||||||
|
} else {
|
||||||
|
logger.info('Dependency found: ${mod.id} requires $dep');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info('Adding active mods to load order...');
|
||||||
loadOrder.order.addAll(activeMods.values.toList());
|
loadOrder.order.addAll(activeMods.values.toList());
|
||||||
|
logger.info(
|
||||||
|
'Active mods added: ${loadOrder.order.map((mod) => mod.id).join(', ')}',
|
||||||
|
);
|
||||||
|
|
||||||
loadOrder.order.sort((a, b) {
|
final modMap = {for (final mod in loadOrder.order) mod.id: mod};
|
||||||
if (a.isBaseGame && !b.isBaseGame) return -1;
|
final graph = <String, Set<String>>{};
|
||||||
if (!a.isBaseGame && b.isBaseGame) return 1;
|
final inDegree = <String, int>{};
|
||||||
if (a.isExpansion && !b.isExpansion) return -1;
|
|
||||||
if (!a.isExpansion && b.isExpansion) return 1;
|
// Step 1: Initialize graph and inDegree
|
||||||
|
for (final mod in loadOrder.order) {
|
||||||
|
graph[mod.id] = <String>{};
|
||||||
|
inDegree[mod.id] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Build dependency graph
|
||||||
|
void addEdge(String from, String to) {
|
||||||
|
final fromMod = modMap[from];
|
||||||
|
if (fromMod == null) {
|
||||||
|
logger.warning('Missing dependency: $from');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final toMod = modMap[to];
|
||||||
|
if (toMod == null) {
|
||||||
|
logger.warning('Missing dependency: $to');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (graph[from]!.add(to)) {
|
||||||
|
inDegree[to] = inDegree[to]! + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final mod in loadOrder.order) {
|
||||||
|
for (final target in mod.loadBefore) {
|
||||||
|
addEdge(mod.id, target);
|
||||||
|
}
|
||||||
|
for (final target in mod.loadAfter) {
|
||||||
|
addEdge(target, mod.id);
|
||||||
|
}
|
||||||
|
for (final dep in mod.dependencies) {
|
||||||
|
addEdge(dep, mod.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Calculate tiers dynamically with cross-tier dependencies
|
||||||
|
final tiers = <Mod, int>{};
|
||||||
|
for (final mod in loadOrder.order) {
|
||||||
|
int tier = 2; // Default to Tier 2
|
||||||
|
|
||||||
|
// Check if mod loads before any base game mod (Tier 0)
|
||||||
|
final loadsBeforeBase = mod.loadBefore.any(
|
||||||
|
(id) => modMap[id]?.isBaseGame ?? false,
|
||||||
|
);
|
||||||
|
if (mod.isBaseGame || loadsBeforeBase) {
|
||||||
|
tier = 0;
|
||||||
|
} else {
|
||||||
|
// Check if mod loads before any expansion (Tier 1)
|
||||||
|
final loadsBeforeExpansion = mod.loadBefore.any(
|
||||||
|
(id) => modMap[id]?.isExpansion ?? false,
|
||||||
|
);
|
||||||
|
if (mod.isExpansion || loadsBeforeExpansion) {
|
||||||
|
tier = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tiers[mod] = tier;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Global priority queue (tier ascending, size descending)
|
||||||
|
final pq = PriorityQueue<Mod>((a, b) {
|
||||||
|
final tierA = tiers[a]!;
|
||||||
|
final tierB = tiers[b]!;
|
||||||
|
if (tierA != tierB) return tierA.compareTo(tierB);
|
||||||
return b.size.compareTo(a.size);
|
return b.size.compareTo(a.size);
|
||||||
});
|
});
|
||||||
|
|
||||||
Map<String, List<Mod>> relations = {};
|
// Initialize queue with mods having inDegree 0
|
||||||
for (int i = loadOrder.order.length - 1; i >= 0; i--) {
|
for (final mod in loadOrder.order) {
|
||||||
final mod = loadOrder!.order[i];
|
if (inDegree[mod.id] == 0) {
|
||||||
logger.info('Processing mod: ${mod.id}');
|
pq.add(mod);
|
||||||
loadOrder = shuffleMod(mod, loadOrder, relations);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return loadOrder!;
|
final orderedMods = <Mod>[];
|
||||||
|
while (pq.isNotEmpty) {
|
||||||
|
final current = pq.removeFirst();
|
||||||
|
orderedMods.add(current);
|
||||||
|
|
||||||
|
for (final neighborId in graph[current.id]!) {
|
||||||
|
inDegree[neighborId] = inDegree[neighborId]! - 1;
|
||||||
|
if (inDegree[neighborId] == 0) {
|
||||||
|
final neighbor = modMap[neighborId]!;
|
||||||
|
pq.add(neighbor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (orderedMods.length != loadOrder.order.length) {
|
||||||
|
loadOrder.errors.add('Cycle detected in dependencies');
|
||||||
|
logger.warning(
|
||||||
|
'Cycle detected in dependencies: expected ${loadOrder.order.length}, got ${orderedMods.length}.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadOrder.order = orderedMods;
|
||||||
|
logger.info(
|
||||||
|
'Load order generated successfully with ${loadOrder.order.length} mods.',
|
||||||
|
);
|
||||||
|
for (final mod in loadOrder.order) {
|
||||||
|
logger.info('Mod: ${mod.toString()}');
|
||||||
|
}
|
||||||
|
return loadOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The point of relations and the recursive call is to handle the case where
|
// The point of relations and the recursive call is to handle the case where
|
||||||
@@ -455,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _expansionNameFromId(String id) {
|
// Replace the errors with the filtered list
|
||||||
final parts = id.split('.');
|
newLoadOrder.errors.clear();
|
||||||
if (parts.length < 3) return id;
|
newLoadOrder.errors.addAll(filteredErrors);
|
||||||
|
}
|
||||||
final expansionPart = parts[2];
|
|
||||||
return expansionPart.substring(0, 1).toUpperCase() +
|
return newLoadOrder;
|
||||||
expansionPart.substring(1);
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
||||||
@@ -210,8 +210,8 @@ void main() {
|
|||||||
'neronix17.toolbox',
|
'neronix17.toolbox',
|
||||||
'automatic.bionicicons',
|
'automatic.bionicicons',
|
||||||
'lwm.deepstorage',
|
'lwm.deepstorage',
|
||||||
'dubwise.dubsmintmenus',
|
|
||||||
'dubwise.dubsmintminimap',
|
'dubwise.dubsmintminimap',
|
||||||
|
'dubwise.dubsmintmenus',
|
||||||
'brrainz.justignoremepassing',
|
'brrainz.justignoremepassing',
|
||||||
];
|
];
|
||||||
expect(result.errors, isEmpty);
|
expect(result.errors, isEmpty);
|
||||||
@@ -304,10 +304,440 @@ 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',
|
||||||
|
];
|
||||||
|
expect(order.loadOrder, equals(expected));
|
||||||
|
});
|
||||||
|
test('Expansions should load before most mods', () {
|
||||||
|
final list = ModList();
|
||||||
|
list.mods = {
|
||||||
|
'bs.betterlog': makeDummy().copyWith(
|
||||||
|
id: 'bs.betterlog',
|
||||||
|
name: 'Better Log - Fix your errors',
|
||||||
|
enabled: true,
|
||||||
|
size: 69,
|
||||||
|
dependencies: [],
|
||||||
|
loadAfter: [
|
||||||
|
'brrainz.harmony',
|
||||||
|
'me.samboycoding.betterloading',
|
||||||
|
'zetrith.prepatcher',
|
||||||
|
'ludeon.rimworld',
|
||||||
|
],
|
||||||
|
loadBefore: [
|
||||||
|
'ludeon.rimworld.royalty',
|
||||||
|
'ludeon.rimworld.ideology',
|
||||||
|
'ludeon.rimworld.biotech',
|
||||||
|
'ludeon.rimworld.anomaly',
|
||||||
|
'bs.performance',
|
||||||
|
'unlimitedhugs.hugslib',
|
||||||
|
],
|
||||||
|
incompatibilities: [],
|
||||||
|
),
|
||||||
|
'zetrith.prepatcher': makeDummy().copyWith(
|
||||||
|
id: 'zetrith.prepatcher',
|
||||||
|
name: 'Prepatcher',
|
||||||
|
path:
|
||||||
|
'C:/Users/Administrator/Seafile/Games-RimWorld/294100\\2934420800',
|
||||||
|
dependencies: [],
|
||||||
|
loadAfter: [],
|
||||||
|
loadBefore: ['ludeon.rimworld', 'brrainz.harmony'],
|
||||||
|
incompatibilities: [],
|
||||||
|
size: 21,
|
||||||
|
isBaseGame: false,
|
||||||
|
isExpansion: false,
|
||||||
|
),
|
||||||
|
'brrainz.harmony': makeDummy().copyWith(
|
||||||
|
id: 'brrainz.harmony',
|
||||||
|
name: 'Harmony',
|
||||||
|
path:
|
||||||
|
'C:/Users/Administrator/Seafile/Games-RimWorld/294100\\2009463077',
|
||||||
|
dependencies: [],
|
||||||
|
loadAfter: [],
|
||||||
|
loadBefore: ['ludeon.rimworld'],
|
||||||
|
incompatibilities: [],
|
||||||
|
size: 17,
|
||||||
|
isBaseGame: false,
|
||||||
|
isExpansion: false,
|
||||||
|
),
|
||||||
|
'ludeon.rimworld': makeDummy().copyWith(
|
||||||
|
id: 'ludeon.rimworld',
|
||||||
|
name: 'RimWorld',
|
||||||
|
path: '',
|
||||||
|
dependencies: [],
|
||||||
|
loadAfter: [],
|
||||||
|
loadBefore: [],
|
||||||
|
incompatibilities: [],
|
||||||
|
size: 0,
|
||||||
|
isBaseGame: true,
|
||||||
|
isExpansion: false,
|
||||||
|
),
|
||||||
|
'rah.rbse': makeDummy().copyWith(
|
||||||
|
id: 'rah.rbse',
|
||||||
|
name: 'RBSE',
|
||||||
|
path: 'C:/Users/Administrator/Seafile/Games-RimWorld/294100\\850429707',
|
||||||
|
dependencies: [],
|
||||||
|
loadAfter: [],
|
||||||
|
loadBefore: [],
|
||||||
|
incompatibilities: [],
|
||||||
|
size: 1729,
|
||||||
|
isBaseGame: false,
|
||||||
|
isExpansion: false,
|
||||||
|
),
|
||||||
|
'mlie.usethisinstead': makeDummy().copyWith(
|
||||||
|
id: 'mlie.usethisinstead',
|
||||||
|
name: 'Use This Instead',
|
||||||
|
path:
|
||||||
|
'C:/Users/Administrator/Seafile/Games-RimWorld/294100\\3396308787',
|
||||||
|
dependencies: [],
|
||||||
|
loadAfter: ['brrainz.harmony'],
|
||||||
|
loadBefore: [],
|
||||||
|
incompatibilities: [],
|
||||||
|
size: 1651,
|
||||||
|
isBaseGame: false,
|
||||||
|
isExpansion: false,
|
||||||
|
),
|
||||||
|
'dubwise.rimatomics': makeDummy().copyWith(
|
||||||
|
id: 'dubwise.rimatomics',
|
||||||
|
name: 'Dubs Rimatomics',
|
||||||
|
path:
|
||||||
|
'C:/Users/Administrator/Seafile/Games-RimWorld/294100\\1127530465',
|
||||||
|
dependencies: [],
|
||||||
|
loadAfter: [],
|
||||||
|
loadBefore: [],
|
||||||
|
incompatibilities: [],
|
||||||
|
size: 1563,
|
||||||
|
isBaseGame: false,
|
||||||
|
isExpansion: false,
|
||||||
|
),
|
||||||
|
'jecrell.doorsexpanded': makeDummy().copyWith(
|
||||||
|
id: 'jecrell.doorsexpanded',
|
||||||
|
name: 'Doors Expanded',
|
||||||
|
path:
|
||||||
|
'C:/Users/Administrator/Seafile/Games-RimWorld/294100\\1316188771',
|
||||||
|
dependencies: [],
|
||||||
|
loadAfter: [],
|
||||||
|
loadBefore: [],
|
||||||
|
incompatibilities: [],
|
||||||
|
size: 765,
|
||||||
|
isBaseGame: false,
|
||||||
|
isExpansion: false,
|
||||||
|
),
|
||||||
|
'balistafreak.stopdropandroll': makeDummy().copyWith(
|
||||||
|
id: 'balistafreak.stopdropandroll',
|
||||||
|
name: 'Stop, Drop, And Roll! [BAL]',
|
||||||
|
path:
|
||||||
|
'C:/Users/Administrator/Seafile/Games-RimWorld/294100\\2362707956',
|
||||||
|
dependencies: [],
|
||||||
|
loadAfter: [],
|
||||||
|
loadBefore: [],
|
||||||
|
incompatibilities: [],
|
||||||
|
size: 755,
|
||||||
|
isBaseGame: false,
|
||||||
|
isExpansion: false,
|
||||||
|
),
|
||||||
|
'fluffy.animaltab': makeDummy().copyWith(
|
||||||
|
id: 'fluffy.animaltab',
|
||||||
|
name: 'Animal Tab',
|
||||||
|
path: 'C:/Users/Administrator/Seafile/Games-RimWorld/294100\\712141500',
|
||||||
|
dependencies: ['brrainz.harmony'],
|
||||||
|
loadAfter: [],
|
||||||
|
loadBefore: [],
|
||||||
|
incompatibilities: [],
|
||||||
|
size: 752,
|
||||||
|
isBaseGame: false,
|
||||||
|
isExpansion: false,
|
||||||
|
),
|
||||||
|
'gt.sam.glittertech': makeDummy().copyWith(
|
||||||
|
id: 'gt.sam.glittertech',
|
||||||
|
name: 'Glitter Tech Classic',
|
||||||
|
path: 'C:/Users/Administrator/Seafile/Games-RimWorld/294100\\725576127',
|
||||||
|
dependencies: [],
|
||||||
|
loadAfter: [],
|
||||||
|
loadBefore: [],
|
||||||
|
incompatibilities: [],
|
||||||
|
size: 747,
|
||||||
|
isBaseGame: false,
|
||||||
|
isExpansion: false,
|
||||||
|
),
|
||||||
|
'dubwise.rimefeller': makeDummy().copyWith(
|
||||||
|
id: 'dubwise.rimefeller',
|
||||||
|
name: 'Rimefeller',
|
||||||
|
path:
|
||||||
|
'C:/Users/Administrator/Seafile/Games-RimWorld/294100\\1321849735',
|
||||||
|
dependencies: [],
|
||||||
|
loadAfter: [],
|
||||||
|
loadBefore: [],
|
||||||
|
incompatibilities: [],
|
||||||
|
size: 744,
|
||||||
|
isBaseGame: false,
|
||||||
|
isExpansion: false,
|
||||||
|
),
|
||||||
|
'darthcy.misc.morebetterdeepdrill': makeDummy().copyWith(
|
||||||
|
id: 'darthcy.misc.morebetterdeepdrill',
|
||||||
|
name: 'More and Better Deep Drill',
|
||||||
|
path:
|
||||||
|
'C:/Users/Administrator/Seafile/Games-RimWorld/294100\\3378527302',
|
||||||
|
dependencies: [],
|
||||||
|
loadAfter: ['brrainz.harmony', 'spdskatr.projectrimfactory'],
|
||||||
|
loadBefore: [],
|
||||||
|
incompatibilities: [],
|
||||||
|
size: 738,
|
||||||
|
isBaseGame: false,
|
||||||
|
isExpansion: false,
|
||||||
|
),
|
||||||
|
'haplo.miscellaneous.training': makeDummy().copyWith(
|
||||||
|
id: 'haplo.miscellaneous.training',
|
||||||
|
name: 'Misc. Training',
|
||||||
|
path: 'C:/Users/Administrator/Seafile/Games-RimWorld/294100\\717575199',
|
||||||
|
dependencies: [],
|
||||||
|
loadAfter: ['haplo.miscellaneous.core'],
|
||||||
|
loadBefore: [],
|
||||||
|
incompatibilities: ['haplo.miscellaneous.trainingnotask'],
|
||||||
|
size: 733,
|
||||||
|
isBaseGame: false,
|
||||||
|
isExpansion: false,
|
||||||
|
),
|
||||||
|
'linkolas.stabilize': makeDummy().copyWith(
|
||||||
|
id: 'linkolas.stabilize',
|
||||||
|
name: 'Stabilize',
|
||||||
|
path:
|
||||||
|
'C:/Users/Administrator/Seafile/Games-RimWorld/294100\\2023407836',
|
||||||
|
dependencies: [],
|
||||||
|
loadAfter: [],
|
||||||
|
loadBefore: [],
|
||||||
|
incompatibilities: [],
|
||||||
|
size: 627,
|
||||||
|
isBaseGame: false,
|
||||||
|
isExpansion: false,
|
||||||
|
),
|
||||||
|
'dubwise.dubsperformanceanalyzer.steam': makeDummy().copyWith(
|
||||||
|
id: 'dubwise.dubsperformanceanalyzer.steam',
|
||||||
|
name: 'Dubs Performance Analyzer',
|
||||||
|
path:
|
||||||
|
'C:/Users/Administrator/Seafile/Games-RimWorld/294100\\2038874626',
|
||||||
|
dependencies: [],
|
||||||
|
loadAfter: [],
|
||||||
|
loadBefore: [],
|
||||||
|
incompatibilities: [],
|
||||||
|
size: 570,
|
||||||
|
isBaseGame: false,
|
||||||
|
isExpansion: false,
|
||||||
|
),
|
||||||
|
'memegoddess.searchanddestroy': makeDummy().copyWith(
|
||||||
|
id: 'memegoddess.searchanddestroy',
|
||||||
|
name: 'Search and Destroy (Unofficial Update)',
|
||||||
|
path:
|
||||||
|
'C:/Users/Administrator/Seafile/Games-RimWorld/294100\\3232242247',
|
||||||
|
dependencies: [],
|
||||||
|
loadAfter: [],
|
||||||
|
loadBefore: [],
|
||||||
|
incompatibilities: [],
|
||||||
|
size: 495,
|
||||||
|
isBaseGame: false,
|
||||||
|
isExpansion: false,
|
||||||
|
),
|
||||||
|
'gogatio.mechanoidupgrades': makeDummy().copyWith(
|
||||||
|
id: 'gogatio.mechanoidupgrades',
|
||||||
|
name: 'Mechanoid Upgrades',
|
||||||
|
path:
|
||||||
|
'C:/Users/Administrator/Seafile/Games-RimWorld/294100\\3365118555',
|
||||||
|
dependencies: [],
|
||||||
|
loadAfter: [],
|
||||||
|
loadBefore: [],
|
||||||
|
incompatibilities: [],
|
||||||
|
size: 487,
|
||||||
|
isBaseGame: false,
|
||||||
|
isExpansion: false,
|
||||||
|
),
|
||||||
|
'issaczhuang.muzzleflash': makeDummy().copyWith(
|
||||||
|
id: 'issaczhuang.muzzleflash',
|
||||||
|
name: 'Muzzle Flash',
|
||||||
|
path:
|
||||||
|
'C:/Users/Administrator/Seafile/Games-RimWorld/294100\\2917732219',
|
||||||
|
dependencies: [],
|
||||||
|
loadAfter: ['ludeon.rimworld'],
|
||||||
|
loadBefore: [],
|
||||||
|
incompatibilities: [],
|
||||||
|
size: 431,
|
||||||
|
isBaseGame: false,
|
||||||
|
isExpansion: false,
|
||||||
|
),
|
||||||
|
'smashphil.vehicleframework': makeDummy().copyWith(
|
||||||
|
id: 'smashphil.vehicleframework',
|
||||||
|
name: 'Vehicle Framework',
|
||||||
|
path:
|
||||||
|
'C:/Users/Administrator/Seafile/Games-RimWorld/294100\\3014915404',
|
||||||
|
dependencies: [],
|
||||||
|
loadAfter: ['brrainz.harmony'],
|
||||||
|
loadBefore: [],
|
||||||
|
incompatibilities: [],
|
||||||
|
size: 426,
|
||||||
|
isBaseGame: false,
|
||||||
|
isExpansion: false,
|
||||||
|
),
|
||||||
|
'cabbage.rimcities': makeDummy().copyWith(
|
||||||
|
id: 'cabbage.rimcities',
|
||||||
|
name: 'RimCities',
|
||||||
|
path:
|
||||||
|
'C:/Users/Administrator/Seafile/Games-RimWorld/294100\\1775170117',
|
||||||
|
dependencies: [],
|
||||||
|
loadAfter: [],
|
||||||
|
loadBefore: [],
|
||||||
|
incompatibilities: [],
|
||||||
|
size: 421,
|
||||||
|
isBaseGame: false,
|
||||||
|
isExpansion: false,
|
||||||
|
),
|
||||||
|
'vis.staticquality': makeDummy().copyWith(
|
||||||
|
id: 'vis.staticquality',
|
||||||
|
name: 'Static Quality',
|
||||||
|
path:
|
||||||
|
'C:/Users/Administrator/Seafile/Games-RimWorld/294100\\2801204005',
|
||||||
|
dependencies: [],
|
||||||
|
loadAfter: [],
|
||||||
|
loadBefore: [],
|
||||||
|
incompatibilities: [],
|
||||||
|
size: 385,
|
||||||
|
isBaseGame: false,
|
||||||
|
isExpansion: false,
|
||||||
|
),
|
||||||
|
'automatic.bionicicons': makeDummy().copyWith(
|
||||||
|
id: 'automatic.bionicicons',
|
||||||
|
name: 'Bionic icons',
|
||||||
|
path:
|
||||||
|
'C:/Users/Administrator/Seafile/Games-RimWorld/294100\\1677616980',
|
||||||
|
dependencies: [],
|
||||||
|
loadAfter: [],
|
||||||
|
loadBefore: [],
|
||||||
|
incompatibilities: [],
|
||||||
|
size: 365,
|
||||||
|
isBaseGame: false,
|
||||||
|
isExpansion: false,
|
||||||
|
),
|
||||||
|
'vanillaexpanded.vanillatraitsexpanded': makeDummy().copyWith(
|
||||||
|
id: 'vanillaexpanded.vanillatraitsexpanded',
|
||||||
|
name: 'Vanilla Traits Expanded',
|
||||||
|
path:
|
||||||
|
'C:/Users/Administrator/Seafile/Games-RimWorld/294100\\2296404655',
|
||||||
|
dependencies: [],
|
||||||
|
loadAfter: [],
|
||||||
|
loadBefore: [],
|
||||||
|
incompatibilities: [],
|
||||||
|
size: 338,
|
||||||
|
isBaseGame: false,
|
||||||
|
isExpansion: false,
|
||||||
|
),
|
||||||
|
'tk421storm.ragdoll': makeDummy().copyWith(
|
||||||
|
id: 'tk421storm.ragdoll',
|
||||||
|
name: 'Ragdoll Physics',
|
||||||
|
path:
|
||||||
|
'C:/Users/Administrator/Seafile/Games-RimWorld/294100\\3179116177',
|
||||||
|
dependencies: [],
|
||||||
|
loadAfter: [],
|
||||||
|
loadBefore: [],
|
||||||
|
incompatibilities: [],
|
||||||
|
size: 329,
|
||||||
|
isBaseGame: false,
|
||||||
|
isExpansion: false,
|
||||||
|
),
|
||||||
|
'andromeda.nicehealthtab': makeDummy().copyWith(
|
||||||
|
id: 'andromeda.nicehealthtab',
|
||||||
|
name: 'Nice Health Tab',
|
||||||
|
path:
|
||||||
|
'C:/Users/Administrator/Seafile/Games-RimWorld/294100\\3328729902',
|
||||||
|
dependencies: [],
|
||||||
|
loadAfter: [],
|
||||||
|
loadBefore: [],
|
||||||
|
incompatibilities: [],
|
||||||
|
size: 319,
|
||||||
|
isBaseGame: false,
|
||||||
|
isExpansion: false,
|
||||||
|
),
|
||||||
|
'ludeon.rimworld.anomaly': makeDummy().copyWith(
|
||||||
|
id: 'ludeon.rimworld.anomaly',
|
||||||
|
name: 'RimWorld Anomaly',
|
||||||
|
path: '',
|
||||||
|
dependencies: [],
|
||||||
|
loadAfter: ['ludeon.rimworld'],
|
||||||
|
loadBefore: [],
|
||||||
|
incompatibilities: [],
|
||||||
|
size: 0,
|
||||||
|
isBaseGame: false,
|
||||||
|
isExpansion: true,
|
||||||
|
),
|
||||||
|
'ludeon.rimworld.biotech': makeDummy().copyWith(
|
||||||
|
id: 'ludeon.rimworld.biotech',
|
||||||
|
name: 'RimWorld Biotech',
|
||||||
|
path: '',
|
||||||
|
dependencies: [],
|
||||||
|
loadAfter: ['ludeon.rimworld'],
|
||||||
|
loadBefore: [],
|
||||||
|
incompatibilities: [],
|
||||||
|
size: 0,
|
||||||
|
isBaseGame: false,
|
||||||
|
isExpansion: true,
|
||||||
|
),
|
||||||
|
'ludeon.rimworld.ideology': makeDummy().copyWith(
|
||||||
|
id: 'ludeon.rimworld.ideology',
|
||||||
|
name: 'RimWorld Ideology',
|
||||||
|
path: '',
|
||||||
|
dependencies: [],
|
||||||
|
loadAfter: ['ludeon.rimworld'],
|
||||||
|
loadBefore: [],
|
||||||
|
incompatibilities: [],
|
||||||
|
size: 0,
|
||||||
|
isBaseGame: false,
|
||||||
|
isExpansion: true,
|
||||||
|
),
|
||||||
|
'ludeon.rimworld.royalty': makeDummy().copyWith(
|
||||||
|
id: 'ludeon.rimworld.royalty',
|
||||||
|
name: 'RimWorld Royalty',
|
||||||
|
path: '',
|
||||||
|
dependencies: [],
|
||||||
|
loadAfter: ['ludeon.rimworld'],
|
||||||
|
loadBefore: [],
|
||||||
|
incompatibilities: [],
|
||||||
|
size: 0,
|
||||||
|
isBaseGame: false,
|
||||||
|
isExpansion: true,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
list.enableAll();
|
||||||
|
final order = list.generateLoadOrder();
|
||||||
|
final expected = [
|
||||||
|
'zetrith.prepatcher',
|
||||||
|
'brrainz.harmony',
|
||||||
|
'ludeon.rimworld',
|
||||||
|
'bs.betterlog',
|
||||||
|
'ludeon.rimworld.royalty',
|
||||||
|
'ludeon.rimworld.ideology',
|
||||||
|
'ludeon.rimworld.biotech',
|
||||||
|
'ludeon.rimworld.anomaly',
|
||||||
|
'rah.rbse',
|
||||||
|
'mlie.usethisinstead',
|
||||||
|
'dubwise.rimatomics',
|
||||||
|
'jecrell.doorsexpanded',
|
||||||
|
'balistafreak.stopdropandroll',
|
||||||
|
'fluffy.animaltab',
|
||||||
|
'gt.sam.glittertech',
|
||||||
|
'dubwise.rimefeller',
|
||||||
|
'darthcy.misc.morebetterdeepdrill',
|
||||||
|
'haplo.miscellaneous.training',
|
||||||
|
'linkolas.stabilize',
|
||||||
|
'dubwise.dubsperformanceanalyzer.steam',
|
||||||
|
'memegoddess.searchanddestroy',
|
||||||
|
'gogatio.mechanoidupgrades',
|
||||||
|
'issaczhuang.muzzleflash',
|
||||||
|
'smashphil.vehicleframework',
|
||||||
|
'cabbage.rimcities',
|
||||||
|
'vis.staticquality',
|
||||||
|
'automatic.bionicicons',
|
||||||
|
'vanillaexpanded.vanillatraitsexpanded',
|
||||||
|
'tk421storm.ragdoll',
|
||||||
|
'andromeda.nicehealthtab',
|
||||||
];
|
];
|
||||||
expect(order.loadOrder, equals(expected));
|
expect(order.loadOrder, equals(expected));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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', () {
|
||||||
@@ -245,7 +277,7 @@ void main() {
|
|||||||
final result = list.loadRequired();
|
final result = list.loadRequired();
|
||||||
|
|
||||||
// We say the mods are incompatible but load them anyway, who are we to decide what isn't loaded?
|
// We say the mods are incompatible but load them anyway, who are we to decide what isn't loaded?
|
||||||
final expected = ['incompatible', 'harmony', 'prepatcher'];
|
final expected = ['harmony', 'prepatcher', 'incompatible'];
|
||||||
expect(result.errors, isNotEmpty);
|
expect(result.errors, isNotEmpty);
|
||||||
expect(result.errors.any((e) => e.contains('incompatible')), isTrue);
|
expect(result.errors.any((e) => e.contains('incompatible')), isTrue);
|
||||||
expect(result.errors.any((e) => e.contains('harmony')), isTrue);
|
expect(result.errors.any((e) => e.contains('harmony')), isTrue);
|
||||||
@@ -302,7 +334,7 @@ void main() {
|
|||||||
|
|
||||||
// We try to not disable mods...... But cyclic dependencies are just hell
|
// We try to not disable mods...... But cyclic dependencies are just hell
|
||||||
// Can not handle it
|
// Can not handle it
|
||||||
final expected = ['modB', 'modA', 'modC'];
|
final expected = [];
|
||||||
expect(result.errors, isNotEmpty);
|
expect(result.errors, isNotEmpty);
|
||||||
expect(result.errors.any((e) => e.contains('modA')), isTrue);
|
expect(result.errors.any((e) => e.contains('modA')), isTrue);
|
||||||
expect(result.errors.any((e) => e.contains('modB')), isTrue);
|
expect(result.errors.any((e) => e.contains('modB')), isTrue);
|
||||||
@@ -487,7 +519,7 @@ void main() {
|
|||||||
list.enableAll();
|
list.enableAll();
|
||||||
|
|
||||||
final result = list.generateLoadOrder();
|
final result = list.generateLoadOrder();
|
||||||
final expected = ['modA', 'modB'];
|
final expected = ['modB', 'modA'];
|
||||||
expect(result.errors, isNotEmpty);
|
expect(result.errors, isNotEmpty);
|
||||||
expect(result.errors.any((e) => e.contains('missing1')), isTrue);
|
expect(result.errors.any((e) => e.contains('missing1')), isTrue);
|
||||||
expect(result.errors.any((e) => e.contains('missing2')), isTrue);
|
expect(result.errors.any((e) => e.contains('missing2')), isTrue);
|
||||||
@@ -548,7 +580,7 @@ void main() {
|
|||||||
list.enableAll();
|
list.enableAll();
|
||||||
|
|
||||||
final result = list.generateLoadOrder();
|
final result = list.generateLoadOrder();
|
||||||
final expected = ['modA', 'modB'];
|
final expected = ['modB', 'modA'];
|
||||||
expect(result.errors, isNotEmpty);
|
expect(result.errors, isNotEmpty);
|
||||||
expect(result.errors.any((e) => e.contains('incompatible')), isTrue);
|
expect(result.errors.any((e) => e.contains('incompatible')), isTrue);
|
||||||
expect(result.errors.any((e) => e.contains('modA')), isTrue);
|
expect(result.errors.any((e) => e.contains('modA')), isTrue);
|
||||||
@@ -570,7 +602,7 @@ void main() {
|
|||||||
list.enableAll();
|
list.enableAll();
|
||||||
|
|
||||||
final result = list.generateLoadOrder();
|
final result = list.generateLoadOrder();
|
||||||
final expected = ['modA', 'modB'];
|
final expected = ['modB', 'modA'];
|
||||||
expect(result.errors, isNotEmpty);
|
expect(result.errors, isNotEmpty);
|
||||||
expect(result.errors.any((e) => e.contains('missingDep')), isTrue);
|
expect(result.errors.any((e) => e.contains('missingDep')), isTrue);
|
||||||
expect(result.errors.any((e) => e.contains('incompatible')), isTrue);
|
expect(result.errors.any((e) => e.contains('incompatible')), isTrue);
|
||||||
@@ -653,7 +685,7 @@ void main() {
|
|||||||
list.enableAll();
|
list.enableAll();
|
||||||
|
|
||||||
final result = list.generateLoadOrder();
|
final result = list.generateLoadOrder();
|
||||||
final expected = ['modA', 'modB'];
|
final expected = ['modB', 'modA'];
|
||||||
|
|
||||||
expect(result.errors, isEmpty);
|
expect(result.errors, isEmpty);
|
||||||
expect(result.loadOrder, equals(expected));
|
expect(result.loadOrder, equals(expected));
|
||||||
@@ -691,7 +723,7 @@ void main() {
|
|||||||
list.enableAll();
|
list.enableAll();
|
||||||
|
|
||||||
final result = list.generateLoadOrder();
|
final result = list.generateLoadOrder();
|
||||||
final expected = ['modA', 'modB'];
|
final expected = ['modB', 'modA'];
|
||||||
|
|
||||||
expect(result.errors, isNotEmpty);
|
expect(result.errors, isNotEmpty);
|
||||||
expect(result.errors.any((e) => e.contains('nonExistentMod')), isTrue);
|
expect(result.errors.any((e) => e.contains('nonExistentMod')), isTrue);
|
||||||
@@ -778,15 +810,11 @@ void main() {
|
|||||||
final result = list.generateLoadOrder();
|
final result = list.generateLoadOrder();
|
||||||
final expected = ['existingMod', 'modA'];
|
final expected = ['existingMod', 'modA'];
|
||||||
|
|
||||||
|
|
||||||
// Should still generatdeequals(mopected)
|
// Should still generatdeequals(mopected)
|
||||||
expect(result.loadOrder.contains('existingMod'), isTrue);
|
expect(result.loadOrder.contains('existingMod'), isTrue);
|
||||||
|
|
||||||
// The existing loadAfter relationship should be honored
|
// The existing loadAfter relationship should be honored
|
||||||
expect(
|
expect(result.loadOrder, equals(expected));
|
||||||
result.loadOrder.indexOf('existingMod'),
|
|
||||||
lessThan(result.loadOrder.indexOf('modA')),
|
|
||||||
);
|
|
||||||
|
|
||||||
// System should track or report the missing loadAfter targets
|
// System should track or report the missing loadAfter targets
|
||||||
expect(result.errors, isEmpty); // Soft constraints shouldn't cause errors
|
expect(result.errors, isEmpty); // Soft constraints shouldn't cause errors
|
||||||
@@ -809,7 +837,8 @@ void main() {
|
|||||||
};
|
};
|
||||||
list.enableAll();
|
list.enableAll();
|
||||||
|
|
||||||
final expected = ['modA']; final result = list.generateLoadOrder();
|
final expected = ['modA'];
|
||||||
|
final result = list.generateLoadOrder();
|
||||||
|
|
||||||
// Should report the missing dependency
|
// Should report the missing dependency
|
||||||
expect(result.errors, isNotEmpty);
|
expect(result.errors, isNotEmpty);
|
||||||
@@ -822,8 +851,7 @@ void main() {
|
|||||||
isFalse,
|
isFalse,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Mod should still be , equals(expected)pendencies
|
expect(result.loadOrder, equals(expected));
|
||||||
expect(result.loadOrder.contains('modA'), isTrue);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -852,7 +880,7 @@ void main() {
|
|||||||
list.enableAll();
|
list.enableAll();
|
||||||
|
|
||||||
final result = list.generateLoadOrder();
|
final result = list.generateLoadOrder();
|
||||||
final expected = ['modB', 'modC', 'modA'];
|
final expected = ['modC', 'modB', 'modA'];
|
||||||
|
|
||||||
// Should report all missing dependencies
|
// Should report all missing dependencies
|
||||||
expect(result.errors, isNotEmpty);
|
expect(result.errors, isNotEmpty);
|
||||||
|
|||||||
@@ -691,52 +691,41 @@ void main() {
|
|||||||
test('Should not fuck up troubleshooter', () {
|
test('Should not fuck up troubleshooter', () {
|
||||||
final troubleshooter = ModListTroubleshooter(modList);
|
final troubleshooter = ModListTroubleshooter(modList);
|
||||||
final expectedFirst = [
|
final expectedFirst = [
|
||||||
'test.mod1',
|
'test.mod10',
|
||||||
// 0 depends on 1
|
'test.mod9',
|
||||||
'test.mod0',
|
'test.mod8',
|
||||||
'test.mod2',
|
'test.mod2',
|
||||||
// 3 depends on 4
|
|
||||||
'test.mod4',
|
'test.mod4',
|
||||||
'test.mod3',
|
'test.mod3',
|
||||||
'test.mod5',
|
'test.mod5',
|
||||||
// 6 depends on 7
|
|
||||||
'test.mod7',
|
'test.mod7',
|
||||||
'test.mod6',
|
'test.mod6',
|
||||||
'test.mod8',
|
'test.mod1',
|
||||||
// 9 depends on 10
|
'test.mod0',
|
||||||
'test.mod10',
|
|
||||||
'test.mod9',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
var result = troubleshooter.linearForward(stepSize: 10);
|
var result = troubleshooter.linearForward(stepSize: 10);
|
||||||
var loadOrder = result.loadRequired();
|
var loadOrder = result.loadRequired();
|
||||||
expect(loadOrder.loadOrder.length, equals(11));
|
expect(loadOrder.loadOrder.length, equals(11));
|
||||||
for (int i = 0; i < expectedFirst.length; i++) {
|
expect(loadOrder.loadOrder, equals(expectedFirst));
|
||||||
expect(loadOrder.loadOrder[i], equals(expectedFirst[i]));
|
|
||||||
}
|
|
||||||
|
|
||||||
final expectedSecond = [
|
final expectedSecond = [
|
||||||
'test.mod10',
|
'test.mod19',
|
||||||
|
'test.mod18',
|
||||||
|
'test.mod17',
|
||||||
'test.mod11',
|
'test.mod11',
|
||||||
// 12 depends on 13
|
|
||||||
'test.mod13',
|
'test.mod13',
|
||||||
'test.mod12',
|
'test.mod12',
|
||||||
'test.mod14',
|
'test.mod14',
|
||||||
// 15 depends on 16
|
|
||||||
'test.mod16',
|
'test.mod16',
|
||||||
'test.mod15',
|
'test.mod15',
|
||||||
'test.mod17',
|
'test.mod10',
|
||||||
// 18 depends on 19
|
|
||||||
'test.mod19',
|
|
||||||
'test.mod18',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
result = troubleshooter.linearForward(stepSize: 10);
|
result = troubleshooter.linearForward(stepSize: 10);
|
||||||
loadOrder = result.loadRequired();
|
loadOrder = result.loadRequired();
|
||||||
expect(loadOrder.loadOrder.length, equals(10));
|
expect(loadOrder.loadOrder.length, equals(10));
|
||||||
for (int i = 0; i < expectedSecond.length; i++) {
|
expect(loadOrder.loadOrder, equals(expectedSecond));
|
||||||
expect(loadOrder.loadOrder[i], equals(expectedSecond[i]));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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