Initial commit: Project documentation and git setup

This commit is contained in:
Tomas Dvorak
2026-01-03 18:35:35 +01:00
commit 1639de69d4
51 changed files with 5005 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
+30
View File
@@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "f6ff1529fd6d8af5f706051d9251ac9231c83407"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
- platform: linux
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
+133
View File
@@ -0,0 +1,133 @@
# LifeTimer Flutter Project
## Overview
LifeTimer is a gamified life countdown app where users create a bucket list (up to 20 entries) and start a 1356-day countdown once they finalize their goals. The countdown cannot be stopped, paused, or extended.
## Project Structure
This Flutter project follows a clean architecture with feature-based organization:
```
lib/
├── main.dart # App entry point
├── bootstrap/ # Initialization
│ ├── bootstrap.dart
│ ├── env.dart
│ └── supabase_client.dart
├── core/ # Cross-cutting concerns
│ ├── theme/
│ │ └── app_theme.dart
│ ├── routing/
│ │ └── app_router.dart
│ ├── widgets/
│ │ └── primary_button.dart
│ └── state/
│ └── providers.dart
├── data/ # Data layer
│ ├── models/
│ │ ├── user_model.dart
│ │ └── goal_model.dart
│ └── repositories/
│ └── auth_repository.dart
└── features/ # Feature modules
├── auth/
├── onboarding/
├── goals/
├── countdown/
├── social/
├── profile/
└── settings/
```
## Tech Stack
- **Framework**: Flutter
- **State Management**: Riverpod
- **Backend**: Supabase (Auth, Database, Storage, Realtime)
- **Navigation**: Go Router
- **Local Storage**: Hive
- **Maps**: Google Maps Flutter
- **Notifications**: Flutter Local Notifications
## Getting Started
### Prerequisites
1. Flutter SDK (>=3.10.0)
2. Dart SDK (>=3.0.0)
3. Supabase project
### Setup
1. Clone this repository
2. Install dependencies:
```bash
flutter pub get
```
3. Set up environment variables:
Create a `.env` file or use build arguments:
```bash
flutter run --dart-define=SUPABASE_URL=your_url --dart-define=SUPABASE_ANON_KEY=your_key
```
4. Run the app:
```bash
flutter run
```
## Key Features
### Phase 1 (MVP)
- [x] User authentication (email, Google, Apple)
- [x] Bucket list creation (up to 20 goals)
- [x] 1356-day countdown timer
- [x] Goal progress tracking
- [x] Basic profile management
### Phase 2 (Social)
- [ ] Public/private profiles
- [ ] Social feed
- [ ] Leaderboards
- [ ] Following system
### Phase 3 (Advanced)
- [ ] Charts and analytics
- [ ] Image API integration
- [ ] Map integration for location-based goals
- [ ] Offline support
## Database Schema
The app uses Supabase PostgreSQL with the following main tables:
- `users` - User profiles and countdown data
- `goals` - Bucket list items
- `goal_steps` - Granular goal milestones
- `followers` - Social relationships
- `activities` - Timeline events
## Architecture Patterns
- **MVVM/Clean Architecture** with clear separation of concerns
- **Repository Pattern** for data access
- **Provider/StateNotifier** for state management
- **Feature-based organization** for scalability
## Development Notes
- All screens are currently placeholder implementations
- Core structure and dependencies are set up
- Authentication flow is partially implemented
- Database models and repositories are defined
- Navigation structure is in place
## Next Steps
1. Complete authentication implementation
2. Implement bucket list creation and management
3. Build countdown timer functionality
4. Add goal progress tracking
5. Implement social features (Phase 2)
6. Add advanced analytics (Phase 3)
+28
View File
@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
+15
View File
@@ -0,0 +1,15 @@
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'env.dart';
import 'supabase_client.dart';
Future<void> bootstrap() async {
WidgetsFlutterBinding.ensureInitialized();
await Supabase.initialize(
url: Env.supabaseUrl,
anonKey: Env.supabaseAnonKey,
);
initializeSupabaseClient();
}
+11
View File
@@ -0,0 +1,11 @@
class Env {
static const String supabaseUrl = String.fromEnvironment(
'SUPABASE_URL',
defaultValue: 'https://your-project.supabase.co',
);
static const String supabaseAnonKey = String.fromEnvironment(
'SUPABASE_ANON_KEY',
defaultValue: 'your-anon-key',
);
}
@@ -0,0 +1,8 @@
import 'package:supabase_flutter/supabase_flutter.dart';
void initializeSupabaseClient() {
// Additional client setup if needed
// For now, we use the default Supabase.instance.client
}
SupabaseClient get supabaseClient => Supabase.instance.client;
@@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../features/auth/presentation/auth_gate.dart';
import '../../features/onboarding/presentation/onboarding_intro_screen.dart';
import '../../features/countdown/presentation/home_countdown_screen.dart';
import '../../features/goals/presentation/goals_list_screen.dart';
import '../../features/social/presentation/social_feed_screen.dart';
import '../../features/profile/presentation/profile_screen.dart';
import '../../features/settings/presentation/settings_home_screen.dart';
final appRouterProvider = Provider<GoRouter>((ref) {
return GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const AuthGate(),
),
GoRoute(
path: '/onboarding',
builder: (context, state) => const OnboardingIntroScreen(),
),
GoRoute(
path: '/home',
builder: (context, state) => const HomeCountdownScreen(),
),
GoRoute(
path: '/goals',
builder: (context, state) => const GoalsListScreen(),
),
GoRoute(
path: '/social',
builder: (context, state) => const SocialFeedScreen(),
),
GoRoute(
path: '/profile',
builder: (context, state) => const ProfileScreen(),
),
GoRoute(
path: '/settings',
builder: (context, state) => const SettingsHomeScreen(),
),
],
errorBuilder: (context, state) => Scaffold(
body: Center(
child: Text('Error: ${state.error}'),
),
),
);
});
+4
View File
@@ -0,0 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final themeModeProvider = StateProvider<ThemeMode>((ref) => ThemeMode.system);
+161
View File
@@ -0,0 +1,161 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class AppTheme {
static const Color primaryColor = Color(0xFF6366F1);
static const Color secondaryColor = Color(0xFF8B5CF6);
static const Color accentColor = Color(0xFFEC4899);
static const Color backgroundColor = Color(0xFFFAFAFA);
static const Color surfaceColor = Color(0xFFFFFFFF);
static const Color errorColor = Color(0xFFEF4444);
static const Color warningColor = Color(0xFFF59E0B);
static const Color successColor = Color(0xFF10B981);
static const ColorScheme lightColorScheme = ColorScheme(
brightness: Brightness.light,
primary: primaryColor,
onPrimary: Color(0xFFFFFFFF),
secondary: secondaryColor,
onSecondary: Color(0xFFFFFFFF),
error: errorColor,
onError: Color(0xFFFFFFFF),
surface: surfaceColor,
onSurface: Color(0xFF1F2937),
background: backgroundColor,
onBackground: Color(0xFF1F2937),
);
static const ColorScheme darkColorScheme = ColorScheme(
brightness: Brightness.dark,
primary: primaryColor,
onPrimary: Color(0xFFFFFFFF),
secondary: secondaryColor,
onSecondary: Color(0xFFFFFFFF),
error: errorColor,
onError: Color(0xFFFFFFFF),
surface: Color(0xFF1F2937),
onSurface: Color(0xFFF9FAFB),
background: Color(0xFF111827),
onBackground: Color(0xFFF9FAFB),
);
static ThemeData get light {
return ThemeData(
useMaterial3: true,
colorScheme: lightColorScheme,
appBarTheme: const AppBarTheme(
backgroundColor: surfaceColor,
foregroundColor: Color(0xFF1F2937),
elevation: 0,
systemOverlayStyle: SystemUiOverlayStyle.dark,
),
cardTheme: CardThemeData(
color: surfaceColor,
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: primaryColor,
foregroundColor: Color(0xFFFFFFFF),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
),
textTheme: const TextTheme(
displayLarge: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Color(0xFF1F2937),
),
displayMedium: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Color(0xFF1F2937),
),
headlineLarge: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
headlineMedium: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
bodyLarge: TextStyle(
fontSize: 16,
color: Color(0xFF4B5563),
),
bodyMedium: TextStyle(
fontSize: 14,
color: Color(0xFF6B7280),
),
),
);
}
static ThemeData get dark {
return ThemeData(
useMaterial3: true,
colorScheme: darkColorScheme,
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFF1F2937),
foregroundColor: Color(0xFFF9FAFB),
elevation: 0,
systemOverlayStyle: SystemUiOverlayStyle.light,
),
cardTheme: CardThemeData(
color: const Color(0xFF374151),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: primaryColor,
foregroundColor: Color(0xFFFFFFFF),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
),
textTheme: const TextTheme(
displayLarge: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Color(0xFFF9FAFB),
),
displayMedium: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Color(0xFFF9FAFB),
),
headlineLarge: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w600,
color: Color(0xFFF9FAFB),
),
headlineMedium: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Color(0xFFF9FAFB),
),
bodyLarge: TextStyle(
fontSize: 16,
color: Color(0xFFD1D5DB),
),
bodyMedium: TextStyle(
fontSize: 14,
color: Color(0xFF9CA3AF),
),
),
);
}
}
@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
class PrimaryButton extends StatelessWidget {
final String text;
final VoidCallback onPressed;
final bool isLoading;
final bool isDisabled;
final Color? backgroundColor;
final Color? foregroundColor;
final double? width;
final double? height;
const PrimaryButton({
super.key,
required this.text,
required this.onPressed,
this.isLoading = false,
this.isDisabled = false,
this.backgroundColor,
this.foregroundColor,
this.width,
this.height,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: width,
height: height ?? 48,
child: ElevatedButton(
onPressed: (isLoading || isDisabled) ? null : onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: isLoading
? SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
foregroundColor ?? Theme.of(context).colorScheme.onPrimary,
),
),
)
: Text(text),
),
);
}
}
+81
View File
@@ -0,0 +1,81 @@
import 'package:equatable/equatable.dart';
class Goal extends Equatable {
final String id;
final String ownerId;
final String title;
final String? description;
final int progress;
final double? locationLat;
final double? locationLng;
final String? locationName;
final String? imageUrl;
final bool completed;
final DateTime createdAt;
final DateTime updatedAt;
const Goal({
required this.id,
required this.ownerId,
required this.title,
this.description,
this.progress = 0,
this.locationLat,
this.locationLng,
this.locationName,
this.imageUrl,
this.completed = false,
required this.createdAt,
required this.updatedAt,
});
bool get hasLocation => locationLat != null && locationLng != null;
bool get hasImage => imageUrl != null && imageUrl!.isNotEmpty;
Goal copyWith({
String? id,
String? ownerId,
String? title,
String? description,
int? progress,
double? locationLat,
double? locationLng,
String? locationName,
String? imageUrl,
bool? completed,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return Goal(
id: id ?? this.id,
ownerId: ownerId ?? this.ownerId,
title: title ?? this.title,
description: description ?? this.description,
progress: progress ?? this.progress,
locationLat: locationLat ?? this.locationLat,
locationLng: locationLng ?? this.locationLng,
locationName: locationName ?? this.locationName,
imageUrl: imageUrl ?? this.imageUrl,
completed: completed ?? this.completed,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
@override
List<Object?> get props => [
id,
ownerId,
title,
description,
progress,
locationLat,
locationLng,
locationName,
imageUrl,
completed,
createdAt,
updatedAt,
];
}
+79
View File
@@ -0,0 +1,79 @@
import 'package:equatable/equatable.dart';
class User extends Equatable {
final String id;
final String username;
final String email;
final String? avatarUrl;
final String? bio;
final bool isPublicProfile;
final DateTime? countdownStartDate;
final DateTime? countdownEndDate;
final DateTime createdAt;
final DateTime updatedAt;
const User({
required this.id,
required this.username,
required this.email,
this.avatarUrl,
this.bio,
this.isPublicProfile = false,
this.countdownStartDate,
this.countdownEndDate,
required this.createdAt,
required this.updatedAt,
});
bool get hasCountdownStarted => countdownStartDate != null;
bool get isCountdownActive {
if (!hasCountdownStarted || countdownEndDate == null) return false;
return DateTime.now().isBefore(countdownEndDate!);
}
int? get daysRemaining {
if (!isCountdownActive) return null;
return countdownEndDate!.difference(DateTime.now()).inDays;
}
User copyWith({
String? id,
String? username,
String? email,
String? avatarUrl,
String? bio,
bool? isPublicProfile,
DateTime? countdownStartDate,
DateTime? countdownEndDate,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return User(
id: id ?? this.id,
username: username ?? this.username,
email: email ?? this.email,
avatarUrl: avatarUrl ?? this.avatarUrl,
bio: bio ?? this.bio,
isPublicProfile: isPublicProfile ?? this.isPublicProfile,
countdownStartDate: countdownStartDate ?? this.countdownStartDate,
countdownEndDate: countdownEndDate ?? this.countdownEndDate,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
@override
List<Object?> get props => [
id,
username,
email,
avatarUrl,
bio,
isPublicProfile,
countdownStartDate,
countdownEndDate,
createdAt,
updatedAt,
];
}
@@ -0,0 +1,125 @@
import '../models/user_model.dart';
import '../../bootstrap/supabase_client.dart';
import 'package:supabase_flutter/supabase_flutter.dart' hide User;
class AuthRepository {
final SupabaseClient _client;
AuthRepository([SupabaseClient? client]) : _client = client ?? supabaseClient;
Stream<User?> get authStateChanges {
return _client.auth.onAuthStateChange.map((data) {
final session = data.session;
if (session?.user != null) {
return _mapSupabaseUserToAppUser(session!.user);
}
return null;
});
}
User? get currentUser {
final user = _client.auth.currentUser;
return user != null ? _mapSupabaseUserToAppUser(user) : null;
}
Future<void> signInWithEmail(String email, String password) async {
await _client.auth.signInWithPassword(email: email, password: password);
}
Future<void> signUpWithEmail(String email, String password, String username) async {
final response = await _client.auth.signUp(
email: email,
password: password,
data: {'username': username},
);
if (response.user != null) {
await _createUserProfile(response.user!.id, username, email);
}
}
Future<void> signInWithGoogle() async {
// TODO: Implement Google OAuth
// await _client.auth.signInWithOAuth(OAuthProvider.google);
throw UnimplementedError('Google OAuth not implemented yet');
}
Future<void> signInWithApple() async {
// TODO: Implement Apple OAuth
// await _client.auth.signInWithOAuth(OAuthProvider.apple);
throw UnimplementedError('Apple OAuth not implemented yet');
}
Future<void> signOut() async {
await _client.auth.signOut();
}
Future<void> resetPassword(String email) async {
await _client.auth.resetPasswordForEmail(email);
}
Future<void> updateProfile({
String? username,
String? bio,
String? avatarUrl,
bool? isPublicProfile,
}) async {
final userId = _client.auth.currentUser?.id;
if (userId == null) throw Exception('User not authenticated');
final updates = <String, dynamic>{};
if (username != null) updates['username'] = username;
if (bio != null) updates['bio'] = bio;
if (avatarUrl != null) updates['avatar_url'] = avatarUrl;
if (isPublicProfile != null) updates['is_public_profile'] = isPublicProfile;
updates['updated_at'] = DateTime.now().toIso8601String();
await _client
.from('users')
.update(updates)
.eq('id', userId);
}
Future<User> _createUserProfile(String userId, String username, String email) async {
final now = DateTime.now().toIso8601String();
final response = await _client.from('users').insert({
'id': userId,
'username': username,
'email': email,
'created_at': now,
'updated_at': now,
}).select().single();
return _mapSupabaseDataToUser(response);
}
User _mapSupabaseUserToAppUser(dynamic supabaseUser) {
return User(
id: supabaseUser.id,
username: supabaseUser.userMetadata?['username'] ?? '',
email: supabaseUser.email ?? '',
createdAt: DateTime.tryParse(supabaseUser.createdAt ?? '') ?? DateTime.now(),
updatedAt: DateTime.tryParse(supabaseUser.updatedAt ?? '') ?? DateTime.now(),
);
}
User _mapSupabaseDataToUser(Map<String, dynamic> data) {
return User(
id: data['id'],
username: data['username'],
email: data['email'],
avatarUrl: data['avatar_url'],
bio: data['bio'],
isPublicProfile: data['is_public_profile'] ?? false,
countdownStartDate: data['countdown_start_date'] != null
? DateTime.parse(data['countdown_start_date'])
: null,
countdownEndDate: data['countdown_end_date'] != null
? DateTime.parse(data['countdown_end_date'])
: null,
createdAt: DateTime.parse(data['created_at']),
updatedAt: DateTime.parse(data['updated_at']),
);
}
}
@@ -0,0 +1,64 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../data/repositories/auth_repository.dart';
import '../../../data/models/user_model.dart';
final authControllerProvider = StateNotifierProvider<AuthController, User?>((ref) {
return AuthController(ref.read(authRepositoryProvider));
});
final authRepositoryProvider = Provider<AuthRepository>((ref) {
return AuthRepository(/* SupabaseClient instance will be injected */);
});
class AuthController extends StateNotifier<User?> {
final AuthRepository _authRepository;
AuthController(this._authRepository) : super(null) {
_init();
}
void _init() {
state = _authRepository.currentUser;
_authRepository.authStateChanges.listen((user) {
state = user;
});
}
Future<void> signInWithEmail(String email, String password) async {
await _authRepository.signInWithEmail(email, password);
}
Future<void> signUpWithEmail(String email, String password, String username) async {
await _authRepository.signUpWithEmail(email, password, username);
}
Future<void> signInWithGoogle() async {
await _authRepository.signInWithGoogle();
}
Future<void> signInWithApple() async {
await _authRepository.signInWithApple();
}
Future<void> signOut() async {
await _authRepository.signOut();
}
Future<void> resetPassword(String email) async {
await _authRepository.resetPassword(email);
}
Future<void> updateProfile({
String? username,
String? bio,
String? avatarUrl,
bool? isPublicProfile,
}) async {
await _authRepository.updateProfile(
username: username,
bio: bio,
avatarUrl: avatarUrl,
isPublicProfile: isPublicProfile,
);
}
}
@@ -0,0 +1,134 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../application/auth_controller.dart';
import '../../onboarding/presentation/onboarding_intro_screen.dart';
class AuthGate extends ConsumerWidget {
const AuthGate({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final authState = ref.watch(authControllerProvider);
if (authState == null) {
return const SignInScreen();
}
return const OnboardingIntroScreen();
}
}
class SignInScreen extends ConsumerStatefulWidget {
const SignInScreen({super.key});
@override
ConsumerState<SignInScreen> createState() => _SignInScreenState();
}
class _SignInScreenState extends ConsumerState<SignInScreen> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isLoading = false;
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
Future<void> _signIn() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _isLoading = true);
try {
await ref.read(authControllerProvider.notifier).signInWithEmail(
_emailController.text.trim(),
_passwordController.text,
);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $e')),
);
}
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('LifeTimer'),
),
body: Padding(
padding: const EdgeInsets.all(24.0),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Welcome Back',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your email';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
),
obscureText: true,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your password';
}
return null;
},
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _isLoading ? null : _signIn,
child: _isLoading
? const CircularProgressIndicator()
: const Text('Sign In'),
),
const SizedBox(height: 16),
TextButton(
onPressed: () {
// Navigate to sign up
},
child: const Text('Don\'t have an account? Sign Up'),
),
],
),
),
),
);
}
}
@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
class HomeCountdownScreen extends StatelessWidget {
const HomeCountdownScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('LifeTimer'),
),
body: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'1356',
style: TextStyle(
fontSize: 72,
fontWeight: FontWeight.bold,
),
),
Text(
'days remaining',
style: TextStyle(fontSize: 24),
),
SizedBox(height: 32),
Text(
'Your countdown starts here',
style: TextStyle(fontSize: 18),
),
],
),
),
);
}
}
@@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
class GoalsListScreen extends StatelessWidget {
const GoalsListScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Goals'),
),
body: const Center(
child: Text('Goals List - Coming Soon'),
),
);
}
}
@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
class OnboardingIntroScreen extends StatelessWidget {
const OnboardingIntroScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('LifeTimer'),
),
body: const Padding(
padding: EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Welcome to LifeTimer',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
SizedBox(height: 16),
Text(
'Your 1356-day journey starts here.\nCreate your bucket list and begin your countdown.',
style: TextStyle(fontSize: 18),
textAlign: TextAlign.center,
),
],
),
),
);
}
}
@@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
class ProfileScreen extends StatelessWidget {
const ProfileScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Profile'),
),
body: const Center(
child: Text('Profile - Coming Soon'),
),
);
}
}
@@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
class SettingsHomeScreen extends StatelessWidget {
const SettingsHomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Settings'),
),
body: const Center(
child: Text('Settings - Coming Soon'),
),
);
}
}
@@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
class SocialFeedScreen extends StatelessWidget {
const SocialFeedScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Social'),
),
body: const Center(
child: Text('Social Feed - Coming Soon'),
),
);
}
}
+37
View File
@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'bootstrap/bootstrap.dart';
import 'core/theme/app_theme.dart';
import 'core/routing/app_router.dart';
import 'core/state/providers.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await bootstrap();
runApp(
const ProviderScope(
child: LifeTimerApp(),
),
);
}
class LifeTimerApp extends ConsumerWidget {
const LifeTimerApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final router = ref.watch(appRouterProvider);
final themeMode = ref.watch(themeModeProvider);
return MaterialApp.router(
title: 'LifeTimer',
debugShowCheckedModeBanner: false,
theme: AppTheme.light,
darkTheme: AppTheme.dark,
themeMode: themeMode,
routerConfig: router,
);
}
}
+1
View File
@@ -0,0 +1 @@
flutter/ephemeral
+128
View File
@@ -0,0 +1,128 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "lifetimer")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.example.lifetimer")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(SET CMP0063 NEW)
# Load bundled libraries from the lib/ directory relative to the binary.
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Define build configuration options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
# Application build; see runner/CMakeLists.txt.
add_subdirectory("runner")
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
install(FILES "${bundled_library}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endforeach(bundled_library)
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()
+88
View File
@@ -0,0 +1,88 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.10)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
# which isn't available in 3.10.
function(list_prepend LIST_NAME PREFIX)
set(NEW_LIST "")
foreach(element ${${LIST_NAME}})
list(APPEND NEW_LIST "${PREFIX}${element}")
endforeach(element)
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
endfunction()
# === Flutter Library ===
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"fl_basic_message_channel.h"
"fl_binary_codec.h"
"fl_binary_messenger.h"
"fl_dart_project.h"
"fl_engine.h"
"fl_json_message_codec.h"
"fl_json_method_codec.h"
"fl_message_codec.h"
"fl_method_call.h"
"fl_method_channel.h"
"fl_method_codec.h"
"fl_method_response.h"
"fl_plugin_registrar.h"
"fl_plugin_registry.h"
"fl_standard_message_codec.h"
"fl_standard_method_codec.h"
"fl_string_codec.h"
"fl_value.h"
"fl_view.h"
"flutter_linux.h"
)
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
target_link_libraries(flutter INTERFACE
PkgConfig::GTK
PkgConfig::GLIB
PkgConfig::GIO
)
add_dependencies(flutter flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/_phony_
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
)
@@ -0,0 +1,23 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <file_selector_linux/file_selector_plugin.h>
#include <gtk/gtk_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) gtk_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
gtk_plugin_register_with_registrar(gtk_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
}
@@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_
@@ -0,0 +1,26 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
gtk
url_launcher_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)
+26
View File
@@ -0,0 +1,26 @@
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# Define the application target. To change its name, change BINARY_NAME in the
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
# work.
#
# Any new source files that you add to the application should be added here.
add_executable(${BINARY_NAME}
"main.cc"
"my_application.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)
# Apply the standard set of build settings. This can be removed for applications
# that need different build settings.
apply_standard_settings(${BINARY_NAME})
# Add preprocessor definitions for the application ID.
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
# Add dependency libraries. Add any application-specific dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
+6
View File
@@ -0,0 +1,6 @@
#include "my_application.h"
int main(int argc, char** argv) {
g_autoptr(MyApplication) app = my_application_new();
return g_application_run(G_APPLICATION(app), argc, argv);
}
+148
View File
@@ -0,0 +1,148 @@
#include "my_application.h"
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#include "flutter/generated_plugin_registrant.h"
struct _MyApplication {
GtkApplication parent_instance;
char** dart_entrypoint_arguments;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Called when first Flutter frame received.
static void first_frame_cb(MyApplication* self, FlView* view) {
gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
}
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// Use a header bar when running in GNOME as this is the common style used
// by applications and is the setup most users will be using (e.g. Ubuntu
// desktop).
// If running on X and not using GNOME then just use a traditional title bar
// in case the window manager does more exotic layout, e.g. tiling.
// If running on Wayland assume the header bar will work (may need changing
// if future cases occur).
gboolean use_header_bar = TRUE;
#ifdef GDK_WINDOWING_X11
GdkScreen* screen = gtk_window_get_screen(window);
if (GDK_IS_X11_SCREEN(screen)) {
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
use_header_bar = FALSE;
}
}
#endif
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "lifetimer");
gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else {
gtk_window_set_title(window, "lifetimer");
}
gtk_window_set_default_size(window, 1280, 720);
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(
project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project);
GdkRGBA background_color;
// Background defaults to black, override it here if necessary, e.g. #00000000
// for transparent.
gdk_rgba_parse(&background_color, "#000000");
fl_view_set_background_color(view, &background_color);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
// Show the window when Flutter renders.
// Requires the view to be realized so we can start rendering.
g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb),
self);
gtk_widget_realize(GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
gtk_widget_grab_focus(GTK_WIDGET(view));
}
// Implements GApplication::local_command_line.
static gboolean my_application_local_command_line(GApplication* application,
gchar*** arguments,
int* exit_status) {
MyApplication* self = MY_APPLICATION(application);
// Strip out the first argument as it is the binary name.
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
g_autoptr(GError) error = nullptr;
if (!g_application_register(application, nullptr, &error)) {
g_warning("Failed to register: %s", error->message);
*exit_status = 1;
return TRUE;
}
g_application_activate(application);
*exit_status = 0;
return TRUE;
}
// Implements GApplication::startup.
static void my_application_startup(GApplication* application) {
// MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application startup.
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
}
// Implements GApplication::shutdown.
static void my_application_shutdown(GApplication* application) {
// MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application shutdown.
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
}
// Implements GObject::dispose.
static void my_application_dispose(GObject* object) {
MyApplication* self = MY_APPLICATION(object);
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
}
static void my_application_class_init(MyApplicationClass* klass) {
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
G_APPLICATION_CLASS(klass)->local_command_line =
my_application_local_command_line;
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
}
static void my_application_init(MyApplication* self) {}
MyApplication* my_application_new() {
// Set the program name to the application ID, which helps various systems
// like GTK and desktop environments map this running application to its
// corresponding .desktop file. This ensures better integration by allowing
// the application to be recognized beyond its binary name.
g_set_prgname(APPLICATION_ID);
return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID, "flags",
G_APPLICATION_NON_UNIQUE, nullptr));
}
+21
View File
@@ -0,0 +1,21 @@
#ifndef FLUTTER_MY_APPLICATION_H_
#define FLUTTER_MY_APPLICATION_H_
#include <gtk/gtk.h>
G_DECLARE_FINAL_TYPE(MyApplication,
my_application,
MY,
APPLICATION,
GtkApplication)
/**
* my_application_new:
*
* Creates a new Flutter-based application.
*
* Returns: a new #MyApplication.
*/
MyApplication* my_application_new();
#endif // FLUTTER_MY_APPLICATION_H_
File diff suppressed because it is too large Load Diff
+72
View File
@@ -0,0 +1,72 @@
name: lifetimer
description: A gamified life countdown app with 1356-day challenge and bucket list tracking.
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: '>=3.0.0 <4.0.0'
flutter: ">=3.10.0"
dependencies:
flutter:
sdk: flutter
# State Management
flutter_riverpod: ^2.4.9
riverpod_annotation: ^2.3.3
# Supabase Backend
supabase_flutter: ^1.10.24
# Navigation
go_router: ^12.1.3
# UI Components
cupertino_icons: ^1.0.2
material_color_utilities: ^0.11.1
# Local Storage & Caching
hive: ^2.2.3
hive_flutter: ^1.1.0
path_provider: ^2.1.1
# Utilities
intl: ^0.18.1
uuid: ^4.2.1
equatable: ^2.0.5
# Image Handling
cached_network_image: ^3.3.0
image_picker: ^1.0.4
# Maps & Location
geolocator: ^10.1.0
google_maps_flutter: ^2.5.0
# Notifications
flutter_local_notifications: ^16.3.0
# Charts & Analytics
fl_chart: ^0.65.0
# Testing
mockito: ^5.4.4
integration_test:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
# Code Generation
riverpod_generator: ^2.3.9
build_runner: ^2.4.7
hive_generator: ^2.0.1
flutter:
uses-material-design: true
assets:
- assets/images/
- assets/icons/
+30
View File
@@ -0,0 +1,30 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:lifetimer/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}