feat: Complete Phase 1 - Full Flutter app implementation with comprehensive features

Version: 1.1.0

Major changes:
- Implemented complete Flutter app structure with all core features
- Added comprehensive UI screens for auth, countdown, goals, profile, settings, and social features
- Integrated Supabase backend with authentication and data repositories
- Added offline support with Hive caching and local storage
- Implemented comprehensive routing with go_router
- Added location services with Google Maps integration
- Implemented notifications and home widget support
- Added voice recording capabilities and AI chat features
- Created comprehensive test suite and documentation
- Added Android and iOS platform configurations
- Implemented achievements system and social features
- Added calendar integration and bucket list functionality

This represents a complete Phase 1 milestone with 3,775 additions across 31 files.
This commit is contained in:
Tomas Dvorak
2026-01-04 14:33:54 +01:00
parent 1a29315672
commit 37ffb93923
210 changed files with 29417 additions and 477 deletions
@@ -0,0 +1,216 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../core/widgets/app_scaffold.dart';
class AboutChallengeScreen extends StatelessWidget {
const AboutChallengeScreen({super.key});
@override
Widget build(BuildContext context) {
return AppScaffold(
body: ListView(
padding: const EdgeInsets.all(16.0),
children: [
const SizedBox(height: 16),
Center(
child: Icon(
Icons.timer_outlined,
size: 80,
color: Theme.of(context).colorScheme.primary,
),
),
const SizedBox(height: 24),
Center(
child: Text(
'The 1356-Day Challenge',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 16),
_buildSection(
context,
title: 'What is it?',
content: 'The 1356-Day Challenge is a personal commitment to achieve your goals within exactly 1356 days (approximately 3 years, 8 months, and 11 days). Once you start your countdown, there is no stopping, pausing, or extending it.',
),
_buildSection(
context,
title: 'How it works',
content: '1. Create your bucket list with 1-20 goals\n'
'2. Add milestones and track your progress\n'
'3. Finalize your list to start the countdown\n'
'4. Work towards completing your goals before time runs out',
),
_buildSection(
context,
title: 'The Rules',
content: '• You can create between 1 and 20 goals\n'
'• The countdown only starts after you finalize your list\n'
'• Once started, the countdown cannot be paused or reset\n'
'• You can track progress but cannot change the duration\n'
'• After 1356 days, the challenge ends',
),
_buildSection(
context,
title: 'Why 1356 days?',
content: '1356 days represents approximately 3.7 years - a meaningful timeframe that\'s long enough to achieve significant life goals but short enough to maintain urgency and motivation. It\'s the perfect balance between ambition and achievability.',
),
_buildSection(
context,
title: 'Tips for Success',
content: '• Choose goals that truly matter to you\n'
'• Break large goals into smaller milestones\n'
'• Update your progress regularly\n'
'• Stay motivated by tracking your achievements\n'
'• Share your journey with others (optional)',
),
_buildSection(
context,
title: 'Privacy',
content: 'Your goals and progress are private by default. You can choose to make your profile public to share your achievements with the community, but your detailed goal information remains private.',
),
const SizedBox(height: 24),
Center(
child: Text(
'About Project 1356',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 16),
_buildSection(
context,
title: 'Origin of Project 1356',
content:
'Project 1356 began in April 2022 as a mysterious social media countdown created by Armin Mehdizadeh. Every day, he posted a short video erasing a number on a whiteboard and writing the next lower number, counting down from 1,356 to zero. The countdown was set to end on January 1, 2026, exactly 1,356 days after it began - a number chosen after asking Google how many days were left until that date.',
),
_buildSection(
context,
title: 'The Reveal',
content:
'On January 1, 2026, Armin revealed that Project 1356 was a personal challenge: achieve six major life goals in 1,356 days or face public embarrassment. The goals included reaching 100,000 YouTube subscribers, earning \$10,000 per month, building a business that makes \$10,000 monthly, reaching a weight of 185 pounds, earning a business administration degree, and becoming a skilled music producer. In the end, he achieved two of the goals related to income and business revenue but did not complete the others.',
),
_buildSection(
context,
title: 'More Than One Person\'s Story',
content:
'Over time, the project grew far beyond Armin\'s personal journey. Thousands of followers started using the countdown idea to track their own milestones - quitting alcohol, improving fitness, getting married, changing careers, and more. The real value was not just reaching zero, but the transformation that happened during the 1,356 days.',
),
_buildSection(
context,
title: 'Project 1356 - Part 2',
content:
'On January 7, 2026, Armin announced Project 1356 Part 2. Instead of everyone watching his countdown, he invited people to set their own six life-changing goals for the next 1,356 days. Participants do not have to reveal their goals publicly, but they commit to the long-term journey of accountability and growth.',
),
Card(
margin: const EdgeInsets.only(bottom: 16),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Follow Project 1356 & Armin',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
const SizedBox(height: 12),
Text(
'To follow the creator and the ongoing community around Project 1356:',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
height: 1.5,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
OutlinedButton.icon(
onPressed: () => _openLink(
context,
'https://www.instagram.com/project.1356/',
),
icon: const Icon(Icons.camera_alt_outlined),
label: const Text('Instagram'),
),
OutlinedButton.icon(
onPressed: () => _openLink(
context,
'https://www.youtube.com/@arminmehdiz',
),
icon: const Icon(Icons.ondemand_video_outlined),
label: const Text('YouTube'),
),
],
),
],
),
),
),
const SizedBox(height: 24),
FilledButton.icon(
onPressed: () => context.pop(),
icon: const Icon(Icons.check),
label: const Text('Got it!'),
),
const SizedBox(height: 16),
],
),
);
}
Widget _buildSection(BuildContext context, {
required String title,
required String content,
}) {
return Card(
margin: const EdgeInsets.only(bottom: 16),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
const SizedBox(height: 12),
Text(
content,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
height: 1.5,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
);
}
Future<void> _openLink(BuildContext context, String url) async {
final uri = Uri.parse(url);
final launched = await launchUrl(
uri,
mode: LaunchMode.externalApplication,
);
if (!launched) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Could not open link')),
);
}
}
}
@@ -0,0 +1,206 @@
// ignore_for_file: deprecated_member_use
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../core/widgets/app_scaffold.dart';
enum ThemeMode { light, dark, system }
enum TimeFormat { twelveHour, twentyFourHour }
class AppearanceSettingsScreen extends ConsumerStatefulWidget {
const AppearanceSettingsScreen({super.key});
@override
ConsumerState<AppearanceSettingsScreen> createState() => _AppearanceSettingsScreenState();
}
class _AppearanceSettingsScreenState extends ConsumerState<AppearanceSettingsScreen> {
ThemeMode _themeMode = ThemeMode.system;
TimeFormat _timeFormat = TimeFormat.twelveHour;
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadPreferences();
}
Future<void> _loadPreferences() async {
final prefs = await SharedPreferences.getInstance();
final themeModeIndex = prefs.getInt('theme_mode') ?? 2;
final timeFormatIndex = prefs.getInt('time_format') ?? 0;
setState(() {
_themeMode = ThemeMode.values[themeModeIndex];
_timeFormat = TimeFormat.values[timeFormatIndex];
_isLoading = false;
});
}
Future<void> _saveThemeMode(ThemeMode mode) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('theme_mode', mode.index);
setState(() => _themeMode = mode);
}
Future<void> _saveTimeFormat(TimeFormat format) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('time_format', format.index);
setState(() => _timeFormat = format);
}
@override
Widget build(BuildContext context) {
return AppScaffold(
title: 'Appearance',
body: _isLoading
? const Center(child: CircularProgressIndicator())
: ListView(
children: [
_buildSection(
context,
title: 'Theme',
children: [
RadioListTile<ThemeMode>(
title: const Text('Light'),
subtitle: const Text('Always use light theme'),
value: ThemeMode.light,
groupValue: _themeMode,
onChanged: (value) {
if (value != null) {
_saveThemeMode(value);
}
},
),
RadioListTile<ThemeMode>(
title: const Text('Dark'),
subtitle: const Text('Always use dark theme'),
value: ThemeMode.dark,
groupValue: _themeMode,
onChanged: (value) {
if (value != null) {
_saveThemeMode(value);
}
},
),
RadioListTile<ThemeMode>(
title: const Text('System Default'),
subtitle: const Text('Follow device theme settings'),
value: ThemeMode.system,
groupValue: _themeMode,
onChanged: (value) {
if (value != null) {
_saveThemeMode(value);
}
},
),
],
),
_buildSection(
context,
title: 'Time Format',
children: [
RadioListTile<TimeFormat>(
title: const Text('12-hour'),
subtitle: const Text('e.g., 3:30 PM'),
value: TimeFormat.twelveHour,
groupValue: _timeFormat,
onChanged: (value) {
if (value != null) {
_saveTimeFormat(value);
}
},
),
RadioListTile<TimeFormat>(
title: const Text('24-hour'),
subtitle: const Text('e.g., 15:30'),
value: TimeFormat.twentyFourHour,
groupValue: _timeFormat,
onChanged: (value) {
if (value != null) {
_saveTimeFormat(value);
}
},
),
],
),
_buildSection(
context,
title: 'Preview',
children: [
Card(
margin: const EdgeInsets.all(16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Countdown Preview',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
_formatTimePreview(),
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
),
),
const SizedBox(height: 8),
Text(
'Days remaining in your challenge',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
),
],
),
const SizedBox(height: 32),
],
),
);
}
String _formatTimePreview() {
final now = DateTime.now();
final hours = now.hour;
final minutes = now.minute.toString().padLeft(2, '0');
if (_timeFormat == TimeFormat.twentyFourHour) {
return '$hours:$minutes';
} else {
final period = hours >= 12 ? 'PM' : 'AM';
final displayHours = hours > 12 ? hours - 12 : (hours == 0 ? 12 : hours);
return '$displayHours:$minutes $period';
}
}
Widget _buildSection(BuildContext context, {
required String title,
required List<Widget> children,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 24, 16, 8),
child: Text(
title,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
),
...children,
],
);
}
}
@@ -0,0 +1,264 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../core/widgets/app_scaffold.dart';
final notificationSettingsProvider = StateNotifierProvider<NotificationSettingsController, NotificationSettings>((ref) {
return NotificationSettingsController();
});
class NotificationSettingsController extends StateNotifier<NotificationSettings> {
NotificationSettingsController() : super(const NotificationSettings());
void updateCountdownReminder(Frequency frequency) {
state = state.copyWith(countdownReminderFrequency: frequency);
}
void updateGoalProgress(bool enabled) {
state = state.copyWith(goalProgressNotifications: enabled);
}
void updateMilestoneAlerts(bool enabled) {
state = state.copyWith(milestoneAlerts: enabled);
}
void updateCountdownCheckpoints(bool enabled) {
state = state.copyWith(countdownCheckpoints: enabled);
}
}
class NotificationSettings {
final Frequency countdownReminderFrequency;
final bool goalProgressNotifications;
final bool milestoneAlerts;
final bool countdownCheckpoints;
const NotificationSettings({
this.countdownReminderFrequency = Frequency.daily,
this.goalProgressNotifications = true,
this.milestoneAlerts = true,
this.countdownCheckpoints = true,
});
NotificationSettings copyWith({
Frequency? countdownReminderFrequency,
bool? goalProgressNotifications,
bool? milestoneAlerts,
bool? countdownCheckpoints,
}) {
return NotificationSettings(
countdownReminderFrequency: countdownReminderFrequency ?? this.countdownReminderFrequency,
goalProgressNotifications: goalProgressNotifications ?? this.goalProgressNotifications,
milestoneAlerts: milestoneAlerts ?? this.milestoneAlerts,
countdownCheckpoints: countdownCheckpoints ?? this.countdownCheckpoints,
);
}
}
enum Frequency {
never,
daily,
weekly,
custom,
}
extension FrequencyExtension on Frequency {
String get label {
switch (this) {
case Frequency.never:
return 'Never';
case Frequency.daily:
return 'Daily';
case Frequency.weekly:
return 'Weekly';
case Frequency.custom:
return 'Custom';
}
}
String get description {
switch (this) {
case Frequency.never:
return 'No reminders';
case Frequency.daily:
return 'Receive daily countdown reminders';
case Frequency.weekly:
return 'Receive weekly countdown reminders';
case Frequency.custom:
return 'Set custom reminder schedule';
}
}
}
class NotificationSettingsScreen extends ConsumerWidget {
const NotificationSettingsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final settings = ref.watch(notificationSettingsProvider);
return AppScaffold(
body: ListView(
children: [
_buildSection(
context,
title: 'Countdown Reminders',
children: [
_FrequencyTile(
title: 'Reminder Frequency',
subtitle: settings.countdownReminderFrequency.description,
currentFrequency: settings.countdownReminderFrequency,
onChanged: (frequency) {
ref.read(notificationSettingsProvider.notifier).updateCountdownReminder(frequency);
},
),
],
),
_buildSection(
context,
title: 'Goal Notifications',
children: [
_SwitchTile(
title: 'Goal Progress',
subtitle: 'Get notified about goal updates',
value: settings.goalProgressNotifications,
onChanged: (value) {
ref.read(notificationSettingsProvider.notifier).updateGoalProgress(value);
},
),
_SwitchTile(
title: 'Milestone Alerts',
subtitle: 'Celebrate when you complete milestones',
value: settings.milestoneAlerts,
onChanged: (value) {
ref.read(notificationSettingsProvider.notifier).updateMilestoneAlerts(value);
},
),
],
),
_buildSection(
context,
title: 'Countdown Checkpoints',
children: [
_SwitchTile(
title: 'Checkpoint Notifications',
subtitle: 'Get notified at 50%, 25%, and 10% remaining',
value: settings.countdownCheckpoints,
onChanged: (value) {
ref.read(notificationSettingsProvider.notifier).updateCountdownCheckpoints(value);
},
),
],
),
const SizedBox(height: 24),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: FilledButton.icon(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Notification preferences saved')),
);
context.pop();
},
icon: const Icon(Icons.save),
label: const Text('Save Preferences'),
),
),
const SizedBox(height: 16),
],
),
);
}
Widget _buildSection(BuildContext context, {
required String title,
required List<Widget> children,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 24, 16, 8),
child: Text(
title,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
),
...children,
],
);
}
}
class _SwitchTile extends StatelessWidget {
final String title;
final String subtitle;
final bool value;
final ValueChanged<bool> onChanged;
const _SwitchTile({
required this.title,
required this.subtitle,
required this.value,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return SwitchListTile(
title: Text(title),
subtitle: Text(
subtitle,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
value: value,
onChanged: onChanged,
);
}
}
class _FrequencyTile extends StatelessWidget {
final String title;
final String subtitle;
final Frequency currentFrequency;
final ValueChanged<Frequency> onChanged;
const _FrequencyTile({
required this.title,
required this.subtitle,
required this.currentFrequency,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(title),
subtitle: Text(
subtitle,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
trailing: DropdownButton<Frequency>(
value: currentFrequency,
items: Frequency.values.map((frequency) {
return DropdownMenuItem<Frequency>(
value: frequency,
child: Text(frequency.label),
);
}).toList(),
onChanged: (value) {
if (value != null) {
onChanged(value);
}
},
),
);
}
}
@@ -0,0 +1,307 @@
// ignore_for_file: deprecated_member_use
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../core/widgets/app_scaffold.dart';
import '../../profile/application/profile_controller.dart';
class PrivacySettingsScreen extends ConsumerStatefulWidget {
const PrivacySettingsScreen({super.key});
@override
ConsumerState<PrivacySettingsScreen> createState() => _PrivacySettingsScreenState();
}
class _PrivacySettingsScreenState extends ConsumerState<PrivacySettingsScreen> {
bool _isLoading = false;
@override
Widget build(BuildContext context) {
final profileState = ref.watch(profileControllerProvider);
final user = profileState.user;
return AppScaffold(
body: ListView(
children: [
_buildSection(
context,
title: 'Profile Visibility',
children: [
if (user != null)
_VisibilityTile(
title: 'Make Profile Public',
subtitle: user.isPublicProfile
? 'Your profile is visible to other users'
: 'Your profile is private and only visible to you',
isPublic: user.isPublicProfile,
onChanged: _isLoading
? null
: (value) => _toggleProfileVisibility(value, user.id),
),
const _InfoTile(
icon: Icons.info_outline,
title: 'What does public mean?',
description: 'When your profile is public, other users can see your username, avatar, and high-level stats. Your goals and detailed progress remain private.',
),
],
),
_buildSection(
context,
title: 'Data & Privacy',
children: [
_SettingsTile(
icon: Icons.download,
title: 'Export My Data',
subtitle: 'Download a copy of your personal data',
onTap: () => _showExportDataDialog(context),
),
_SettingsTile(
icon: Icons.block,
title: 'Blocked Users',
subtitle: 'Manage users you have blocked',
onTap: () => context.push('/settings/privacy/blocked'),
),
],
),
_buildSection(
context,
title: 'Account Control',
children: [
_SettingsTile(
icon: Icons.delete_forever,
title: 'Delete Account',
subtitle: 'Permanently delete your account and all data',
onTap: () => _showDeleteAccountDialog(context),
isDestructive: true,
),
],
),
const SizedBox(height: 24),
],
),
);
}
Widget _buildSection(BuildContext context, {
required String title,
required List<Widget> children,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 24, 16, 8),
child: Text(
title,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
),
...children,
],
);
}
Future<void> _toggleProfileVisibility(bool isPublic, String userId) async {
setState(() => _isLoading = true);
try {
await ref.read(profileControllerProvider.notifier).toggleProfileVisibility(userId);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(isPublic
? 'Your profile is now public'
: 'Your profile is now private'),
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to update visibility: $e')),
);
}
} finally {
if (mounted) {
setState(() => _isLoading = false);
}
}
}
void _showExportDataDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Export My Data'),
content: const Text(
'We will prepare a downloadable file containing your profile information, goals, and progress. This may take a few moments.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Data export request submitted. You will receive an email when ready.'),
),
);
},
child: const Text('Request Export'),
),
],
),
);
}
void _showDeleteAccountDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Delete Account'),
content: const Text(
'Are you sure you want to delete your account? This action cannot be undone and all your data will be permanently lost.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Account deletion requires email confirmation'),
),
);
},
style: TextButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.error,
),
child: const Text('Delete'),
),
],
),
);
}
}
class _VisibilityTile extends StatelessWidget {
final String title;
final String subtitle;
final bool isPublic;
final ValueChanged<bool>? onChanged;
const _VisibilityTile({
required this.title,
required this.subtitle,
required this.isPublic,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return SwitchListTile(
title: Text(title),
subtitle: Text(
subtitle,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
value: isPublic,
onChanged: onChanged,
secondary: Icon(
isPublic ? Icons.public : Icons.lock,
color: isPublic
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurfaceVariant,
),
);
}
}
class _InfoTile extends StatelessWidget {
final IconData icon;
final String title;
final String description;
const _InfoTile({
required this.icon,
required this.title,
required this.description,
});
@override
Widget build(BuildContext context) {
return ListTile(
leading: Icon(
icon,
color: Theme.of(context).colorScheme.primary.withOpacity(0.7),
),
title: Text(
title,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
),
),
subtitle: Text(
description,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
);
}
}
class _SettingsTile extends StatelessWidget {
final IconData icon;
final String title;
final String subtitle;
final VoidCallback onTap;
final bool isDestructive;
const _SettingsTile({
required this.icon,
required this.title,
required this.subtitle,
required this.onTap,
this.isDestructive = false,
});
@override
Widget build(BuildContext context) {
return ListTile(
leading: Icon(
icon,
color: isDestructive
? Theme.of(context).colorScheme.error
: Theme.of(context).colorScheme.primary,
),
title: Text(
title,
style: TextStyle(
color: isDestructive
? Theme.of(context).colorScheme.error
: null,
),
),
subtitle: Text(
subtitle,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
trailing: const Icon(Icons.chevron_right),
onTap: onTap,
);
}
}
@@ -1,16 +1,305 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:supabase_flutter/supabase_flutter.dart' as supabase;
import '../../../core/widgets/app_scaffold.dart';
class SettingsHomeScreen extends StatelessWidget {
class SettingsHomeScreen extends ConsumerWidget {
const SettingsHomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Settings'),
Widget build(BuildContext context, WidgetRef ref) {
return AppScaffold(
body: ListView(
children: [
_buildSection(
context,
title: 'Account',
children: [
_SettingsTile(
icon: Icons.person,
title: 'Edit Profile',
subtitle: 'Update your avatar, username, or bio',
onTap: () => context.push('/profile/edit'),
),
_SettingsTile(
icon: Icons.email,
title: 'Email',
subtitle: supabase.Supabase.instance.client.auth.currentUser?.email ?? '',
onTap: () => context.push('/settings/account'),
),
_SettingsTile(
icon: Icons.lock,
title: 'Change Password',
subtitle: 'Update your password',
onTap: () => context.push('/settings/account/password'),
),
],
),
_buildSection(
context,
title: 'Preferences',
children: [
_SettingsTile(
icon: Icons.palette,
title: 'Appearance',
subtitle: 'Theme, time format',
onTap: () => context.push('/settings/appearance'),
),
_SettingsTile(
icon: Icons.notifications,
title: 'Notifications',
subtitle: 'Reminders and alerts',
onTap: () => context.push('/settings/notifications'),
),
],
),
_buildSection(
context,
title: 'Privacy',
children: [
_SettingsTile(
icon: Icons.visibility,
title: 'Profile Visibility',
subtitle: 'Public or Private profile',
onTap: () => context.push('/settings/privacy'),
),
_SettingsTile(
icon: Icons.block,
title: 'Blocked Users',
subtitle: 'Manage blocked accounts',
onTap: () => context.push('/settings/privacy/blocked'),
),
],
),
_buildSection(
context,
title: 'About',
children: [
_SettingsTile(
icon: Icons.info_outline,
title: 'About the Challenge',
subtitle: 'Learn about the 1356-day challenge',
onTap: () => context.push('/settings/about'),
),
_SettingsTile(
icon: Icons.description,
title: 'Terms of Service',
subtitle: 'Legal terms and conditions',
onTap: () => _showTermsOfService(context),
),
_SettingsTile(
icon: Icons.privacy_tip,
title: 'Privacy Policy',
subtitle: 'How we handle your data',
onTap: () => _showPrivacyPolicy(context),
),
],
),
_buildSection(
context,
title: 'Danger Zone',
children: [
_SettingsTile(
icon: Icons.delete_forever,
title: 'Delete Account',
subtitle: 'Permanently delete your account and data',
onTap: () => _showDeleteAccountDialog(context),
isDestructive: true,
),
],
),
const SizedBox(height: 32),
Center(
child: Text(
'LifeTimer v1.0.0',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
const SizedBox(height: 16),
],
),
body: const Center(
child: Text('Settings - Coming Soon'),
);
}
Widget _buildSection(BuildContext context, {
required String title,
required List<Widget> children,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 24, 16, 8),
child: Text(
title,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
),
...children,
],
);
}
void _showTermsOfService(BuildContext context) {
showDialog(
context: context,
builder: (context) => Semantics(
label: 'Terms of Service dialog',
child: AlertDialog(
title: const Text('Terms of Service'),
content: const SingleChildScrollView(
child: Text(
'LifeTimer Terms of Service\n\n'
'1. Acceptance of Terms\n'
'By using LifeTimer, you agree to these terms.\n\n'
'2. User Responsibilities\n'
'Users are responsible for maintaining the security of their account.\n\n'
'3. Content\n'
'Users own their goals and progress data.\n\n'
'4. Service Availability\n'
'We strive to keep the service available but cannot guarantee 100% uptime.\n\n'
'5. Changes to Terms\n'
'We may update these terms from time to time.',
),
),
actions: [
Semantics(
button: true,
label: 'Close terms of service',
child: TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'),
),
),
],
),
),
);
}
void _showPrivacyPolicy(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Privacy Policy'),
content: const SingleChildScrollView(
child: Text(
'LifeTimer Privacy Policy\n\n'
'1. Data Collection\n'
'We collect only the data necessary to provide the service.\n\n'
'2. Data Usage\n'
'Your data is used to track your goals and countdown progress.\n\n'
'3. Data Security\n'
'We use industry-standard security measures to protect your data.\n\n'
'4. Public Profiles\n'
'You can choose to make your profile public or private.\n\n'
'5. Data Deletion\n'
'You can request deletion of your account and associated data.',
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'),
),
],
),
);
}
void _showDeleteAccountDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => Semantics(
label: 'Delete account confirmation dialog',
child: AlertDialog(
title: const Text('Delete Account'),
content: const Text(
'Are you sure you want to delete your account? This action cannot be undone and all your data will be permanently lost.',
),
actions: [
Semantics(
button: true,
label: 'Cancel account deletion',
child: TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
),
Semantics(
button: true,
label: 'Confirm account deletion',
hint: 'This action cannot be undone',
child: TextButton(
onPressed: () {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Account deletion requires confirmation via email')),
);
},
style: TextButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.error,
),
child: const Text('Delete'),
),
),
],
),
),
);
}
}
class _SettingsTile extends StatelessWidget {
final IconData icon;
final String title;
final String subtitle;
final VoidCallback onTap;
final bool isDestructive;
const _SettingsTile({
required this.icon,
required this.title,
required this.subtitle,
required this.onTap,
this.isDestructive = false,
});
@override
Widget build(BuildContext context) {
return Semantics(
button: true,
label: title,
hint: subtitle,
child: ListTile(
leading: Icon(
icon,
color: isDestructive
? Theme.of(context).colorScheme.error
: Theme.of(context).colorScheme.primary,
),
title: Text(
title,
style: TextStyle(
color: isDestructive
? Theme.of(context).colorScheme.error
: null,
),
),
subtitle: Text(
subtitle,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
trailing: const Icon(Icons.chevron_right),
onTap: onTap,
),
);
}