Skip to content
Merged
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
16 changes: 10 additions & 6 deletions workout-logger/lib/main.dart
Original file line numberDiff line numberDiff line change
Expand Up@@ -200,12 +200,16 @@ class _AppInitializerState extends State<AppInitializer> {
// so the opt-in flag is loaded; never blocks or fails app init.
readiness.refresh();

// Fire-and-forget analytics in background.
api.sendHeartbeat();
api.trackEvent('app_open');
provider.getQuickStats().then((stats) => api.reportUsage(stats)).catchError(
(Object e) => debugPrint('Failed to report usage: $e'),
);
// Fire-and-forget analytics in background — never fires for F-Droid
// installs, and honors the user's Settings toggle otherwise. Must
// run after settings.init() so both are loaded.
if (settings.telemetryAllowed) {
api.sendHeartbeat();
api.trackEvent('app_open');
provider.getQuickStats().then((stats) => api.reportUsage(stats)).catchError(
(Object e) => debugPrint('Failed to report usage: $e'),
);
}

if (!mounted) return;
setState(() {
Expand Down
27 changes: 1 addition & 26 deletions workout-logger/lib/screens/profile_screen.dart
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,6 @@ import 'package:package_info_plus/package_info_plus.dart';

import '../services/workout_provider.dart';
import '../services/settings_provider.dart';
import '../services/api_service.dart';
import '../services/interfaces/health_connect_service_interface.dart';
import '../services/managers/readiness_manager.dart';
import '../theme/app_theme.dart';
Expand All@@ -31,7 +30,6 @@ class _ProfileScreenState extends State<ProfileScreen>
with WidgetsBindingObserver {
bool _isExporting = false;
bool _isImporting = false;
bool _isBackingUp = false;
bool _isRequestingHcPermission = false;
bool _isRequestingReadinessPermission = false;
String _appVersion = '';
Expand DownExpand Up@@ -295,27 +293,6 @@ class _ProfileScreenState extends State<ProfileScreen>
}
}

Future<void> _performCloudBackup() async {
setState(() => _isBackingUp = true);
final provider = context.read<WorkoutProvider>();
final api = context.read<ApiService>();
try {
final jsonString = await provider.exportAllData();
final data = jsonDecode(jsonString) as Map<String, dynamic>;
await api.trackEvent('backup_triggered').catchError((_) => null);
final success = await api.backupData(data);
if (!mounted) return;
_showSnack(
success ? 'Cloud backup successful!' : 'Backup failed. Please try again.',
success ? AppColors.success : AppColors.error,
);
} catch (_) {
if (mounted) _showSnack('Something went wrong.', AppColors.error);
} finally {
if (mounted) setState(() => _isBackingUp = false);
}
}

void _showSnack(String message, Color color) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
Expand DownExpand Up@@ -379,15 +356,13 @@ class _ProfileScreenState extends State<ProfileScreen>
DataManagementSection(
isExporting: _isExporting,
isImporting: _isImporting,
isBackingUp: _isBackingUp,
onExport: _isExporting ? null : _exportToFile,
onImport: _isImporting ? null : _importFromFile,
onCloudBackup: _isBackingUp ? null : _performCloudBackup,
),
const SizedBox(height: AppSpacing.md),
const AiSettingsSection(),
const SizedBox(height: AppSpacing.md),
const CloudSyncSection(),
PrivacySection(settings: settings),
const SizedBox(height: AppSpacing.md),
AboutSection(appVersion: _appVersion),
const SizedBox(height: AppSpacing.xxl),
Expand Down
135 changes: 48 additions & 87 deletions workout-logger/lib/screens/widgets/profile_sections.dart
Original file line numberDiff line numberDiff line change
Expand Up@@ -356,26 +356,22 @@ class DataManagementSection extends StatelessWidget {
super.key,
required this.isExporting,
required this.isImporting,
required this.isBackingUp,
required this.onExport,
required this.onImport,
required this.onCloudBackup,
});

final bool isExporting;
final bool isImporting;
final bool isBackingUp;
final VoidCallback? onExport;
final VoidCallback? onImport;
final VoidCallback? onCloudBackup;

@override
Widget build(BuildContext context) {
return _ProfileSection(
icon: Icons.storage_rounded,
iconColor: AppColors.secondary,
title: 'Data Management',
subtitle: 'Export, import, or backup your workout data',
subtitle: 'Exportor import your workout data',
child: Column(
children: [
_ActionTile(
Expand All@@ -395,77 +391,67 @@ class DataManagementSection extends StatelessWidget {
loading: isImporting,
onTap: onImport,
),
const _SectionDivider(),
_ActionTile(
icon: Icons.cloud_upload_outlined,
iconColor: AppColors.primary,
title: 'Cloud Backup',
subtitle: 'Sync to RepForge cloud (requires account)',
loading: isBackingUp,
onTap: onCloudBackup,
),
],
),
);
}
}

// ── Cloud Sync section (placeholder) ─────────────────────────────────────────
class CloudSyncSection extends StatelessWidget {
const CloudSyncSection({super.key});
// ── Privacy section ────────────────────────────────────────────────────────────
class PrivacySection extends StatelessWidget {
const PrivacySection({super.key, required this.settings});

final SettingsProvider settings;

static const _color = AppColors.textSoft;

@override
Widget build(BuildContext context) {
final isFdroid = settings.isFdroidInstall;
final enabled = settings.telemetryAllowed;
return _ProfileSection(
icon: Icons.sync_rounded,
iconColor: AppColors.warning,
title: 'Cloud Sync',
subtitle: 'Sync your data across devices',
trailing: const _ComingSoonBadge(),
icon: Icons.privacy_tip_outlined,
iconColor: _color,
title: 'Privacy',
subtitle: 'Control anonymous usage data',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const _SectionLabel('MONGODB CONNECTION STRING'),
const SizedBox(height: AppSpacing.sm),
Container(
decoration: BoxDecoration(
color: AppColors.glass,
borderRadius: BorderRadius.circular(AppRadius.sm),
border: Border.all(color: AppColors.glassBorder),
),
child: TextField(
enabled: false,
style: TextStyle(fontFamily: 'GeistMono',
color: AppColors.textFaint,
fontSize: 12,
),
decoration: InputDecoration(
hintText: 'mongodb+srv://user:pass@cluster.mongodb.net/db',
hintStyle: TextStyle(fontFamily: 'GeistMono',
color: AppColors.textFaint,
fontSize: 12,
),
prefixIcon: const Icon(
Icons.link_rounded,
color: AppColors.textFaint,
size: 16,
),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm + 4,
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Share anonymous usage data',
style: TextStyle(fontFamily: 'Geist',
color: AppColors.textPrimary,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
isFdroid
? 'Always off for F-Droid installs'
: 'Install ID, platform, and workout counts — no personal data',
style: TextStyle(fontFamily: 'Geist',
color: AppColors.textMuted,
fontSize: 12,
),
),
],
),
),
),
),
const SizedBox(height: AppSpacing.sm),
Text(
'Cloud sync with custom MongoDB will be available in a future update.',
style: TextStyle(fontFamily: 'Geist',
color: AppColors.textFaint,
fontSize: 11,
fontStyle: FontStyle.italic,
),
Switch(
value: enabled,
onChanged: isFdroid
? null
: (v) => settings.setAnalyticsEnabled(v),
activeThumbColor: _color,
activeTrackColor: _color.withValues(alpha: 0.35),
),
],
),
],
),
Expand DownExpand Up@@ -1154,28 +1140,3 @@ class _DebugLogSheet extends StatelessWidget {
);
}
}

class _ComingSoonBadge extends StatelessWidget {
const _ComingSoonBadge();

@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: AppColors.warning.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(AppRadius.full),
border: Border.all(color: AppColors.warning.withValues(alpha: 0.35)),
),
child: Text(
'Soon',
style: TextStyle(fontFamily: 'Geist',
color: AppColors.warning,
fontSize: 10,
fontWeight: FontWeight.w600,
letterSpacing: 0.3,
),
),
);
}
}
34 changes: 34 additions & 0 deletions workout-logger/lib/services/settings_provider.dart
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,8 @@ class SettingsProvider extends ChangeNotifier {
String _weeklyInsights = '';
DateTime? _weeklyInsightsDate;
bool _showAdvancedMetrics = false;
bool _analyticsEnabled = true;
bool _isFdroidInstall = false;

WeightUnit get weightUnit => _weightUnit;
double get weightIncrement => _weightIncrement;
Expand All@@ -34,6 +36,22 @@ class SettingsProvider extends ChangeNotifier {
DateTime? get weeklyInsightsDate => _weeklyInsightsDate;
bool get showAdvancedMetrics => _showAdvancedMetrics;

/// Whether this install came from the F-Droid client (detected at runtime
/// via the Android installer package name — never baked in at compile
/// time, since a compile-time difference between the F-Droid build and
/// the GitHub release binary would break F-Droid's byte-for-byte
/// reproducible-build verification against `Binaries:` in fdroiddata).
bool get isFdroidInstall => _isFdroidInstall;

/// User's analytics preference, as stored. F-Droid installs are always
/// telemetry-free regardless of this value — see [telemetryAllowed].
bool get analyticsEnabled => _analyticsEnabled;

/// Whether automatic telemetry (heartbeat/app-open/usage-report) may
/// fire. False for F-Droid installs unconditionally; otherwise follows
/// the user's setting.
bool get telemetryAllowed => _analyticsEnabled && !_isFdroidInstall;

SettingsProvider(this._storage);

Future<void> init() async {
Expand All@@ -60,6 +78,16 @@ class SettingsProvider extends ChangeNotifier {
_weeklyInsightsDate = dateStr != null ? DateTime.tryParse(dateStr) : null;
final advMetrics = await _storage.getSetting('showAdvancedMetrics');
_showAdvancedMetrics = advMetrics == 'true';

final analytics = await _storage.getSetting('analyticsEnabled');
_analyticsEnabled = analytics != 'false';

try {
final info = await PackageInfo.fromPlatform();
_isFdroidInstall = info.installerStore == 'org.fdroid.fdroid';
} catch (_) {
_isFdroidInstall = false;
}
}

Future<void> setUserName(String name) async {
Expand DownExpand Up@@ -130,6 +158,12 @@ class SettingsProvider extends ChangeNotifier {
notifyListeners();
}

Future<void> setAnalyticsEnabled(bool value) async {
_analyticsEnabled = value;
await _storage.saveSetting('analyticsEnabled', value.toString());
notifyListeners();
}

Future<void> saveWeeklyInsights(String insights) async {
_weeklyInsights = insights;
_weeklyInsightsDate = DateTime.now();
Expand Down
Loading