Skip to content

Commit 60a6cf2

Browse files
committed
feat: Implement advanced diff calculation with key-based, order-based, and smart match comparison modes, alongside a new advanced diff view.
1 parent 2ef8851 commit 60a6cf2

16 files changed

Lines changed: 908 additions & 601 deletions

‎lib/core/services/comparison_engine.dart‎

Lines changed: 16 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -195,14 +195,11 @@ class ComparisonEngine {
195195
/// Isolate entry point for diff calculation with progress reporting
196196
static void _diffIsolateEntryPoint(_DiffIsolateParams params) {
197197
try {
198+
final settings = AppSettings.fromJson(params.diffParams.settingsAsJson);
198199
final diff = DiffCalculator.calculateDiff(
199-
data1: params.diffParams.data1,
200-
data2: params.diffParams.data2,
201-
ignoreCase: params.diffParams.ignoreCase,
202-
ignorePatterns: params.diffParams.ignorePatterns,
203-
ignoreWhitespace: params.diffParams.ignoreWhitespace,
204-
comparisonMode: params.diffParams.comparisonMode,
205-
similarityThreshold: params.diffParams.similarityThreshold,
200+
sourceData: params.diffParams.sourceData,
201+
targetData: params.diffParams.targetData,
202+
settings: settings,
206203
onProgress: (processed, total) {
207204
params.sendPort.send(_IsolateProgressMessage(processed, total));
208205
},
@@ -260,13 +257,9 @@ class ComparisonEngine {
260257
onProgress?.call(50, 'Calculating differences...');
261258

262259
final diffParams = _ComputeDiffParams(
263-
data1: file1Data,
264-
data2: file2Data,
265-
ignoreCase: settings.ignoreCase,
266-
ignorePatterns: settings.ignorePatterns,
267-
ignoreWhitespace: settings.ignoreWhitespace,
268-
comparisonMode: settings.comparisonMode,
269-
similarityThreshold: settings.similarityThreshold,
260+
sourceData: file1Data,
261+
targetData: file2Data,
262+
settingsAsJson: settings.toJson(),
270263
);
271264

272265
final rawDiff = await _runDiffWithProgress(
@@ -348,13 +341,9 @@ class ComparisonEngine {
348341
onProgress?.call(50, 'Calculating differences...');
349342

350343
final diffParams = _ComputeDiffParams(
351-
data1: sourceData,
352-
data2: targetData,
353-
ignoreCase: settings.ignoreCase,
354-
ignorePatterns: settings.ignorePatterns,
355-
ignoreWhitespace: settings.ignoreWhitespace,
356-
comparisonMode: settings.comparisonMode,
357-
similarityThreshold: settings.similarityThreshold,
344+
sourceData: sourceData,
345+
targetData: targetData,
346+
settingsAsJson: settings.toJson(),
358347
);
359348

360349
final rawDiff = await _runDiffWithProgress(
@@ -393,22 +382,14 @@ class _ComputeParseParams {
393382

394383
// Helper class for parameters to diff calculation
395384
class _ComputeDiffParams {
396-
final Map<String, String> data1;
397-
final Map<String, String> data2;
398-
final bool ignoreCase;
399-
final List<String> ignorePatterns;
400-
final bool ignoreWhitespace;
401-
final String comparisonMode;
402-
final double similarityThreshold;
385+
final Map<String, String> sourceData;
386+
final Map<String, String> targetData;
387+
final Map<String, dynamic> settingsAsJson;
403388

404389
_ComputeDiffParams({
405-
required this.data1,
406-
required this.data2,
407-
required this.ignoreCase,
408-
required this.ignorePatterns,
409-
required this.ignoreWhitespace,
410-
required this.comparisonMode,
411-
required this.similarityThreshold,
390+
required this.sourceData,
391+
required this.targetData,
392+
required this.settingsAsJson,
412393
});
413394
}
414395

‎lib/core/services/diff_calculator.dart‎

Lines changed: 51 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:string_similarity/string_similarity.dart';
22
import 'package:flutter/foundation.dart';
33
import 'package:localizer_app_main/data/models/comparison_status_detail.dart';
4+
import 'package:localizer_app_main/data/models/app_settings.dart';
45

56
/// Progress callback for reporting diff calculation progress.
67
typedef DiffProgressCallback = void Function(int processed, int total);
@@ -14,62 +15,60 @@ class DiffCalculator {
1415
///
1516
/// [onProgress] is an optional callback for reporting progress during calculation.
1617
static Map<String, ComparisonStatusDetail> calculateDiff({
17-
required Map<String, String> data1,
18-
required Map<String, String> data2,
19-
required bool ignoreCase,
20-
required List<String> ignorePatterns,
21-
required bool ignoreWhitespace,
22-
required String comparisonMode,
23-
required double similarityThreshold,
18+
required Map<String, String> sourceData,
19+
required Map<String, String> targetData,
20+
required AppSettings settings,
2421
DiffProgressCallback? onProgress,
2522
}) {
26-
switch (comparisonMode) {
23+
final mode = settings.comparisonMode;
24+
25+
switch (mode) {
2726
case 'Order-based':
2827
return _calculateOrderBasedDiff(
29-
data1: data1,
30-
data2: data2,
31-
ignoreCase: ignoreCase,
32-
ignorePatterns: ignorePatterns,
33-
ignoreWhitespace: ignoreWhitespace,
34-
similarityThreshold: similarityThreshold,
28+
sourceData: sourceData,
29+
targetData: targetData,
30+
ignoreCase: settings.ignoreCase,
31+
ignorePatterns: settings.ignorePatterns,
32+
ignoreWhitespace: settings.ignoreWhitespace,
33+
similarityThreshold: settings.similarityThreshold,
3534
onProgress: onProgress,
3635
);
3736
case 'Smart Match':
3837
return _calculateSmartMatchDiff(
39-
data1: data1,
40-
data2: data2,
41-
ignoreCase: ignoreCase,
42-
ignorePatterns: ignorePatterns,
43-
ignoreWhitespace: ignoreWhitespace,
44-
similarityThreshold: similarityThreshold,
38+
sourceData: sourceData,
39+
targetData: targetData,
40+
ignoreCase: settings.ignoreCase,
41+
ignorePatterns: settings.ignorePatterns,
42+
ignoreWhitespace: settings.ignoreWhitespace,
43+
similarityThreshold: settings.similarityThreshold,
4544
onProgress: onProgress,
4645
);
4746
case 'Key-based':
4847
default:
4948
return _calculateKeyBasedDiff(
50-
data1: data1,
51-
data2: data2,
52-
ignoreCase: ignoreCase,
53-
ignorePatterns: ignorePatterns,
54-
ignoreWhitespace: ignoreWhitespace,
55-
similarityThreshold: similarityThreshold,
49+
sourceData: sourceData,
50+
targetData: targetData,
51+
ignoreCase: settings.ignoreCase,
52+
ignorePatterns: settings.ignorePatterns,
53+
ignoreWhitespace: settings.ignoreWhitespace,
54+
similarityThreshold: settings.similarityThreshold,
5655
onProgress: onProgress,
5756
);
5857
}
5958
}
6059

6160
/// Key-based matching: matches entries by their key name.
6261
static Map<String, ComparisonStatusDetail> _calculateKeyBasedDiff({
63-
required Map<String, String> data1,
64-
required Map<String, String> data2,
62+
required Map<String, String> sourceData,
63+
required Map<String, String> targetData,
6564
required bool ignoreCase,
6665
required List<String> ignorePatterns,
6766
required bool ignoreWhitespace,
6867
required double similarityThreshold,
6968
DiffProgressCallback? onProgress,
7069
}) {
7170
final diff = <String, ComparisonStatusDetail>{};
72-
final allKeys = {...data1.keys, ...data2.keys}.toList();
71+
final allKeys = {...sourceData.keys, ...targetData.keys}.toList();
7372
final totalKeys = allKeys.length;
7473
int processedKeys = 0;
7574
int lastReportedPercent = 0;
@@ -80,8 +79,8 @@ class DiffCalculator {
8079
continue;
8180
}
8281

83-
String? value1 = data1[key];
84-
String? value2 = data2[key];
82+
String? value1 = sourceData[key];
83+
String? value2 = targetData[key];
8584

8685
value1 = _normalizeValue(value1, ignoreCase, ignoreWhitespace);
8786
value2 = _normalizeValue(value2, ignoreCase, ignoreWhitespace);
@@ -107,19 +106,21 @@ class DiffCalculator {
107106

108107
/// Order-based matching: matches entries by their position (index).
109108
static Map<String, ComparisonStatusDetail> _calculateOrderBasedDiff({
110-
required Map<String, String> data1,
111-
required Map<String, String> data2,
109+
required Map<String, String> sourceData,
110+
required Map<String, String> targetData,
112111
required bool ignoreCase,
113112
required List<String> ignorePatterns,
114113
required bool ignoreWhitespace,
115114
required double similarityThreshold,
116115
DiffProgressCallback? onProgress,
117116
}) {
118117
final diff = <String, ComparisonStatusDetail>{};
119-
final keys1 =
120-
data1.keys.where((k) => !_shouldIgnoreKey(k, ignorePatterns)).toList();
121-
final keys2 =
122-
data2.keys.where((k) => !_shouldIgnoreKey(k, ignorePatterns)).toList();
118+
final keys1 = sourceData.keys
119+
.where((k) => !_shouldIgnoreKey(k, ignorePatterns))
120+
.toList();
121+
final keys2 = targetData.keys
122+
.where((k) => !_shouldIgnoreKey(k, ignorePatterns))
123+
.toList();
123124

124125
final maxLength = keys1.length > keys2.length ? keys1.length : keys2.length;
125126
int lastReportedPercent = 0;
@@ -131,8 +132,8 @@ class DiffCalculator {
131132
// Use key2 as the primary key for display (target file), fallback to key1
132133
final displayKey = key2 ?? key1!;
133134

134-
String? value1 = key1 != null ? data1[key1] : null;
135-
String? value2 = key2 != null ? data2[key2] : null;
135+
String? value1 = key1 != null ? sourceData[key1] : null;
136+
String? value2 = key2 != null ? targetData[key2] : null;
136137

137138
value1 = _normalizeValue(value1, ignoreCase, ignoreWhitespace);
138139
value2 = _normalizeValue(value2, ignoreCase, ignoreWhitespace);
@@ -155,8 +156,8 @@ class DiffCalculator {
155156

156157
/// Smart Match: Key-based first, then detects potential key renames.
157158
static Map<String, ComparisonStatusDetail> _calculateSmartMatchDiff({
158-
required Map<String, String> data1,
159-
required Map<String, String> data2,
159+
required Map<String, String> sourceData,
160+
required Map<String, String> targetData,
160161
required bool ignoreCase,
161162
required List<String> ignorePatterns,
162163
required bool ignoreWhitespace,
@@ -168,7 +169,7 @@ class DiffCalculator {
168169
final matchedKeys2 = <String>{};
169170

170171
// Calculate total work: phase1 + phase2 + phase3 + phase4
171-
final totalKeys = data1.length + data2.length;
172+
final totalKeys = sourceData.length + targetData.length;
172173
int processedKeys = 0;
173174
int lastReportedPercent = 0;
174175

@@ -183,17 +184,17 @@ class DiffCalculator {
183184
}
184185

185186
// Phase 1: Exact key matches
186-
for (final key in data1.keys) {
187+
for (final key in sourceData.keys) {
187188
if (_shouldIgnoreKey(key, ignorePatterns)) {
188189
processedKeys++;
189190
continue;
190191
}
191192

192-
if (data2.containsKey(key)) {
193+
if (targetData.containsKey(key)) {
193194
String? value1 =
194-
_normalizeValue(data1[key], ignoreCase, ignoreWhitespace);
195+
_normalizeValue(sourceData[key], ignoreCase, ignoreWhitespace);
195196
String? value2 =
196-
_normalizeValue(data2[key], ignoreCase, ignoreWhitespace);
197+
_normalizeValue(targetData[key], ignoreCase, ignoreWhitespace);
197198
diff[key] = _compareValues(value1, value2, similarityThreshold);
198199
matchedKeys1.add(key);
199200
matchedKeys2.add(key);
@@ -203,11 +204,11 @@ class DiffCalculator {
203204
}
204205

205206
// Phase 2: Find unmatched keys
206-
final unmatchedKeys1 = data1.keys
207+
final unmatchedKeys1 = sourceData.keys
207208
.where((k) =>
208209
!matchedKeys1.contains(k) && !_shouldIgnoreKey(k, ignorePatterns))
209210
.toList();
210-
final unmatchedKeys2 = data2.keys
211+
final unmatchedKeys2 = targetData.keys
211212
.where((k) =>
212213
!matchedKeys2.contains(k) && !_shouldIgnoreKey(k, ignorePatterns))
213214
.toList();
@@ -216,14 +217,14 @@ class DiffCalculator {
216217
final usedKeys2 = <String>{};
217218
for (final key1 in unmatchedKeys1) {
218219
String? value1 =
219-
_normalizeValue(data1[key1], ignoreCase, ignoreWhitespace);
220+
_normalizeValue(sourceData[key1], ignoreCase, ignoreWhitespace);
220221
String? bestMatch;
221222
double bestSimilarity = 0.0;
222223

223224
for (final key2 in unmatchedKeys2) {
224225
if (usedKeys2.contains(key2)) continue;
225226
String? value2 =
226-
_normalizeValue(data2[key2], ignoreCase, ignoreWhitespace);
227+
_normalizeValue(targetData[key2], ignoreCase, ignoreWhitespace);
227228

228229
if (value1 != null && value2 != null) {
229230
final similarity = value1.similarityTo(value2);

‎lib/core/services/toast_service.dart‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,13 @@ class ToastService {
8181

8282
/// Shows an error toast with a red warning icon.
8383
/// Optionally provide a recovery suggestion for actionable feedback.
84+
/// When [onRetry] is provided, a "Retry" button is shown and the toast
85+
/// uses expanded (non-compact) mode to prevent message truncation.
8486
static void showError(
8587
BuildContext context,
8688
String message, {
8789
String? recoverySuggestion,
90+
VoidCallback? onRetry,
8891
Duration duration = const Duration(seconds: 6),
8992
}) {
9093
final fullMessage =
@@ -94,6 +97,9 @@ class ToastService {
9497
message: fullMessage,
9598
type: ToastType.error,
9699
duration: duration,
100+
actionLabel: onRetry != null ? 'Retry' : null,
101+
onAction: onRetry,
102+
forceExpanded: onRetry != null,
97103
);
98104
}
99105

@@ -133,9 +139,10 @@ class ToastService {
133139
String? actionLabel,
134140
VoidCallback? onAction,
135141
Duration duration = const Duration(seconds: 4),
142+
bool forceExpanded = false,
136143
}) {
137144
final alignment = _getAlignment();
138-
final isCompact = _isCompact();
145+
final isCompact = forceExpanded ? false : _isCompact();
139146

140147
toastification.showCustom(
141148
context: context,

0 commit comments

Comments
 (0)