This commit is contained in:
Tomas Dvorak
2026-01-06 13:46:49 +01:00
18 changed files with 464 additions and 276 deletions
+2 -2
View File
@@ -7,7 +7,7 @@ plugins {
} }
android { android {
namespace = "com.example.lifetimer" namespace = "com.example.app1356"
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion ndkVersion = flutter.ndkVersion
@@ -23,7 +23,7 @@ android {
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.lifetimer" applicationId = "com.example.app1356"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config. // For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion minSdk = flutter.minSdkVersion
@@ -1,7 +1,13 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application <application
android:label="lifetimer" android:label="1356"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true"> android:usesCleartextTraffic="true">
@@ -30,7 +36,7 @@
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="lifetimer" /> <data android:scheme="app1356" />
</intent-filter> </intent-filter>
</activity> </activity>
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
@@ -1,10 +1,10 @@
package com.example.lifetimer package com.example.app1356
import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.widget.RemoteViews import android.widget.RemoteViews
import com.example.lifetimer.R import com.example.app1356.R
import es.antonborri.home_widget.HomeWidgetProvider import es.antonborri.home_widget.HomeWidgetProvider
class NextCountdownWidgetProvider : HomeWidgetProvider() { class NextCountdownWidgetProvider : HomeWidgetProvider() {
@@ -16,7 +16,7 @@ class NextCountdownWidgetProvider : HomeWidgetProvider() {
) { ) {
appWidgetIds.forEach { widgetId -> appWidgetIds.forEach { widgetId ->
val title = widgetData.getString("next_title", "Next goal") val title = widgetData.getString("next_title", "Next goal")
val subtitle = widgetData.getString("next_subtitle", "Open Lifetimer to see details") val subtitle = widgetData.getString("next_subtitle", "Open 1356 to see details")
val timeLeft = widgetData.getString("next_time_left", "0 days left") val timeLeft = widgetData.getString("next_time_left", "0 days left")
val views = RemoteViews(context.packageName, R.layout.next_countdown_widget).apply { val views = RemoteViews(context.packageName, R.layout.next_countdown_widget).apply {
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#1E1E1E" />
<corners android:radius="16dp" />
<stroke
android:width="1dp"
android:color="#33FFFFFF" />
</shape>
@@ -2,7 +2,8 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="#121212" android:background="#1A1A1A"
android:backgroundTint="#2A2A2A"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="vertical" android:orientation="vertical"
android:padding="12dp"> android:padding="12dp">
@@ -13,6 +14,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Next goal" android:text="Next goal"
android:textColor="#FFFFFF" android:textColor="#FFFFFF"
android:textStyle="bold"
android:textSize="16sp" android:textSize="16sp"
android:maxLines="1" android:maxLines="1"
android:ellipsize="end" /> android:ellipsize="end" />
@@ -23,7 +25,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:text="0 days left" android:text="0 days left"
android:textColor="#FFCC66" android:textColor="#4CAF50"
android:textStyle="bold"
android:textSize="20sp" android:textSize="20sp"
android:maxLines="1" android:maxLines="1"
android:ellipsize="end" /> android:ellipsize="end" />
@@ -33,7 +36,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="2dp" android:layout_marginTop="2dp"
android:text="Open Lifetimer to see details" android:text="Open 1356 to see details"
android:textColor="#B3FFFFFF" android:textColor="#B3FFFFFF"
android:textSize="12sp" android:textSize="12sp"
android:maxLines="2" android:maxLines="2"
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/widget_background"
android:gravity="center"
android:orientation="vertical"
android:padding="16dp">
<!-- 1356 Branding -->
<TextView
android:id="@+id/text_brand"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="1356"
android:textColor="#FFFFFF"
android:textSize="12sp"
android:textStyle="bold"
android:background="#4CAF50"
android:paddingHorizontal="8dp"
android:paddingVertical="4dp"
android:layout_marginBottom="12dp" />
<!-- Days Remaining -->
<TextView
android:id="@+id/text_days"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="1356"
android:textColor="#FFFFFF"
android:textSize="36sp"
android:textStyle="bold"
android:layout_marginBottom="4dp" />
<!-- Label -->
<TextView
android:id="@+id/text_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="days remaining"
android:textColor="#B3FFFFFF"
android:textSize="12sp"
android:layout_marginBottom="16dp" />
<!-- Progress Bar -->
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="match_parent"
android:layout_height="4dp"
android:layout_marginBottom="8dp"
style="?android:attr/progressBarStyleHorizontal"
android:progressTint="#4CAF50"
android:progressBackgroundTint="#33FFFFFF" />
<!-- Progress Text -->
<TextView
android:id="@+id/text_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0% Complete"
android:textColor="#B3FFFFFF"
android:textSize="10sp" />
</LinearLayout>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">1356</string>
<!-- Google Sign-In Configuration -->
<string name="default_web_client_id" translatable="false">YOUR_WEB_CLIENT_ID_HERE</string>
</resources>
@@ -84,27 +84,51 @@ class AuthRepository {
} }
Future<void> signInWithGoogle() async { Future<void> signInWithGoogle() async {
final GoogleSignIn googleSignIn = GoogleSignIn(); try {
final GoogleSignIn googleSignIn = GoogleSignIn(
scopes: ['email', 'profile'],
);
final googleUser = await googleSignIn.signIn(); // Check if user is already signed in
if (googleUser == null) { final googleUser = await googleSignIn.signInSilently();
throw Exception('Google sign-in was cancelled'); if (googleUser != null) {
await _handleGoogleUser(googleUser);
return;
}
// Sign in interactively
final interactiveUser = await googleSignIn.signIn();
if (interactiveUser == null) {
throw Exception('Google sign-in was cancelled');
}
await _handleGoogleUser(interactiveUser);
} catch (e) {
throw Exception('Google sign-in failed: ${e.toString()}');
} }
}
final googleAuth = await googleUser.authentication; Future<void> _handleGoogleUser(dynamic googleUser) async {
final idToken = googleAuth.idToken; try {
final googleAuth = await googleUser.authentication;
final idToken = googleAuth.idToken;
final accessToken = googleAuth.accessToken;
if (idToken == null) { if (idToken == null && accessToken == null) {
throw Exception('No ID token from Google sign-in'); throw Exception('No ID token or access token from Google sign-in');
} }
final response = await _client.auth.signInWithIdToken( final response = await _client.auth.signInWithIdToken(
provider: supabase.OAuthProvider.google, provider: supabase.OAuthProvider.google,
idToken: idToken, idToken: idToken,
); accessToken: accessToken,
);
if (response.user != null) { if (response.user != null) {
await _ensureUserProfileExists(response.user!.id, response.user!); await _ensureUserProfileExists(response.user!.id, response.user!);
}
} catch (e) {
throw Exception('Failed to authenticate with Google: ${e.toString()}');
} }
} }
@@ -102,7 +102,7 @@ class _AuthChoiceScreenState extends ConsumerState<AuthChoiceScreen> {
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
Text( Text(
'LifeTimer', '1356',
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.headlineLarge .headlineLarge
@@ -64,7 +64,7 @@ class AuthShowcaseScreen extends ConsumerWidget {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
'LifeTimer helps you design a 1356-day experiment, focus on a small set of meaningful goals, and see time as a single bold countdown.', '1356 helps you design a 1356-day experiment, focus on a small set of meaningful goals, and see time as a single bold countdown.',
style: theme.textTheme.bodyLarge?.copyWith( style: theme.textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurface.withValues(alpha:0.7), color: colorScheme.onSurface.withValues(alpha:0.7),
height: 1.6, height: 1.6,
@@ -111,7 +111,7 @@ class _SignInScreenState extends ConsumerState<SignInScreen> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
'LifeTimer', '1356',
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.titleMedium .titleMedium
@@ -91,7 +91,7 @@ class _SignUpScreenState extends ConsumerState<SignUpScreen> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
'LifeTimer', '1356',
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.titleMedium .titleMedium
@@ -30,7 +30,9 @@ class _HomeCountdownScreenState extends ConsumerState<HomeCountdownScreen> {
return AppScaffold( return AppScaffold(
body: SafeArea( body: SafeArea(
child: countdownState.isLoading child: Padding(
padding: const EdgeInsets.only(top: 30.0, bottom: 30.0),
child: countdownState.isLoading
? const Center(child: LoadingIndicator()) ? const Center(child: LoadingIndicator())
: countdownState.error != null : countdownState.error != null
? Center( ? Center(
@@ -156,7 +158,7 @@ class _CountdownActiveScreen extends StatelessWidget {
child: SingleChildScrollView( child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
child: Padding( child: Padding(
padding: const EdgeInsets.all(24.0), padding: const EdgeInsets.only(top: 30.0, left: 24.0, right: 24.0, bottom: 30.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
@@ -16,82 +16,67 @@ class OnboardingHowItWorksScreen extends ConsumerWidget {
return AppScaffold( return AppScaffold(
body: SafeArea( body: SafeArea(
child: Column( child: Padding(
children: [ padding: const EdgeInsets.only(top: 30.0, left: 24.0, right: 24.0, bottom: 30.0),
// Progress indicator and back button child: Column(
Padding( crossAxisAlignment: CrossAxisAlignment.stretch,
padding: const EdgeInsets.all(24.0), children: [
child: Row( // Progress Bar and Navigation
children: [ _OnboardingProgress(currentStep: 2, totalSteps: 3),
IconButton( const SizedBox(height: 24),
onPressed: () { Text(
context.pop(); 'How It Works',
}, style: Theme.of(context).textTheme.headlineLarge?.copyWith(
icon: const Icon(Icons.arrow_back), fontWeight: FontWeight.bold,
), ),
Expanded( textAlign: TextAlign.center,
child: LinearProgressIndicator(
value: 2 / 3, // Step 2 of 3
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.primary,
),
),
),
const SizedBox(width: 48), // Balance the back button
],
), ),
), const SizedBox(height: 32),
// Scrollable content const _StepCard(
Expanded( number: 1,
child: SingleChildScrollView( title: 'Create Your Bucket List',
padding: const EdgeInsets.symmetric(horizontal: 24.0), description: 'Add between 1 and 20 goals you want to achieve. Each goal can have a description, location, and image.',
child: Column( icon: Icons.edit_note,
crossAxisAlignment: CrossAxisAlignment.stretch, ),
children: [ const SizedBox(height: 16),
const SizedBox(height: 8), const _StepCard(
Text( number: 2,
'How It Works', title: 'Finalize Your List',
style: Theme.of(context).textTheme.headlineLarge?.copyWith( description: 'Once you\'re happy with your goals, confirm your bucket list. This action cannot be undone.',
fontWeight: FontWeight.bold, icon: Icons.lock,
), ),
textAlign: TextAlign.center, const SizedBox(height: 16),
const _StepCard(
number: 3,
title: 'Start Your 1356-Day Journey',
description: 'The countdown begins immediately. Track your progress and make every day count.',
icon: Icons.timer,
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextButton(
onPressed: () => context.pop(),
child: const Text('Back'),
), ),
const SizedBox(height: 20), ),
const _StepCard( const SizedBox(width: 16),
number: 1, Expanded(
title: 'Create Your Bucket List', flex: 2,
description: 'Add between 1 and 20 goals you want to achieve. Each goal can have a description, location, and image.', child: PrimaryButton(
icon: Icons.edit_note,
),
const SizedBox(height: 12),
const _StepCard(
number: 2,
title: 'Finalize Your List',
description: 'Once you\'re happy with your goals, confirm your bucket list. This action cannot be undone.',
icon: Icons.lock,
),
const SizedBox(height: 12),
const _StepCard(
number: 3,
title: 'Start Your 1356-Day Journey',
description: 'The countdown begins immediately. Track your progress and make every day count.',
icon: Icons.timer,
),
const SizedBox(height: 24),
PrimaryButton(
onPressed: () { onPressed: () {
controller.completeStep('how_it_works'); controller.completeStep('how_it_works');
context.push('/onboarding/motivation'); context.push('/onboarding/motivation');
}, },
text: 'Continue', text: 'Continue',
), ),
const SizedBox(height: 20), ),
], ],
),
), ),
), const SizedBox(height: 16),
], ],
),
), ),
), ),
); );
@@ -190,3 +175,44 @@ class _StepCard extends StatelessWidget {
); );
} }
} }
class _OnboardingProgress extends StatelessWidget {
final int currentStep;
final int totalSteps;
const _OnboardingProgress({
required this.currentStep,
required this.totalSteps,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Step $currentStep of $totalSteps',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
),
TextButton(
onPressed: () => context.pop(),
child: const Text('Back'),
),
],
),
const SizedBox(height: 12),
LinearProgressIndicator(
value: currentStep / totalSteps,
backgroundColor: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3),
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.primary,
),
),
],
);
}
}
@@ -16,105 +16,84 @@ class OnboardingIntroScreen extends ConsumerWidget {
return AppScaffold( return AppScaffold(
body: SafeArea( body: SafeArea(
child: Column( child: Padding(
children: [ padding: const EdgeInsets.only(top: 30.0, left: 24.0, right: 24.0, bottom: 30.0),
// Progress indicator and back button child: Column(
Padding( crossAxisAlignment: CrossAxisAlignment.stretch,
padding: const EdgeInsets.all(24.0), children: [
child: Row( // Progress Bar and Navigation
children: [ _OnboardingProgress(currentStep: 1, totalSteps: 3),
IconButton( const SizedBox(height: 48),
onPressed: () { const Icon(
// Can't go back from intro, go to auth choice Icons.timer_outlined,
context.push('/auth-choice'); size: 100,
}, color: null,
icon: const Icon(Icons.arrow_back),
),
Expanded(
child: LinearProgressIndicator(
value: 1 / 3, // Step 1 of 3
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.primary,
),
),
),
const SizedBox(width: 48), // Balance the back button
],
), ),
), const SizedBox(height: 32),
// Scrollable content Text(
Expanded( 'Welcome to 1356',
child: SingleChildScrollView( style: Theme.of(context).textTheme.headlineLarge?.copyWith(
padding: const EdgeInsets.symmetric(horizontal: 24.0), fontWeight: FontWeight.bold,
child: Column( ),
crossAxisAlignment: CrossAxisAlignment.stretch, textAlign: TextAlign.center,
children: [ ),
const SizedBox(height: 16), const SizedBox(height: 16),
const Icon( Text(
Icons.timer_outlined, 'Your 1356-day journey starts here.\nCreate your bucket list and begin your countdown.',
size: 64, style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: null, color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
), height: 1.5,
const SizedBox(height: 20), ),
Text( textAlign: TextAlign.center,
'Welcome to LifeTimer', ),
style: Theme.of(context).textTheme.headlineLarge?.copyWith( const SizedBox(height: 48),
fontWeight: FontWeight.bold, const _FeatureCard(
), icon: Icons.flag,
textAlign: TextAlign.center, title: 'Set Your Goals',
), description: 'Create a bucket list of 1-20 meaningful goals',
const SizedBox(height: 10), ),
Text( const SizedBox(height: 16),
'Your 1356-day journey starts here.\nCreate your bucket list and begin your countdown.', const _FeatureCard(
style: Theme.of(context).textTheme.bodyLarge?.copyWith( icon: Icons.lock_clock,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8), title: 'Fixed Timeline',
height: 1.5, description: '1356 days to achieve everything - no extensions',
), ),
textAlign: TextAlign.center, const SizedBox(height: 16),
), const _FeatureCard(
const SizedBox(height: 24), icon: Icons.trending_up,
const _FeatureCard( title: 'Track Progress',
icon: Icons.flag, description: 'Watch yourself grow day by day',
title: 'Set Your Goals', ),
description: 'Create a bucket list of 1-20 meaningful goals', const Spacer(),
), Row(
const SizedBox(height: 10), children: [
const _FeatureCard( Expanded(
icon: Icons.lock_clock, child: TextButton(
title: 'Fixed Timeline',
description: '1356 days to achieve everything - no extensions',
),
const SizedBox(height: 10),
const _FeatureCard(
icon: Icons.trending_up,
title: 'Track Progress',
description: 'Watch yourself grow day by day',
),
const SizedBox(height: 32),
PrimaryButton(
onPressed: () {
controller.completeStep('intro');
context.push('/onboarding/how-it-works');
},
text: 'Get Started',
),
const SizedBox(height: 10),
TextButton(
onPressed: () async { onPressed: () async {
await controller.skipOnboarding(); await controller.skipOnboarding();
if (context.mounted) { if (context.mounted) {
context.push('/home'); context.push('/home');
} }
}, },
child: const Text('Skip onboarding'), child: const Text('Skip'),
), ),
const SizedBox(height: 20), ),
], const SizedBox(width: 16),
), Expanded(
flex: 2,
child: PrimaryButton(
onPressed: () {
controller.completeStep('intro');
context.push('/onboarding/how-it-works');
},
text: 'Get Started',
),
),
],
), ),
), const SizedBox(height: 16),
], ],
),
), ),
), ),
); );
@@ -176,3 +155,44 @@ class _FeatureCard extends StatelessWidget {
); );
} }
} }
class _OnboardingProgress extends StatelessWidget {
final int currentStep;
final int totalSteps;
const _OnboardingProgress({
required this.currentStep,
required this.totalSteps,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Step $currentStep of $totalSteps',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
),
TextButton(
onPressed: () => context.pop(),
child: const Text('Back'),
),
],
),
const SizedBox(height: 12),
LinearProgressIndicator(
value: currentStep / totalSteps,
backgroundColor: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3),
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.primary,
),
),
],
);
}
}
@@ -16,100 +16,86 @@ class OnboardingMotivationScreen extends ConsumerWidget {
return AppScaffold( return AppScaffold(
body: SafeArea( body: SafeArea(
child: Column( child: SingleChildScrollView(
children: [ child: Padding(
// Progress indicator and back button padding: const EdgeInsets.only(top: 30.0, left: 24.0, right: 24.0, bottom: 30.0),
Padding( child: Column(
padding: const EdgeInsets.all(24.0), crossAxisAlignment: CrossAxisAlignment.stretch,
child: Row( children: [
children: [ // Progress Bar and Navigation
IconButton( _OnboardingProgress(currentStep: 3, totalSteps: 3),
onPressed: () { const SizedBox(height: 24),
context.pop(); const Icon(
}, Icons.psychology_outlined,
icon: const Icon(Icons.arrow_back), size: 80,
), color: Colors.amber,
Expanded(
child: LinearProgressIndicator(
value: 3 / 3, // Step 3 of 3 (complete)
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.primary,
),
),
),
const SizedBox(width: 48), // Balance the back button
],
), ),
), const SizedBox(height: 24),
// Scrollable content Text(
Expanded( 'Your Time is Now',
child: SingleChildScrollView( style: Theme.of(context).textTheme.headlineLarge?.copyWith(
padding: const EdgeInsets.symmetric(horizontal: 24.0), fontWeight: FontWeight.bold,
child: Column( ),
crossAxisAlignment: CrossAxisAlignment.stretch, textAlign: TextAlign.center,
children: [ ),
const SizedBox(height: 8), const SizedBox(height: 16),
const Icon( Text(
Icons.psychology_outlined, '1356 days is approximately 3 years and 8 months.\n\n'
size: 64, 'That\'s enough time to transform your life, learn new skills, '
color: Colors.amber, 'build meaningful relationships, and achieve your biggest dreams.\n\n'
'Every day counts. Every step matters. Your journey begins now.',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
height: 1.5,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
const _MotivationCard(
icon: Icons.trending_up,
title: 'Track Progress',
description: 'Watch yourself grow as you complete goals and milestones.',
),
const SizedBox(height: 16),
const _MotivationCard(
icon: Icons.people,
title: 'Join Community',
description: 'Connect with others on similar journeys (optional).',
),
const SizedBox(height: 16),
const _MotivationCard(
icon: Icons.celebration,
title: 'Celebrate Wins',
description: 'Every achievement is worth celebrating.',
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextButton(
onPressed: () => context.pop(),
child: const Text('Back'),
), ),
const SizedBox(height: 16), ),
Text( const SizedBox(width: 16),
'Your Time is Now', Expanded(
style: Theme.of(context).textTheme.headlineLarge?.copyWith( flex: 2,
fontWeight: FontWeight.bold, child: PrimaryButton(
),
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
Text(
'1356 days is approximately 3 years and 8 months.\n\n'
'That\'s enough time to transform your life, learn new skills, '
'build meaningful relationships, and achieve your biggest dreams.\n\n'
'Every day counts. Every step matters. Your journey begins now.',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
height: 1.5,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
const _MotivationCard(
icon: Icons.trending_up,
title: 'Track Progress',
description: 'Watch yourself grow as you complete goals and milestones.',
),
const SizedBox(height: 10),
const _MotivationCard(
icon: Icons.people,
title: 'Join Community',
description: 'Connect with others on similar journeys (optional).',
),
const SizedBox(height: 10),
const _MotivationCard(
icon: Icons.celebration,
title: 'Celebrate Wins',
description: 'Every achievement is worth celebrating.',
),
const SizedBox(height: 24),
PrimaryButton(
onPressed: () async { onPressed: () async {
controller.completeStep('motivation'); controller.completeStep('motivation');
await controller.completeOnboarding(); await controller.completeOnboarding();
if (context.mounted) { if (context.mounted) {
context.push('/profile-setup'); context.push('/profile/create');
} }
}, },
text: 'Get Started', text: 'Get Started',
), ),
const SizedBox(height: 20), ),
], ],
),
), ),
), const SizedBox(height: 16),
], ],
),
), ),
), ),
); );
@@ -171,3 +157,44 @@ class _MotivationCard extends StatelessWidget {
); );
} }
} }
class _OnboardingProgress extends StatelessWidget {
final int currentStep;
final int totalSteps;
const _OnboardingProgress({
required this.currentStep,
required this.totalSteps,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Step $currentStep of $totalSteps',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
),
TextButton(
onPressed: () => context.pop(),
child: const Text('Back'),
),
],
),
const SizedBox(height: 12),
LinearProgressIndicator(
value: currentStep / totalSteps,
backgroundColor: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3),
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.primary,
),
),
],
);
}
}
+5 -4
View File
@@ -17,6 +17,7 @@ void main() async {
const SystemUiOverlayStyle( const SystemUiOverlayStyle(
statusBarColor: Colors.transparent, statusBarColor: Colors.transparent,
systemNavigationBarColor: Colors.transparent, systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
), ),
); );
@@ -28,13 +29,13 @@ void main() async {
runApp( runApp(
const ProviderScope( const ProviderScope(
child: LifeTimerApp(), child: App1356(),
), ),
); );
} }
class LifeTimerApp extends ConsumerWidget { class App1356 extends ConsumerWidget {
const LifeTimerApp({super.key}); const App1356({super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@@ -42,7 +43,7 @@ class LifeTimerApp extends ConsumerWidget {
final themeMode = ref.watch(themeModeProvider); final themeMode = ref.watch(themeModeProvider);
return MaterialApp.router( return MaterialApp.router(
title: 'LifeTimer', title: '1356',
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: AppTheme.light, theme: AppTheme.light,
darkTheme: AppTheme.dark, darkTheme: AppTheme.dark,
+1 -1
View File
@@ -1,4 +1,4 @@
name: lifetimer name: 1356
description: A gamified life countdown app with 1356-day challenge and bucket list tracking. description: A gamified life countdown app with 1356-day challenge and bucket list tracking.
publish_to: 'none' publish_to: 'none'
version: 1.0.0+1 version: 1.0.0+1