Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions lib/app/models/storage/savefile.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:io';
import 'dart:typed_data';
import 'dart:convert';
import 'package:file_selector/file_selector.dart';
import 'package:file_picker_writable/file_picker_writable.dart';

Expand All @@ -22,6 +23,137 @@ Future<void> saveServerCert(String contents) async {
}
}

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.
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
}
}
Comment on lines +26 to +40
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.

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.
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'
};

Comment on lines +42 to +63
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.
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'
];
Comment on lines +68 to +87
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.
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');
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.
} 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
}
Comment on lines +136 to +142
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.
}

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();
}
}
Comment on lines +42 to +155
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.

Future<void> exportTasks({
required String contents,
required String suggestedName,
Expand Down
4 changes: 3 additions & 1 deletion lib/app/modules/profile/views/profile_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,10 @@ class ProfileView extends GetView<ProfileController> {
onPressed: () {
// Navigator.of(context).pop();
Get.back();
// Convert exported data to human-readable TXT
var txt = formatTasksAsTxt(tasks);
exportTasks(
contents: tasks,
contents: txt,
suggestedName: 'tasks-$now.txt',
);
},
Expand Down