mirror of
https://github.com/Dvorinka/1356.git
synced 2026-06-03 19:42:57 +00:00
Initial commit: Project documentation and git setup
This commit is contained in:
@@ -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
|
||||
@@ -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'
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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}'),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
final themeModeProvider = StateProvider<ThemeMode>((ref) => ThemeMode.system);
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
@@ -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'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
flutter/ephemeral
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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}")
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
@@ -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/
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user