diff --git a/fdroid/metadata/com.devasy.repforge.yml b/fdroid/metadata/com.devasy.repforge.yml index 7b0ab30..d407a0f 100644 --- a/fdroid/metadata/com.devasy.repforge.yml +++ b/fdroid/metadata/com.devasy.repforge.yml @@ -1,56 +1,88 @@ +AntiFeatures: + NonFreeNet: + en-US: "The optional AI Coach and Routine Optimizer features send data to Google's + Gemini API. These features are fully disabled unless the user provides their + own Gemini API key in Settings → AI Settings." Categories: - Sports & Health License: Apache-2.0 AuthorName: Devasy Patel -SourceCode: https://github.com/Devasy23/RepForge -IssueTracker: https://github.com/Devasy23/RepForge/issues +SourceCode: https://github.com/Devasy/RepForge +IssueTracker: https://github.com/Devasy/RepForge/issues AutoName: RepForge -Summary: Open-source workout logger with AI coaching and progress analytics -Description: |- - RepForge is a privacy-first workout logging app that helps you track sets, - reps, and weights across customizable routines. - - Features: - * Log workout sessions with sets, reps, and weights - * Visualise progress with charts (volume, strength curves) - * AI-powered set recommendations using linear regression - * Customisable exercise library with 50+ built-in exercises - * Goal tracking with ML-estimated completion dates - * Reusable workout routines - * Health Connect integration for sleep and heart rate readiness scores - * Optional AI Coach powered by Google Gemini (requires user-supplied API key) - * Full data export/import for portability - - All workout data is stored locally on-device using Hive. No account required. - No data is sent to any server unless you enable the optional AI Coach feature. RepoType: git -Repo: https://github.com/Devasy23/RepForge - -AntiFeatures: - NonFreeNet: - - description: > - The optional AI Coach and Routine Optimizer features send data to - Google's Gemini API (a proprietary cloud service). These features are - fully disabled unless the user provides their own Gemini API key in - Settings → AI Settings. The app is fully functional as an offline - workout logger without configuring a key. +Repo: https://github.com/Devasy/RepForge Builds: - - versionName: 2.0.1 - versionCode: 21 - commit: v2.0.1 + - versionName: 2.0.5 + versionCode: 251 + commit: ed8be2420595337f69cf550aeb4d77cb1ae943ef + subdir: workout-logger + output: build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk + srclibs: + - flutter@stable + prebuild: + - export PUB_CACHE=$(pwd)/.pub-cache + - 'export FLUTTER_VERSION=$(sed -n -E "s/^ flutter: (.+)/\1/p" pubspec.yaml)' + - '[[ $FLUTTER_VERSION ]]' + - git -C $$flutter$$ checkout -f $FLUTTER_VERSION + - $$flutter$$/bin/flutter config --no-analytics + - $$flutter$$/bin/flutter pub get --enforce-lockfile + scandelete: + - workout-logger/.pub-cache + build: + - export PUB_CACHE=$(pwd)/.pub-cache + - $$flutter$$/bin/flutter build apk --release --split-per-abi --target-platform="android-arm" + + - versionName: 2.0.5 + versionCode: 252 + commit: ed8be2420595337f69cf550aeb4d77cb1ae943ef + subdir: workout-logger + output: build/app/outputs/flutter-apk/app-arm64-v8a-release.apk + srclibs: + - flutter@stable + prebuild: + - export PUB_CACHE=$(pwd)/.pub-cache + - 'export FLUTTER_VERSION=$(sed -n -E "s/^ flutter: (.+)/\1/p" pubspec.yaml)' + - '[[ $FLUTTER_VERSION ]]' + - git -C $$flutter$$ checkout -f $FLUTTER_VERSION + - $$flutter$$/bin/flutter config --no-analytics + - $$flutter$$/bin/flutter pub get --enforce-lockfile + scandelete: + - workout-logger/.pub-cache + build: + - export PUB_CACHE=$(pwd)/.pub-cache + - $$flutter$$/bin/flutter build apk --release --split-per-abi --target-platform="android-arm64" + + - versionName: 2.0.5 + versionCode: 253 + commit: ed8be2420595337f69cf550aeb4d77cb1ae943ef subdir: workout-logger - gradle: - - release + output: build/app/outputs/flutter-apk/app-x86_64-release.apk + srclibs: + - flutter@stable prebuild: - - flutter pub get + - export PUB_CACHE=$(pwd)/.pub-cache + - 'export FLUTTER_VERSION=$(sed -n -E "s/^ flutter: (.+)/\1/p" pubspec.yaml)' + - '[[ $FLUTTER_VERSION ]]' + - git -C $$flutter$$ checkout -f $FLUTTER_VERSION + - $$flutter$$/bin/flutter config --no-analytics + - $$flutter$$/bin/flutter pub get --enforce-lockfile + scandelete: + - workout-logger/.pub-cache build: - - flutter build apk --release --split-per-abi + - export PUB_CACHE=$(pwd)/.pub-cache + - $$flutter$$/bin/flutter build apk --release --split-per-abi --target-platform="android-x64" -AutoUpdateMode: Version v%v -UpdateCheckMode: Tags -UpdateCheckData: workout-logger/pubspec.yaml|^version:\s+([\d.]+)\+|.| -CurrentVersion: 2.0.1 -CurrentVersionCode: 21 +AutoUpdateMode: Version +UpdateCheckMode: Tags ^v[\d.]+$ +VercodeOperation: + - 10 * %c + 1 + - 10 * %c + 2 + - 10 * %c + 3 +UpdateCheckData: + workout-logger/pubspec.yaml|version:\s.*\+(\d+)|workout-logger/pubspec.yaml|version:\s([\d.]+)\+ +CurrentVersion: 2.0.5 +CurrentVersionCode: 253 diff --git a/workout-logger/CLAUDE.md b/workout-logger/CLAUDE.md new file mode 100644 index 0000000..805596b --- /dev/null +++ b/workout-logger/CLAUDE.md @@ -0,0 +1,9 @@ +## graphify + +This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships. + +Rules: +- ALWAYS read graphify-out/GRAPH_REPORT.md before reading any source files, running grep/glob searches, or answering codebase questions. The graph is your primary map of the codebase. +- IF graphify-out/wiki/index.md EXISTS, navigate it instead of reading raw files +- For cross-module "how does X relate to Y" questions, prefer `graphify query ""`, `graphify path "" ""`, or `graphify explain ""` over grep — these traverse the graph's EXTRACTED + INFERRED edges instead of scanning files +- After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost). diff --git a/workout-logger/android/app/build.gradle.kts b/workout-logger/android/app/build.gradle.kts index baca78a..32e478b 100644 --- a/workout-logger/android/app/build.gradle.kts +++ b/workout-logger/android/app/build.gradle.kts @@ -1,3 +1,5 @@ +import com.android.build.gradle.internal.api.ApkVariantOutputImpl + plugins { id("com.android.application") id("kotlin-android") @@ -87,3 +89,15 @@ android { flutter { source = "../.." } + +val abiCodes = mapOf("armeabi-v7a" to 1, "arm64-v8a" to 2, "x86_64" to 3) +android.applicationVariants.configureEach { + val variant = this + variant.outputs.forEach { output -> + val abiVersionCode = abiCodes[output.filters.find { it.filterType == "ABI" }?.identifier] + if (abiVersionCode != null) { + (output as ApkVariantOutputImpl).versionCodeOverride = variant.versionCode * 10 + abiVersionCode + } + } +} + diff --git a/workout-logger/android/app/src/main/AndroidManifest.xml b/workout-logger/android/app/src/main/AndroidManifest.xml index 0c0b757..6e23a42 100644 --- a/workout-logger/android/app/src/main/AndroidManifest.xml +++ b/workout-logger/android/app/src/main/AndroidManifest.xml @@ -49,6 +49,13 @@ + + + + separatorBuilder: (_, _) => const SizedBox(height: AppSpacing.sm), itemBuilder: (_, i) { final c = conversations[i]; diff --git a/workout-logger/lib/screens/analytics_screen.dart b/workout-logger/lib/screens/analytics_screen.dart index 73b9764..4708d3b 100644 --- a/workout-logger/lib/screens/analytics_screen.dart +++ b/workout-logger/lib/screens/analytics_screen.dart @@ -89,7 +89,7 @@ class _AnalyticsScreenState extends State { padding: const EdgeInsets.all(3), decoration: BoxDecoration( color: AppColors.glass2, - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(AppRadius.button), border: Border.all(color: AppColors.glassBorder), ), child: Row( @@ -99,7 +99,7 @@ class _AnalyticsScreenState extends State { child: GestureDetector( onTap: () => setState(() => _tab = i), child: AnimatedContainer( - duration: const Duration(milliseconds: 200), + duration: AppDurations.normal, curve: Curves.easeOut, padding: const EdgeInsets.symmetric(vertical: 8), decoration: BoxDecoration( @@ -546,7 +546,7 @@ class _FilterChip extends StatelessWidget { return GestureDetector( onTap: onTap, child: AnimatedContainer( - duration: const Duration(milliseconds: 160), + duration: AppDurations.fast, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( diff --git a/workout-logger/lib/screens/home_screen.dart b/workout-logger/lib/screens/home_screen.dart index 4be9887..5ad5b8f 100644 --- a/workout-logger/lib/screens/home_screen.dart +++ b/workout-logger/lib/screens/home_screen.dart @@ -532,7 +532,7 @@ class _DashboardTab extends StatelessWidget { ), const SizedBox(height: 6), AnimatedContainer( - duration: const Duration(milliseconds: 300), + duration: AppDurations.medium, height: 6, decoration: BoxDecoration( borderRadius: BorderRadius.circular(3), @@ -1298,7 +1298,7 @@ class _WeeklyInsightsCardState extends State<_WeeklyInsightsCard> { GestureDetector( onTap: _loading ? null : _refresh, child: AnimatedContainer( - duration: const Duration(milliseconds: 150), + duration: AppDurations.fast, padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: AppColors.primary.withValues(alpha: 0.12), diff --git a/workout-logger/lib/screens/profile_screen.dart b/workout-logger/lib/screens/profile_screen.dart index 60ccf00..ecfbfaa 100644 --- a/workout-logger/lib/screens/profile_screen.dart +++ b/workout-logger/lib/screens/profile_screen.dart @@ -263,7 +263,7 @@ class _ProfileScreenState extends State setState(() => _isImporting = true); try { - final result = await FilePicker.platform.pickFiles( + final result = await FilePicker.pickFiles( type: FileType.custom, allowedExtensions: ['json'], ); diff --git a/workout-logger/lib/screens/programs/import_program_screen.dart b/workout-logger/lib/screens/programs/import_program_screen.dart index 08938ed..81de167 100644 --- a/workout-logger/lib/screens/programs/import_program_screen.dart +++ b/workout-logger/lib/screens/programs/import_program_screen.dart @@ -266,7 +266,7 @@ class _ImportProgramScreenState extends State { Future _pickFile() async { try { - final result = await FilePicker.platform.pickFiles( + final result = await FilePicker.pickFiles( type: FileType.custom, allowedExtensions: ['json'], allowMultiple: false, diff --git a/workout-logger/lib/screens/programs/program_designer_screen.dart b/workout-logger/lib/screens/programs/program_designer_screen.dart index b3856dd..c7f5304 100644 --- a/workout-logger/lib/screens/programs/program_designer_screen.dart +++ b/workout-logger/lib/screens/programs/program_designer_screen.dart @@ -299,8 +299,8 @@ class _ProgramDesignerScreenState extends State { height: 32, decoration: BoxDecoration( color: week.isDeload - ? Colors.amber.withOpacity(0.2) - : AppTheme.primaryColor.withOpacity(0.2), + ? Colors.amber.withValues(alpha: 0.2) + : AppTheme.primaryColor.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(6), ), alignment: Alignment.center, @@ -875,6 +875,7 @@ class _ProgramDesignerScreenState extends State { ); if (proceed != true) return; } + if (!mounted) return; final provider = context.read(); final program = TrainingProgram( diff --git a/workout-logger/lib/screens/routine_optimizer_screen.dart b/workout-logger/lib/screens/routine_optimizer_screen.dart index 166e649..fb1de00 100644 --- a/workout-logger/lib/screens/routine_optimizer_screen.dart +++ b/workout-logger/lib/screens/routine_optimizer_screen.dart @@ -542,7 +542,7 @@ class _ConversationsSheet extends StatelessWidget { child: ListView.separated( shrinkWrap: true, itemCount: conversations.length, - separatorBuilder: (_, __) => + separatorBuilder: (_, _) => const SizedBox(height: AppSpacing.sm), itemBuilder: (_, i) { final c = conversations[i]; diff --git a/workout-logger/lib/screens/settings_screen.dart b/workout-logger/lib/screens/settings_screen.dart index c4191ed..ca5e33a 100644 --- a/workout-logger/lib/screens/settings_screen.dart +++ b/workout-logger/lib/screens/settings_screen.dart @@ -55,6 +55,7 @@ class _SettingsScreenState extends State { final jsonString = await provider.exportAllData(); final data = jsonDecode(jsonString) as Map; + if (!mounted) return; final api = context.read(); await api.trackEvent('backup_triggered').catchError((_) => null); final success = await api.backupData(data); @@ -89,9 +90,12 @@ class _SettingsScreenState extends State { final file = File('${tempDir.path}/$fileName'); await file.writeAsString(jsonString); - final result = await Share.shareXFiles([ - XFile(file.path), - ], subject: 'RepForge Backup'); + final result = await SharePlus.instance.share( + ShareParams( + files: [XFile(file.path)], + subject: 'RepForge Backup', + ), + ); if (result.status == ShareResultStatus.success || result.status == ShareResultStatus.dismissed) { @@ -110,6 +114,7 @@ class _SettingsScreenState extends State { // ==================== Local Import ==================== Future _importFromFile() async { + final provider = context.read(); // Show confirmation dialog final confirmed = await showDialog( context: context, @@ -154,7 +159,7 @@ class _SettingsScreenState extends State { setState(() => _isImporting = true); try { - final result = await FilePicker.platform.pickFiles( + final result = await FilePicker.pickFiles( type: FileType.custom, allowedExtensions: ['json'], ); @@ -166,6 +171,7 @@ class _SettingsScreenState extends State { final file = File(result.files.single.path!); final jsonString = await file.readAsString(); + if (!mounted) return; // Basic validation: ensure it's valid JSON with expected keys final data = jsonDecode(jsonString) as Map; @@ -174,8 +180,8 @@ class _SettingsScreenState extends State { return; } - final provider = context.read(); await provider.importData(jsonString); + if (!mounted) return; final itemCount = (data['sessions'] as List?)?.length ?? 0; final routineCount = (data['routines'] as List?)?.length ?? 0; @@ -239,7 +245,7 @@ class _SettingsScreenState extends State { Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( - color: AppTheme.primaryColor.withOpacity(0.1), + color: AppTheme.primaryColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: const Icon( @@ -325,7 +331,7 @@ class _SettingsScreenState extends State { ), selected: selected, onSelected: (_) => settings.setWeightIncrement(inc), - selectedColor: AppTheme.primaryColor.withOpacity(0.3), + selectedColor: AppTheme.primaryColor.withValues(alpha: 0.3), backgroundColor: AppTheme.surfaceColor, labelStyle: TextStyle( color: selected ? AppTheme.primaryColor : AppTheme.textSecondary, @@ -369,7 +375,7 @@ class _SettingsScreenState extends State { Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( - color: AppTheme.secondaryColor.withOpacity(0.1), + color: AppTheme.secondaryColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: const Icon( @@ -480,7 +486,7 @@ class _SettingsScreenState extends State { Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( - color: AppTheme.primaryColor.withOpacity(0.1), + color: AppTheme.primaryColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: const Icon( @@ -564,7 +570,7 @@ class _UnitButton extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 10), decoration: BoxDecoration( color: selected - ? AppTheme.primaryColor.withOpacity(0.2) + ? AppTheme.primaryColor.withValues(alpha: 0.2) : AppTheme.surfaceColor, borderRadius: BorderRadius.circular(8), border: Border.all( diff --git a/workout-logger/lib/screens/widgets/analytics_overview.dart b/workout-logger/lib/screens/widgets/analytics_overview.dart index cb2a286..c6ae88d 100644 --- a/workout-logger/lib/screens/widgets/analytics_overview.dart +++ b/workout-logger/lib/screens/widgets/analytics_overview.dart @@ -325,7 +325,7 @@ class _VolumeTrendCardState extends State<_VolumeTrendCard> { isStrokeCapRound: true, dotData: FlDotData( show: weeks <= 12, - getDotPainter: (_, __, ___, ____) => + getDotPainter: (_, _, _, _) => FlDotCirclePainter( radius: 3.5, color: AppColors.primary, @@ -378,7 +378,7 @@ class _RangeToggle extends StatelessWidget { return GestureDetector( onTap: () => onChanged(r), child: AnimatedContainer( - duration: const Duration(milliseconds: 180), + duration: AppDurations.fast, curve: Curves.easeOut, padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( @@ -644,7 +644,7 @@ class _FrequencyGrid extends StatelessWidget { color: active ? AppColors.primary.withValues(alpha: 0.12 + count * 0.06) : AppColors.glass2, - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(AppRadius.button), border: Border.all( color: active ? AppColors.primary.withValues(alpha: 0.4) diff --git a/workout-logger/lib/screens/widgets/exercise_progress_view.dart b/workout-logger/lib/screens/widgets/exercise_progress_view.dart index b56ec6c..ed499e8 100644 --- a/workout-logger/lib/screens/widgets/exercise_progress_view.dart +++ b/workout-logger/lib/screens/widgets/exercise_progress_view.dart @@ -699,7 +699,7 @@ class _ChartModeToggle extends StatelessWidget { GestureDetector( onTap: () => onChanged(mode), child: AnimatedContainer( - duration: const Duration(milliseconds: 180), + duration: AppDurations.fast, padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: mode == value ? AppColors.primary : Colors.transparent, @@ -787,7 +787,7 @@ class _VolumeChart extends StatelessWidget { barWidth: 2.5, dotData: FlDotData( show: true, - getDotPainter: (_, __, ___, ____) => FlDotCirclePainter( + getDotPainter: (_, _, _, _) => FlDotCirclePainter( radius: 3, color: AppColors.secondary, strokeWidth: 1.5, @@ -1350,13 +1350,13 @@ class _ToggleLegend extends StatelessWidget { onTap: onTap, behavior: HitTestBehavior.opaque, child: AnimatedOpacity( - duration: const Duration(milliseconds: 200), + duration: AppDurations.normal, opacity: active ? 1.0 : 0.32, child: Row( mainAxisSize: MainAxisSize.min, children: [ AnimatedContainer( - duration: const Duration(milliseconds: 200), + duration: AppDurations.normal, width: 10, height: 10, decoration: BoxDecoration( @@ -1402,7 +1402,7 @@ class _SetModeToggle extends StatelessWidget { GestureDetector( onTap: () => onChanged(mode), child: AnimatedContainer( - duration: const Duration(milliseconds: 160), + duration: AppDurations.fast, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: mode == value ? AppColors.primary : Colors.transparent, diff --git a/workout-logger/lib/screens/widgets/profile_sections.dart b/workout-logger/lib/screens/widgets/profile_sections.dart index 55a0c0e..d6bbee3 100644 --- a/workout-logger/lib/screens/widgets/profile_sections.dart +++ b/workout-logger/lib/screens/widgets/profile_sections.dart @@ -76,7 +76,7 @@ class _ProfileSection extends StatelessWidget { ], ), ), - if (trailing != null) trailing!, + ?trailing, ], ), const SizedBox(height: AppSpacing.md), @@ -154,7 +154,7 @@ class PreferencesSection extends StatelessWidget { settings.setWeightIncrement(inc); }, child: AnimatedContainer( - duration: const Duration(milliseconds: 150), + duration: AppDurations.fast, padding: const EdgeInsets.symmetric( horizontal: 14, vertical: 7, @@ -582,7 +582,7 @@ class _UnitToggleButton extends StatelessWidget { return GestureDetector( onTap: onTap, child: AnimatedContainer( - duration: const Duration(milliseconds: 150), + duration: AppDurations.fast, padding: const EdgeInsets.symmetric(vertical: 11), decoration: BoxDecoration( color: selected @@ -906,7 +906,7 @@ class _AiSettingsSectionState extends State { return GestureDetector( onTap: () => _selectModel(id), child: AnimatedContainer( - duration: const Duration(milliseconds: 150), + duration: AppDurations.fast, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7), decoration: BoxDecoration( color: selected @@ -936,7 +936,7 @@ class _AiSettingsSectionState extends State { SizedBox( width: double.infinity, child: AnimatedContainer( - duration: const Duration(milliseconds: 150), + duration: AppDurations.fast, decoration: BoxDecoration( color: AppColors.primary.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(AppRadius.sm), diff --git a/workout-logger/lib/screens/widgets/program_week_editor.dart b/workout-logger/lib/screens/widgets/program_week_editor.dart index ff67ed8..54e2db4 100644 --- a/workout-logger/lib/screens/widgets/program_week_editor.dart +++ b/workout-logger/lib/screens/widgets/program_week_editor.dart @@ -208,7 +208,7 @@ class _ProgramWeekEditorStepState extends State { style: TextStyle(color: AppColors.textPrimary, fontSize: 13), ), value: week.isDeload, - activeColor: Colors.amber, + activeThumbColor: Colors.amber, onChanged: (v) { final updated = List.from(_weeks); updated[idx] = week.copyWith(isDeload: v); diff --git a/workout-logger/lib/screens/widgets/rf_cards.dart b/workout-logger/lib/screens/widgets/rf_cards.dart index 4e5d35e..589c90a 100644 --- a/workout-logger/lib/screens/widgets/rf_cards.dart +++ b/workout-logger/lib/screens/widgets/rf_cards.dart @@ -151,7 +151,7 @@ class SessionCard extends StatelessWidget { fontWeight: FontWeight.w700, ), ), - if (trailing != null) trailing!, + ?trailing, ], ), ), diff --git a/workout-logger/lib/screens/widgets/rf_inputs.dart b/workout-logger/lib/screens/widgets/rf_inputs.dart index bfde525..46e9e53 100644 --- a/workout-logger/lib/screens/widgets/rf_inputs.dart +++ b/workout-logger/lib/screens/widgets/rf_inputs.dart @@ -352,7 +352,7 @@ class RFToggle extends StatelessWidget { return GestureDetector( onTap: () => onChanged(i), child: AnimatedContainer( - duration: const Duration(milliseconds: 180), + duration: AppDurations.fast, curve: Curves.easeInOut, padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md), decoration: BoxDecoration( diff --git a/workout-logger/lib/screens/widgets/rf_widgets.dart b/workout-logger/lib/screens/widgets/rf_widgets.dart index 14ae34e..261a498 100644 --- a/workout-logger/lib/screens/widgets/rf_widgets.dart +++ b/workout-logger/lib/screens/widgets/rf_widgets.dart @@ -11,8 +11,8 @@ import '../../theme/app_theme.dart'; // Right-to-left slide push, shared by the home screen and detail entry points. PageRouteBuilder slideRoute(Widget page) { return PageRouteBuilder( - pageBuilder: (_, __, ___) => page, - transitionsBuilder: (_, anim, __, child) => SlideTransition( + pageBuilder: (_, _, _) => page, + transitionsBuilder: (_, anim, _, child) => SlideTransition( position: Tween( begin: const Offset(1, 0), end: Offset.zero, @@ -96,11 +96,9 @@ class GlassCard extends StatelessWidget { } // ── AmbientGlow ────────────────────────────────────────────────────────────── -// Decorative ambient gradient wash — place inside a Stack as first child. // Matches the design's rf-ambient pseudo-elements. class AmbientGlow extends StatelessWidget { - const AmbientGlow({super.key, this.showBottom = true}); - final bool showBottom; + const AmbientGlow({super.key}); @override Widget build(BuildContext context) { @@ -130,26 +128,6 @@ class AmbientGlow extends StatelessWidget { ), ), ), - // Bottom cyan wash - if (showBottom) - Positioned( - bottom: -200, - right: -100, - child: Container( - width: 400, - height: 400, - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: RadialGradient( - colors: [ - AppColors.secondary.withValues(alpha: 0.20), - Colors.transparent, - ], - stops: const [0, 0.6], - ), - ), - ), - ), ], ), ), @@ -260,7 +238,7 @@ class _NavItem extends StatelessWidget { children: [ // Accent indicator above icon AnimatedContainer( - duration: const Duration(milliseconds: 200), + duration: AppDurations.normal, width: active ? 18 : 0, height: 2, margin: const EdgeInsets.only(bottom: 4), @@ -334,8 +312,8 @@ class _GlowButtonState extends State super.initState(); _ctrl = AnimationController( vsync: this, - duration: const Duration(milliseconds: 100), - reverseDuration: const Duration(milliseconds: 200), + duration: AppDurations.micro, + reverseDuration: AppDurations.normal, lowerBound: 0.95, upperBound: 1.0, value: 1.0, @@ -562,7 +540,7 @@ class RFSectionHeader extends StatelessWidget { letterSpacing: 1.2, ), ), - if (trailing != null) trailing!, + ?trailing, ], ), ); @@ -638,7 +616,7 @@ class AnimatedCounter extends StatelessWidget { this.style, this.decimals = 0, this.suffix = '', - this.duration = const Duration(milliseconds: 800), + this.duration = AppDurations.xslow, }); final double value; @@ -834,7 +812,7 @@ class _RFLoadingDotsState extends State final c = widget.color ?? AppColors.primary; return AnimatedBuilder( animation: _ctrl, - builder: (_, __) { + builder: (_, _) { return Row( mainAxisSize: MainAxisSize.min, children: List.generate(3, (i) { @@ -889,7 +867,7 @@ class RFProgressBar extends StatelessWidget { return Stack( children: [ AnimatedContainer( - duration: const Duration(milliseconds: 600), + duration: AppDurations.slow, curve: Curves.easeOutCubic, width: constraints.maxWidth * clamped, decoration: BoxDecoration( @@ -1046,7 +1024,7 @@ class _SkeletonBoxState extends State Widget build(BuildContext context) { return AnimatedBuilder( animation: _ctrl, - builder: (_, __) => Container( + builder: (_, _) => Container( width: widget.width, height: widget.height, decoration: BoxDecoration( diff --git a/workout-logger/lib/screens/workout_flow_screen.dart b/workout-logger/lib/screens/workout_flow_screen.dart index cdcab5b..24bbd90 100644 --- a/workout-logger/lib/screens/workout_flow_screen.dart +++ b/workout-logger/lib/screens/workout_flow_screen.dart @@ -399,7 +399,7 @@ class _WorkoutFlowScreenState extends State { padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( color: AppColors.glass2, - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(AppRadius.button), border: Border.all(color: AppColors.glassBorderStrong), ), child: Row( @@ -435,7 +435,7 @@ class _WorkoutFlowScreenState extends State { padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14), decoration: BoxDecoration( color: isLast ? AppColors.success : AppColors.primary, - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(AppRadius.button), boxShadow: [ BoxShadow( color: (isLast ? AppColors.success : AppColors.primary) diff --git a/workout-logger/lib/services/ai/coach_tool_service.dart b/workout-logger/lib/services/ai/coach_tool_service.dart index 1e929cf..c43c384 100644 --- a/workout-logger/lib/services/ai/coach_tool_service.dart +++ b/workout-logger/lib/services/ai/coach_tool_service.dart @@ -348,7 +348,7 @@ class CoachToolService { return { 'exercise': exercise.name, 'session_count': progression.length, - if (days != null) 'window_days': days, + 'window_days': ?days, 'volume_trend': [ for (final p in progression.length > trendCap ? progression.sublist(progression.length - trendCap) @@ -472,7 +472,7 @@ class CoachToolService { 'routine': routine.name, 'exercises': [for (final id in routine.exerciseIds) _wp.getExerciseName(id)], 'session_count': sessions.length, - if (days != null) 'window_days': days, + 'window_days': ?days, 'total_volume': _round(totalVolume), 'volume_over_time': [ for (final s in sessions.length > _limitArg(args, 40) diff --git a/workout-logger/lib/services/api_service.dart b/workout-logger/lib/services/api_service.dart index e66213a..4b93765 100644 --- a/workout-logger/lib/services/api_service.dart +++ b/workout-logger/lib/services/api_service.dart @@ -92,7 +92,7 @@ class ApiService { 'event': event, 'platform': _platform, 'timestamp': DateTime.now().toUtc().toIso8601String(), - if (metadata != null) 'metadata': metadata, + 'metadata': ?metadata, }; final res = await _client .post( diff --git a/workout-logger/lib/theme/app_theme.dart b/workout-logger/lib/theme/app_theme.dart index 5e2bcef..1b27eb2 100644 --- a/workout-logger/lib/theme/app_theme.dart +++ b/workout-logger/lib/theme/app_theme.dart @@ -141,7 +141,7 @@ class AppTheme { elevation: 0, padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(AppRadius.button), ), textStyle: TextStyle(fontFamily: 'Geist', fontSize: 14, @@ -156,7 +156,7 @@ class AppTheme { side: const BorderSide(color: AppColors.primary), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(AppRadius.button), ), ), ), @@ -289,14 +289,35 @@ class AppSpacing { class AppRadius { const AppRadius._(); + static const double xs = 6; // micro pill, small inner accents static const double sm = 8; static const double md = 12; static const double lg = 16; - static const double xl = 18; // glass card radius - static const double xxl = 22; // nav pill radius + static const double button = 14; // standard button shape radius + static const double xl = 18; // glass card radius + static const double xxl = 22; // nav pill radius static const double full = 999; } +// ── Animation Durations ────────────────────────────────────────────────────── +class AppDurations { + const AppDurations._(); + /// 100ms — instant toggles, checkbox presses + static const micro = Duration(milliseconds: 100); + /// 150ms — press state feedback, chip selection + static const fast = Duration(milliseconds: 150); + /// 200ms — standard UI transitions (tab switches, card expands) + static const normal = Duration(milliseconds: 200); + /// 250ms — slightly heavier containers + static const moderate = Duration(milliseconds: 250); + /// 300ms — page & modal slide transitions + static const medium = Duration(milliseconds: 300); + /// 600ms — progress rings, loading fills + static const slow = Duration(milliseconds: 600); + /// 800ms — counter roll-up animations + static const xslow = Duration(milliseconds: 800); +} + class AppBreakpoints { const AppBreakpoints._(); diff --git a/workout-logger/pubspec.lock b/workout-logger/pubspec.lock index bdc931d..a22582b 100644 --- a/workout-logger/pubspec.lock +++ b/workout-logger/pubspec.lock @@ -237,10 +237,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "57d9a1dd5063f85fa3107fb42d1faffda52fdc948cefd5fe5ea85267a5fc7343" + sha256: f13a03000d942e476bc1ff0a736d2e9de711d2f89a95cd4c1d88f861c3348387 url: "https://pub.dev" source: hosted - version: "10.3.10" + version: "11.0.2" fixnum: dependency: transitive description: @@ -274,10 +274,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "6.0.0" flutter_math_fork: dependency: transitive description: @@ -508,10 +508,10 @@ packages: dependency: transitive description: name: lints - sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "6.1.0" logging: dependency: transitive description: diff --git a/workout-logger/pubspec.yaml b/workout-logger/pubspec.yaml index 53c39b5..0fe39e4 100644 --- a/workout-logger/pubspec.yaml +++ b/workout-logger/pubspec.yaml @@ -59,7 +59,7 @@ dependencies: google_generative_ai: ^0.4.3 # Backup export/import - file_picker: ^10.3.10 + file_picker: ^11.0.2 path_provider: ^2.1.5 share_plus: ^12.0.1 gpt_markdown: ^1.1.7 @@ -73,7 +73,7 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^5.0.0 + flutter_lints: ^6.0.0 flutter_launcher_icons: ^0.13.1 # Testing