Skip to content

Conversation

@mulikruchi07
Copy link
Contributor

Description

This PR improves the readability of TXT task exports by converting raw JSON output into a human-readable label: value format.
JSON export remains unchanged to preserve raw data integrity.

Fixes #525

Screenshots

TXT export after enhancement (human-readable format):
enh_ref2_525

Checklist

  • Tests have been added or updated to cover the changes
  • Documentation has been updated to reflect the changes
  • Code follows the established coding style guidelines
  • All tests are passing

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances the TXT export functionality by converting raw JSON task data into a human-readable label: value format. The JSON export remains unchanged to preserve data integrity for programmatic access.

Key changes:

  • Added formatTasksAsTxt() function to convert JSON task data to human-readable text format
  • Added formatDateValue() helper to format date fields in local timezone
  • Modified TXT export button handler to apply formatting before export

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 8 comments.

File Description
lib/app/models/storage/savefile.dart Implements new formatting functions (formatTasksAsTxt, formatDateValue) with label mapping, field ordering, and special handling for dates, lists, and annotations
lib/app/modules/profile/views/profile_view.dart Integrates the new formatting by calling formatTasksAsTxt() on task data before TXT export

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +42 to +155
String formatTasksAsTxt(String contents) {
Map<String, String> labelMap = {
'description': 'Description',
'status': 'Status',
'due': 'Due',
'project': 'Project',
'priority': 'Priority',
'uuid': 'UUID',
'tags': 'Tags',
'depends': 'Depends',
'annotations': 'Annotations',
'entry': 'Entry',
'modified': 'Modified',
'start': 'Start',
'wait': 'Wait',
'recur': 'Recur',
'rtype': 'RType',
'urgency': 'Urgency',
'end': 'End',
'id': 'ID'
};

String formatTaskMap(Map m) {
final entryVal = m['entry'];
final startVal = m['start'];

List<String> order = [
'id',
'description',
'status',
'project',
'due',
'priority',
'uuid',
'entry',
'modified',
'start',
'wait',
'recur',
'rtype',
'urgency',
'end',
'tags',
'depends',
'annotations'
];
List<String> lines = [];
for (var key in order) {
if (!m.containsKey(key) || m[key] == null) continue;
if (key == 'start' &&
startVal != null &&
entryVal != null &&
startVal.toString() == entryVal.toString()) {
continue;
}

var val = m[key];
if (key == 'tags' || key == 'depends') {
if (val is List) {
lines.add('${labelMap[key]}: ${val.join(', ')}');
} else {
lines.add('${labelMap[key]}: $val');
}
} else if (key == 'annotations') {
if (val is List && val.isNotEmpty) {
lines.add('${labelMap[key]}:');
for (var a in val) {
if (a is Map) {
var entry = a['entry'] ?? '';
var desc = a['description'] ?? '';
lines.add(' - ${labelMap['entry']}: $entry');
lines.add(' Description: $desc');
} else {
lines.add(' - $a');
}
}
}
} else {
final isDateField =
['due', 'entry', 'modified', 'start', 'wait', 'end'].contains(key);

lines.add(
'${labelMap[key] ?? key}: '
'${isDateField ? formatDateValue(val) : val.toString()}',
);
}
}
return lines.join('\n');
}

dynamic parsed;
try {
parsed = json.decode(contents);
} catch (_) {
try {
// Attempt to convert Dart-style maps (single quotes) to JSON
var fixed = contents.replaceAll("'", '"');
parsed = json.decode(fixed);
} catch (e) {
return contents; // fallback to original if parsing fails
}
}

if (parsed is List) {
return parsed.map((e) {
if (e is Map) return formatTaskMap(Map.from(e));
return e.toString();
}).join('\n\n');
} else if (parsed is Map) {
return formatTaskMap(Map.from(parsed));
} else {
return parsed.toString();
}
}
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new formatTasksAsTxt function lacks test coverage. Given the complexity of the formatting logic (handling multiple data types, date parsing, nested annotations, fallback scenarios), comprehensive tests should be added to verify correct behavior for: valid JSON input with all field types, edge cases (null values, empty arrays, malformed dates), Dart-style map input with single quotes, and various annotation structures.

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +40
String formatDateValue(dynamic val) {
if (val == null) return '-';

try {
final dt = DateTime.parse(val.toString()).toLocal();
return '${dt.year.toString().padLeft(4, '0')}-'
'${dt.month.toString().padLeft(2, '0')}-'
'${dt.day.toString().padLeft(2, '0')} '
'${dt.hour.toString().padLeft(2, '0')}:'
'${dt.minute.toString().padLeft(2, '0')}:'
'${dt.second.toString().padLeft(2, '0')}';
} catch (_) {
return val.toString(); // fallback
}
}
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new formatDateValue function lacks test coverage. Tests should verify: null input returns '-', valid ISO 8601 date strings are formatted correctly in local timezone, invalid date strings fall back to original value, and various date/time values (with and without time zones) are handled properly.

Copilot uses AI. Check for mistakes.
Comment on lines +136 to +142
try {
// Attempt to convert Dart-style maps (single quotes) to JSON
var fixed = contents.replaceAll("'", '"');
parsed = json.decode(fixed);
} catch (e) {
return contents; // fallback to original if parsing fails
}
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback logic that converts single quotes to double quotes is fragile and may produce incorrect results. This approach will incorrectly transform legitimate single quotes within string values (e.g., "It's a task" becomes "It"s a task" which is invalid JSON). Consider using a proper Dart literal parser or explicitly handling the expected input format.

Copilot uses AI. Check for mistakes.
return val.toString(); // fallback
}
}

Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function lacks documentation explaining its purpose, parameters, return value, and potential exceptions. Consider adding a dartdoc comment that describes: the expected input format (JSON string containing task data), what the function returns (human-readable formatted text), and edge cases like parsing failures.

Suggested change
/// Formats task data contained in a JSON string into a human‑readable
/// plain‑text representation.
///
/// The [contents] parameter is expected to be a JSON string that decodes to
/// either:
/// * a `List` of task maps, or
/// * a single task represented as a `Map`.
///
/// Each task map is rendered into labeled, line‑separated fields using a
/// predefined mapping of task attribute keys (for example, `description`,
/// `status`, `due`) to human‑friendly labels, and multiple tasks are
/// separated by blank lines.
///
/// If JSON decoding fails, a secondary attempt is made by replacing
/// single quotes with double quotes to handle Dart‑style map literals.
/// If parsing still fails, the original [contents] string is returned
/// unchanged. If [contents] decodes successfully but does not represent
/// a `List` or `Map`, the resulting value's `toString()` representation
/// is returned.

Copilot uses AI. Check for mistakes.
);
}
}

Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function lacks documentation explaining its purpose, parameters, return value, and behavior. Consider adding a dartdoc comment describing: the expected input format (date string or DateTime object), the output format (YYYY-MM-DD HH:MM:SS in local timezone or '-' for null), and that it returns the original value as a string if parsing fails.

Suggested change
/// Formats a date-like value for display.
///
/// Expects [val] to be either a [DateTime] instance or a string that can be
/// parsed by [DateTime.parse]. If [val] is `null`, this returns `'-'`.
///
/// When parsing succeeds, the date-time is converted to the local time zone
/// and formatted as `YYYY-MM-DD HH:MM:SS` (e.g. `2024-01-31 14:05:09`).
///
/// If parsing fails, the original [val] is returned as a string by calling
/// `val.toString()`.

Copilot uses AI. Check for mistakes.
Comment on lines +42 to +63
String formatTasksAsTxt(String contents) {
Map<String, String> labelMap = {
'description': 'Description',
'status': 'Status',
'due': 'Due',
'project': 'Project',
'priority': 'Priority',
'uuid': 'UUID',
'tags': 'Tags',
'depends': 'Depends',
'annotations': 'Annotations',
'entry': 'Entry',
'modified': 'Modified',
'start': 'Start',
'wait': 'Wait',
'recur': 'Recur',
'rtype': 'RType',
'urgency': 'Urgency',
'end': 'End',
'id': 'ID'
};

Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The labelMap is defined as a local variable within the function, making it recreated on every function call. Consider moving this constant map outside the function scope (as a top-level const or static const) to improve performance and code organization.

Suggested change
String formatTasksAsTxt(String contents) {
Map<String, String> labelMap = {
'description': 'Description',
'status': 'Status',
'due': 'Due',
'project': 'Project',
'priority': 'Priority',
'uuid': 'UUID',
'tags': 'Tags',
'depends': 'Depends',
'annotations': 'Annotations',
'entry': 'Entry',
'modified': 'Modified',
'start': 'Start',
'wait': 'Wait',
'recur': 'Recur',
'rtype': 'RType',
'urgency': 'Urgency',
'end': 'End',
'id': 'ID'
};
const Map<String, String> _taskLabelMap = <String, String>{
'description': 'Description',
'status': 'Status',
'due': 'Due',
'project': 'Project',
'priority': 'Priority',
'uuid': 'UUID',
'tags': 'Tags',
'depends': 'Depends',
'annotations': 'Annotations',
'entry': 'Entry',
'modified': 'Modified',
'start': 'Start',
'wait': 'Wait',
'recur': 'Recur',
'rtype': 'RType',
'urgency': 'Urgency',
'end': 'End',
'id': 'ID',
};
String formatTasksAsTxt(String contents) {

Copilot uses AI. Check for mistakes.
var entry = a['entry'] ?? '';
var desc = a['description'] ?? '';
lines.add(' - ${labelMap['entry']}: $entry');
lines.add(' Description: $desc');
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The annotation entry field uses labelMap['entry'] which resolves to 'Entry', but the description field uses a hardcoded string 'Description'. For consistency, consider either using labelMap['description'] or making both hardcoded. This ensures consistent labeling throughout the formatting.

Suggested change
lines.add(' Description: $desc');
lines.add(' ${labelMap['description'] ?? 'Description'}: $desc');

Copilot uses AI. Check for mistakes.
Comment on lines +68 to +87
List<String> order = [
'id',
'description',
'status',
'project',
'due',
'priority',
'uuid',
'entry',
'modified',
'start',
'wait',
'recur',
'rtype',
'urgency',
'end',
'tags',
'depends',
'annotations'
];
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The order list is defined as a local variable within the nested function, making it recreated on every task formatting. Consider moving this constant list outside the function scope (as a top-level const or static const) to improve performance and code organization.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improve TXT export readability by using label:value format instead of raw JSON

1 participant