Files
sharedinbox/lib/ui/utils/about_markdown.dart
T
Thomas SharedInboxandClaude Sonnet 4.6 bb29b37257 feat: copy button for sync log entries with stack trace and device info (#210)
- Add copy button to each sync log tile; copies a markdown summary of the
  entry plus the full About section (app version, platform, device info).
- Store stack trace and isPermanent flag on error entries (schema v33) so
  bug reports contain enough context to diagnose device-specific failures
  like MissingPluginException on Android.
- Add Android device info (manufacturer, model, OS version) to the About
  screen via device_info_plus; shared with the sync log copy via a new
  lib/ui/utils/about_markdown.dart utility.
- Show isPermanent in the subtitle ("Error (permanent)") and in the
  copied markdown.
- Display stack trace in red monospace in the expanded tile view.
- Update migration tests to assert schema v33 columns exist.
- Update fake SyncLogRepository implementations in tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 12:42:42 +02:00

74 lines
2.7 KiB
Dart

import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
const _gitHash = String.fromEnvironment('GIT_HASH');
/// Returns Android device info (manufacturer, model, OS release).
/// Returns an empty map on non-Android platforms or if the plugin fails.
Future<Map<String, String>> fetchAndroidDeviceInfo() async {
if (!Platform.isAndroid) return {};
try {
final info = await DeviceInfoPlugin().androidInfo;
return {
'Manufacturer': info.manufacturer,
'Model': info.model,
'Android Version': info.version.release,
};
} catch (_) {
return {};
}
}
String _capitalize(String s) =>
s.isEmpty ? s : '${s[0].toUpperCase()}${s.substring(1)}';
/// Builds the standard "about" markdown table for sharing / bug reports.
///
/// Pass [deviceInfo] from [fetchAndroidDeviceInfo].
/// Pass [imapCount] and [jmapCount] when available; both default to 0.
String buildAboutMarkdown(
BuildContext context,
PackageInfo? pkg,
int imapCount,
int jmapCount,
Map<String, String> deviceInfo,
) {
final size = MediaQuery.of(context).size;
final pixelRatio = MediaQuery.of(context).devicePixelRatio;
final physW = (size.width * pixelRatio).toInt();
final physH = (size.height * pixelRatio).toInt();
final version = pkg != null ? '${pkg.version}+${pkg.buildNumber}' : 'unknown';
final versionDisplay = _gitHash.isNotEmpty
? '[$version](https://codeberg.org/guettli/sharedinbox/commit/$_gitHash)'
: version;
final osName = _capitalize(Platform.operatingSystem);
final isDark = MediaQuery.of(context).platformBrightness == Brightness.dark;
final gitCommitLine = _gitHash.isNotEmpty
? '| Git Commit | [$_gitHash](https://codeberg.org/guettli/sharedinbox/commit/$_gitHash) |\n'
: '';
final deviceLines =
deviceInfo.entries.map((e) => '| ${e.key} | ${e.value} |\n').join();
return '## [sharedinbox.de](https://sharedinbox.de)\n\n'
'| Property | Value |\n'
'|----------|-------|\n'
'| App Version | $versionDisplay |\n'
'$gitCommitLine'
'| Platform | ${Platform.operatingSystem} |\n'
'| $osName Version | ${Platform.operatingSystemVersion} |\n'
'$deviceLines'
'| Resolution | ${physW}x$physH px'
' (logical: ${size.width.toInt()}x${size.height.toInt()} pt,'
' ratio: ${pixelRatio.toStringAsFixed(1)}x) |\n'
'| Dart Version | ${Platform.version.split(' ').first} |\n'
'| Processors | ${Platform.numberOfProcessors} |\n'
'| Dark Mode | ${isDark ? 'yes' : 'no'} |\n'
'| IMAP Accounts | $imapCount |\n'
'| JMAP Accounts | $jmapCount |\n';
}