Initial scaffold: FocusFlow shared Dart package
Models (Task, Streak, StreakEntry, Reward, User, TimeEstimate, CoworkingRoom, ApiResponse), enums (EnergyLevel, TaskStatus, RewardType, RewardStyle), constants (ApiConstants, ErrorCodes, AppLimits), validators, and generated .g.dart files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# https://dart.dev/guides/libraries/private-files
|
||||||
|
# Created by `dart pub`
|
||||||
|
.dart_tool/
|
||||||
|
|
||||||
|
# Avoid committing pubspec.lock for library packages; see
|
||||||
|
# https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||||
|
pubspec.lock
|
||||||
3
CHANGELOG.md
Normal file
3
CHANGELOG.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
## 1.0.0
|
||||||
|
|
||||||
|
- Initial version.
|
||||||
39
README.md
Normal file
39
README.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<!--
|
||||||
|
This README describes the package. If you publish this package to pub.dev,
|
||||||
|
this README's contents appear on the landing page for your package.
|
||||||
|
|
||||||
|
For information about how to write a good package README, see the guide for
|
||||||
|
[writing package pages](https://dart.dev/tools/pub/writing-package-pages).
|
||||||
|
|
||||||
|
For general information about developing packages, see the Dart guide for
|
||||||
|
[creating packages](https://dart.dev/guides/libraries/create-packages)
|
||||||
|
and the Flutter guide for
|
||||||
|
[developing packages and plugins](https://flutter.dev/to/develop-packages).
|
||||||
|
-->
|
||||||
|
|
||||||
|
TODO: Put a short description of the package here that helps potential users
|
||||||
|
know whether this package might be useful for them.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
TODO: List what your package can do. Maybe include images, gifs, or videos.
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
TODO: List prerequisites and provide or point to information on how to
|
||||||
|
start using the package.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
TODO: Include short and useful examples for package users. Add longer examples
|
||||||
|
to `/example` folder.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
const like = 'sample';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional information
|
||||||
|
|
||||||
|
TODO: Tell users more about the package: where to find more information, how to
|
||||||
|
contribute to the package, how to file issues, what response they can expect
|
||||||
|
from the package authors, and more.
|
||||||
30
analysis_options.yaml
Normal file
30
analysis_options.yaml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# This file configures the static analysis results for your project (errors,
|
||||||
|
# warnings, and lints).
|
||||||
|
#
|
||||||
|
# This enables the 'recommended' set of lints from `package:lints`.
|
||||||
|
# This set helps identify many issues that may lead to problems when running
|
||||||
|
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
|
||||||
|
# style and format.
|
||||||
|
#
|
||||||
|
# If you want a smaller set of lints you can change this to specify
|
||||||
|
# 'package:lints/core.yaml'. These are just the most critical lints
|
||||||
|
# (the recommended set includes the core lints).
|
||||||
|
# The core lints are also what is used by pub.dev for scoring packages.
|
||||||
|
|
||||||
|
include: package:lints/recommended.yaml
|
||||||
|
|
||||||
|
# Uncomment the following section to specify additional rules.
|
||||||
|
|
||||||
|
# linter:
|
||||||
|
# rules:
|
||||||
|
# - camel_case_types
|
||||||
|
|
||||||
|
# analyzer:
|
||||||
|
# exclude:
|
||||||
|
# - path/to/excluded/files/**
|
||||||
|
|
||||||
|
# For more information about the core and recommended set of lints, see
|
||||||
|
# https://dart.dev/go/core-lints
|
||||||
|
|
||||||
|
# For additional information about configuring this file, see
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
30
example/focusflow_shared_example.dart
Normal file
30
example/focusflow_shared_example.dart
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import 'package:focusflow_shared/focusflow_shared.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// Create a task
|
||||||
|
final task = Task(
|
||||||
|
id: 'task-001',
|
||||||
|
userId: 'user-001',
|
||||||
|
title: 'Write project proposal',
|
||||||
|
status: TaskStatus.pending.apiValue,
|
||||||
|
energyLevel: EnergyLevel.medium.name,
|
||||||
|
priority: 75,
|
||||||
|
estimatedMinutes: 30,
|
||||||
|
tags: ['work', 'writing'],
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
|
print('Task: ${task.title} (priority: ${task.priority})');
|
||||||
|
print('Status: ${TaskStatus.pending.displayName}');
|
||||||
|
print('Energy: ${EnergyLevel.medium.displayName}');
|
||||||
|
|
||||||
|
// Validate
|
||||||
|
final titleError = TaskValidator.validateTitle(task.title);
|
||||||
|
print('Title valid: ${titleError == null}');
|
||||||
|
|
||||||
|
// API path
|
||||||
|
print('Task endpoint: ${ApiConstants.task(task.id)}');
|
||||||
|
|
||||||
|
// Limits
|
||||||
|
print('Free tier max tasks: ${AppLimits.freeMaxActiveTasks}');
|
||||||
|
}
|
||||||
26
lib/focusflow_shared.dart
Normal file
26
lib/focusflow_shared.dart
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/// Shared models, enums, and utilities for FocusFlow ADHD app.
|
||||||
|
library;
|
||||||
|
|
||||||
|
// Models
|
||||||
|
export 'src/models/task.dart';
|
||||||
|
export 'src/models/streak.dart';
|
||||||
|
export 'src/models/streak_entry.dart';
|
||||||
|
export 'src/models/reward.dart';
|
||||||
|
export 'src/models/user.dart';
|
||||||
|
export 'src/models/time_estimate.dart';
|
||||||
|
export 'src/models/coworking_room.dart';
|
||||||
|
export 'src/models/api_response.dart';
|
||||||
|
|
||||||
|
// Enums
|
||||||
|
export 'src/enums/energy_level.dart';
|
||||||
|
export 'src/enums/task_status.dart';
|
||||||
|
export 'src/enums/reward_type.dart';
|
||||||
|
export 'src/enums/reward_style.dart';
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
export 'src/constants/api_constants.dart';
|
||||||
|
export 'src/constants/error_codes.dart';
|
||||||
|
export 'src/constants/app_limits.dart';
|
||||||
|
|
||||||
|
// Validators
|
||||||
|
export 'src/validators/task_validator.dart';
|
||||||
47
lib/src/constants/api_constants.dart
Normal file
47
lib/src/constants/api_constants.dart
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
class ApiConstants {
|
||||||
|
ApiConstants._();
|
||||||
|
static const String apiVersion = 'v1';
|
||||||
|
static const String basePath = '/api/$apiVersion';
|
||||||
|
|
||||||
|
// Auth
|
||||||
|
static const String authRegister = '$basePath/auth/register';
|
||||||
|
static const String authLogin = '$basePath/auth/login';
|
||||||
|
static const String authRefresh = '$basePath/auth/refresh';
|
||||||
|
static const String authLogout = '$basePath/auth/logout';
|
||||||
|
|
||||||
|
// Tasks
|
||||||
|
static const String tasks = '$basePath/tasks';
|
||||||
|
static String task(String id) => '$basePath/tasks/$id';
|
||||||
|
static String completeTask(String id) => '$basePath/tasks/$id/complete';
|
||||||
|
static String skipTask(String id) => '$basePath/tasks/$id/skip';
|
||||||
|
static const String nextTask = '$basePath/tasks/next';
|
||||||
|
static const String dopamineOrdered = '$basePath/tasks/dopamine-ordered';
|
||||||
|
|
||||||
|
// Streaks
|
||||||
|
static const String streaks = '$basePath/streaks';
|
||||||
|
static String streak(String id) => '$basePath/streaks/$id';
|
||||||
|
static String forgiveStreak(String id) => '$basePath/streaks/$id/forgive';
|
||||||
|
static String streakHistory(String id) => '$basePath/streaks/$id/history';
|
||||||
|
|
||||||
|
// Rewards
|
||||||
|
static const String generateReward = '$basePath/rewards/generate';
|
||||||
|
static const String rewardHistory = '$basePath/rewards/history';
|
||||||
|
|
||||||
|
// Time
|
||||||
|
static const String timeEstimate = '$basePath/time/estimate';
|
||||||
|
static const String timeActual = '$basePath/time/actual';
|
||||||
|
static const String timeAccuracy = '$basePath/time/accuracy';
|
||||||
|
|
||||||
|
// Sync
|
||||||
|
static const String syncPush = '$basePath/sync/push';
|
||||||
|
static const String syncPull = '$basePath/sync/pull';
|
||||||
|
|
||||||
|
// Rooms (body doubling)
|
||||||
|
static const String rooms = '$basePath/rooms';
|
||||||
|
static String room(String id) => '$basePath/rooms/$id';
|
||||||
|
static String joinRoom(String id) => '$basePath/rooms/$id/join';
|
||||||
|
static String leaveRoom(String id) => '$basePath/rooms/$id/leave';
|
||||||
|
|
||||||
|
// Subscription
|
||||||
|
static const String subscriptionStatus = '$basePath/subscription/status';
|
||||||
|
}
|
||||||
17
lib/src/constants/app_limits.dart
Normal file
17
lib/src/constants/app_limits.dart
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
class AppLimits {
|
||||||
|
AppLimits._();
|
||||||
|
// Free tier
|
||||||
|
static const int freeMaxActiveTasks = 15;
|
||||||
|
static const int freeMaxStreaks = 3;
|
||||||
|
// Premium tier
|
||||||
|
static const int premiumMaxActiveTasks = -1; // unlimited
|
||||||
|
static const int premiumMaxStreaks = -1;
|
||||||
|
// Defaults
|
||||||
|
static const int defaultGraceDays = 2;
|
||||||
|
static const int defaultFocusMinutes = 25;
|
||||||
|
static const int defaultTaskLoad = 5;
|
||||||
|
static const int maxRoomParticipants = 10;
|
||||||
|
// Pagination
|
||||||
|
static const int defaultPageSize = 20;
|
||||||
|
static const int maxPageSize = 100;
|
||||||
|
}
|
||||||
18
lib/src/constants/error_codes.dart
Normal file
18
lib/src/constants/error_codes.dart
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
class ErrorCodes {
|
||||||
|
ErrorCodes._();
|
||||||
|
static const String invalidCredentials = 'INVALID_CREDENTIALS';
|
||||||
|
static const String emailAlreadyExists = 'EMAIL_ALREADY_EXISTS';
|
||||||
|
static const String tokenExpired = 'TOKEN_EXPIRED';
|
||||||
|
static const String tokenInvalid = 'TOKEN_INVALID';
|
||||||
|
static const String unauthorized = 'UNAUTHORIZED';
|
||||||
|
static const String taskNotFound = 'TASK_NOT_FOUND';
|
||||||
|
static const String streakNotFound = 'STREAK_NOT_FOUND';
|
||||||
|
static const String noTasksAvailable = 'NO_TASKS_AVAILABLE';
|
||||||
|
static const String roomFull = 'ROOM_FULL';
|
||||||
|
static const String roomNotFound = 'ROOM_NOT_FOUND';
|
||||||
|
static const String validationError = 'VALIDATION_ERROR';
|
||||||
|
static const String notFound = 'NOT_FOUND';
|
||||||
|
static const String internalError = 'INTERNAL_ERROR';
|
||||||
|
static const String rateLimited = 'RATE_LIMITED';
|
||||||
|
static const String premiumRequired = 'PREMIUM_REQUIRED';
|
||||||
|
}
|
||||||
13
lib/src/enums/energy_level.dart
Normal file
13
lib/src/enums/energy_level.dart
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
enum EnergyLevel {
|
||||||
|
low,
|
||||||
|
medium,
|
||||||
|
high;
|
||||||
|
|
||||||
|
String get displayName {
|
||||||
|
switch (this) {
|
||||||
|
case EnergyLevel.low: return 'Low Energy';
|
||||||
|
case EnergyLevel.medium: return 'Medium Energy';
|
||||||
|
case EnergyLevel.high: return 'High Energy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
lib/src/enums/reward_style.dart
Normal file
13
lib/src/enums/reward_style.dart
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
enum RewardStyle {
|
||||||
|
playful,
|
||||||
|
minimal,
|
||||||
|
data;
|
||||||
|
|
||||||
|
String get displayName {
|
||||||
|
switch (this) {
|
||||||
|
case RewardStyle.playful: return 'Playful (animations & messages)';
|
||||||
|
case RewardStyle.minimal: return 'Minimal (subtle feedback)';
|
||||||
|
case RewardStyle.data: return 'Data-driven (stats & charts)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
lib/src/enums/reward_type.dart
Normal file
19
lib/src/enums/reward_type.dart
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
enum RewardType {
|
||||||
|
points,
|
||||||
|
badge,
|
||||||
|
animation,
|
||||||
|
message,
|
||||||
|
unlock,
|
||||||
|
surprise;
|
||||||
|
|
||||||
|
String get displayName {
|
||||||
|
switch (this) {
|
||||||
|
case RewardType.points: return 'Points';
|
||||||
|
case RewardType.badge: return 'Badge';
|
||||||
|
case RewardType.animation: return 'Celebration';
|
||||||
|
case RewardType.message: return 'Encouragement';
|
||||||
|
case RewardType.unlock: return 'Unlock';
|
||||||
|
case RewardType.surprise: return 'Surprise!';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
lib/src/enums/task_status.dart
Normal file
27
lib/src/enums/task_status.dart
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
enum TaskStatus {
|
||||||
|
pending,
|
||||||
|
inProgress,
|
||||||
|
completed,
|
||||||
|
skipped,
|
||||||
|
archived;
|
||||||
|
|
||||||
|
String get displayName {
|
||||||
|
switch (this) {
|
||||||
|
case TaskStatus.pending: return 'To Do';
|
||||||
|
case TaskStatus.inProgress: return 'In Progress';
|
||||||
|
case TaskStatus.completed: return 'Done';
|
||||||
|
case TaskStatus.skipped: return 'Skipped';
|
||||||
|
case TaskStatus.archived: return 'Archived';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String get apiValue {
|
||||||
|
switch (this) {
|
||||||
|
case TaskStatus.pending: return 'pending';
|
||||||
|
case TaskStatus.inProgress: return 'in_progress';
|
||||||
|
case TaskStatus.completed: return 'completed';
|
||||||
|
case TaskStatus.skipped: return 'skipped';
|
||||||
|
case TaskStatus.archived: return 'archived';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
lib/src/models/api_response.dart
Normal file
44
lib/src/models/api_response.dart
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
part 'api_response.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable(genericArgumentFactories: true)
|
||||||
|
class ApiResponse<T> {
|
||||||
|
final String status;
|
||||||
|
final T? data;
|
||||||
|
final ApiError? error;
|
||||||
|
final PaginationMeta? meta;
|
||||||
|
|
||||||
|
const ApiResponse({required this.status, this.data, this.error, this.meta});
|
||||||
|
|
||||||
|
factory ApiResponse.success(T data, {PaginationMeta? meta}) =>
|
||||||
|
ApiResponse(status: 'success', data: data, meta: meta);
|
||||||
|
|
||||||
|
factory ApiResponse.error(String code, String message) =>
|
||||||
|
ApiResponse(status: 'error', error: ApiError(code: code, message: message));
|
||||||
|
|
||||||
|
factory ApiResponse.fromJson(Map<String, dynamic> json, T Function(Object? json) fromJsonT) =>
|
||||||
|
_$ApiResponseFromJson(json, fromJsonT);
|
||||||
|
Map<String, dynamic> toJson(Object? Function(T value) toJsonT) =>
|
||||||
|
_$ApiResponseToJson(this, toJsonT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class ApiError {
|
||||||
|
final String code;
|
||||||
|
final String message;
|
||||||
|
final dynamic details;
|
||||||
|
const ApiError({required this.code, required this.message, this.details});
|
||||||
|
factory ApiError.fromJson(Map<String, dynamic> json) => _$ApiErrorFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$ApiErrorToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class PaginationMeta {
|
||||||
|
final int page;
|
||||||
|
final int limit;
|
||||||
|
final int total;
|
||||||
|
final bool hasMore;
|
||||||
|
const PaginationMeta({required this.page, required this.limit, required this.total, required this.hasMore});
|
||||||
|
factory PaginationMeta.fromJson(Map<String, dynamic> json) => _$PaginationMetaFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$PaginationMetaToJson(this);
|
||||||
|
}
|
||||||
71
lib/src/models/api_response.g.dart
Normal file
71
lib/src/models/api_response.g.dart
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'api_response.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
ApiResponse<T> _$ApiResponseFromJson<T>(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
T Function(Object? json) fromJsonT,
|
||||||
|
) => ApiResponse<T>(
|
||||||
|
status: json['status'] as String,
|
||||||
|
data: _$nullableGenericFromJson(json['data'], fromJsonT),
|
||||||
|
error:
|
||||||
|
json['error'] == null
|
||||||
|
? null
|
||||||
|
: ApiError.fromJson(json['error'] as Map<String, dynamic>),
|
||||||
|
meta:
|
||||||
|
json['meta'] == null
|
||||||
|
? null
|
||||||
|
: PaginationMeta.fromJson(json['meta'] as Map<String, dynamic>),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$ApiResponseToJson<T>(
|
||||||
|
ApiResponse<T> instance,
|
||||||
|
Object? Function(T value) toJsonT,
|
||||||
|
) => <String, dynamic>{
|
||||||
|
'status': instance.status,
|
||||||
|
'data': _$nullableGenericToJson(instance.data, toJsonT),
|
||||||
|
'error': instance.error,
|
||||||
|
'meta': instance.meta,
|
||||||
|
};
|
||||||
|
|
||||||
|
T? _$nullableGenericFromJson<T>(
|
||||||
|
Object? input,
|
||||||
|
T Function(Object? json) fromJson,
|
||||||
|
) => input == null ? null : fromJson(input);
|
||||||
|
|
||||||
|
Object? _$nullableGenericToJson<T>(
|
||||||
|
T? input,
|
||||||
|
Object? Function(T value) toJson,
|
||||||
|
) => input == null ? null : toJson(input);
|
||||||
|
|
||||||
|
ApiError _$ApiErrorFromJson(Map<String, dynamic> json) => ApiError(
|
||||||
|
code: json['code'] as String,
|
||||||
|
message: json['message'] as String,
|
||||||
|
details: json['details'],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$ApiErrorToJson(ApiError instance) => <String, dynamic>{
|
||||||
|
'code': instance.code,
|
||||||
|
'message': instance.message,
|
||||||
|
'details': instance.details,
|
||||||
|
};
|
||||||
|
|
||||||
|
PaginationMeta _$PaginationMetaFromJson(Map<String, dynamic> json) =>
|
||||||
|
PaginationMeta(
|
||||||
|
page: (json['page'] as num).toInt(),
|
||||||
|
limit: (json['limit'] as num).toInt(),
|
||||||
|
total: (json['total'] as num).toInt(),
|
||||||
|
hasMore: json['hasMore'] as bool,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$PaginationMetaToJson(PaginationMeta instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'page': instance.page,
|
||||||
|
'limit': instance.limit,
|
||||||
|
'total': instance.total,
|
||||||
|
'hasMore': instance.hasMore,
|
||||||
|
};
|
||||||
34
lib/src/models/coworking_room.dart
Normal file
34
lib/src/models/coworking_room.dart
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
part 'coworking_room.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class CoworkingRoom {
|
||||||
|
final String id;
|
||||||
|
final String hostUserId;
|
||||||
|
final String name;
|
||||||
|
final String? description;
|
||||||
|
final int maxParticipants;
|
||||||
|
final bool isPublic;
|
||||||
|
final String status; // active, ended, scheduled
|
||||||
|
final String? ambientSound; // cafe, rain, library, silent
|
||||||
|
final DateTime? scheduledAt;
|
||||||
|
final DateTime? endedAt;
|
||||||
|
final DateTime createdAt;
|
||||||
|
|
||||||
|
const CoworkingRoom({
|
||||||
|
required this.id,
|
||||||
|
required this.hostUserId,
|
||||||
|
required this.name,
|
||||||
|
this.description,
|
||||||
|
this.maxParticipants = 10,
|
||||||
|
this.isPublic = true,
|
||||||
|
this.status = 'active',
|
||||||
|
this.ambientSound,
|
||||||
|
this.scheduledAt,
|
||||||
|
this.endedAt,
|
||||||
|
required this.createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory CoworkingRoom.fromJson(Map<String, dynamic> json) => _$CoworkingRoomFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$CoworkingRoomToJson(this);
|
||||||
|
}
|
||||||
43
lib/src/models/coworking_room.g.dart
Normal file
43
lib/src/models/coworking_room.g.dart
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'coworking_room.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
CoworkingRoom _$CoworkingRoomFromJson(Map<String, dynamic> json) =>
|
||||||
|
CoworkingRoom(
|
||||||
|
id: json['id'] as String,
|
||||||
|
hostUserId: json['hostUserId'] as String,
|
||||||
|
name: json['name'] as String,
|
||||||
|
description: json['description'] as String?,
|
||||||
|
maxParticipants: (json['maxParticipants'] as num?)?.toInt() ?? 10,
|
||||||
|
isPublic: json['isPublic'] as bool? ?? true,
|
||||||
|
status: json['status'] as String? ?? 'active',
|
||||||
|
ambientSound: json['ambientSound'] as String?,
|
||||||
|
scheduledAt:
|
||||||
|
json['scheduledAt'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['scheduledAt'] as String),
|
||||||
|
endedAt:
|
||||||
|
json['endedAt'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['endedAt'] as String),
|
||||||
|
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$CoworkingRoomToJson(CoworkingRoom instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'hostUserId': instance.hostUserId,
|
||||||
|
'name': instance.name,
|
||||||
|
'description': instance.description,
|
||||||
|
'maxParticipants': instance.maxParticipants,
|
||||||
|
'isPublic': instance.isPublic,
|
||||||
|
'status': instance.status,
|
||||||
|
'ambientSound': instance.ambientSound,
|
||||||
|
'scheduledAt': instance.scheduledAt?.toIso8601String(),
|
||||||
|
'endedAt': instance.endedAt?.toIso8601String(),
|
||||||
|
'createdAt': instance.createdAt.toIso8601String(),
|
||||||
|
};
|
||||||
32
lib/src/models/reward.dart
Normal file
32
lib/src/models/reward.dart
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
part 'reward.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class Reward {
|
||||||
|
final String id;
|
||||||
|
final String userId;
|
||||||
|
final String? taskId;
|
||||||
|
final String rewardType; // points, badge, animation, message, unlock, surprise
|
||||||
|
final int magnitude; // 1-100
|
||||||
|
final String? title;
|
||||||
|
final String? description;
|
||||||
|
final String? animationKey; // Lottie animation identifier
|
||||||
|
final bool isSurprise;
|
||||||
|
final DateTime createdAt;
|
||||||
|
|
||||||
|
const Reward({
|
||||||
|
required this.id,
|
||||||
|
required this.userId,
|
||||||
|
this.taskId,
|
||||||
|
required this.rewardType,
|
||||||
|
required this.magnitude,
|
||||||
|
this.title,
|
||||||
|
this.description,
|
||||||
|
this.animationKey,
|
||||||
|
this.isSurprise = false,
|
||||||
|
required this.createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Reward.fromJson(Map<String, dynamic> json) => _$RewardFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$RewardToJson(this);
|
||||||
|
}
|
||||||
33
lib/src/models/reward.g.dart
Normal file
33
lib/src/models/reward.g.dart
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'reward.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
Reward _$RewardFromJson(Map<String, dynamic> json) => Reward(
|
||||||
|
id: json['id'] as String,
|
||||||
|
userId: json['userId'] as String,
|
||||||
|
taskId: json['taskId'] as String?,
|
||||||
|
rewardType: json['rewardType'] as String,
|
||||||
|
magnitude: (json['magnitude'] as num).toInt(),
|
||||||
|
title: json['title'] as String?,
|
||||||
|
description: json['description'] as String?,
|
||||||
|
animationKey: json['animationKey'] as String?,
|
||||||
|
isSurprise: json['isSurprise'] as bool? ?? false,
|
||||||
|
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$RewardToJson(Reward instance) => <String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'userId': instance.userId,
|
||||||
|
'taskId': instance.taskId,
|
||||||
|
'rewardType': instance.rewardType,
|
||||||
|
'magnitude': instance.magnitude,
|
||||||
|
'title': instance.title,
|
||||||
|
'description': instance.description,
|
||||||
|
'animationKey': instance.animationKey,
|
||||||
|
'isSurprise': instance.isSurprise,
|
||||||
|
'createdAt': instance.createdAt.toIso8601String(),
|
||||||
|
};
|
||||||
40
lib/src/models/streak.dart
Normal file
40
lib/src/models/streak.dart
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
part 'streak.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class Streak {
|
||||||
|
final String id;
|
||||||
|
final String userId;
|
||||||
|
final String name;
|
||||||
|
final String streakType; // daily, weekly, custom
|
||||||
|
final int currentCount;
|
||||||
|
final int longestCount;
|
||||||
|
final int graceDays;
|
||||||
|
final int graceUsed;
|
||||||
|
final DateTime? lastCompleted;
|
||||||
|
final bool isActive;
|
||||||
|
final DateTime? frozenUntil;
|
||||||
|
final bool encouragementShown;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime? updatedAt;
|
||||||
|
|
||||||
|
const Streak({
|
||||||
|
required this.id,
|
||||||
|
required this.userId,
|
||||||
|
required this.name,
|
||||||
|
this.streakType = 'daily',
|
||||||
|
this.currentCount = 0,
|
||||||
|
this.longestCount = 0,
|
||||||
|
this.graceDays = 2,
|
||||||
|
this.graceUsed = 0,
|
||||||
|
this.lastCompleted,
|
||||||
|
this.isActive = true,
|
||||||
|
this.frozenUntil,
|
||||||
|
this.encouragementShown = false,
|
||||||
|
required this.createdAt,
|
||||||
|
this.updatedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Streak.fromJson(Map<String, dynamic> json) => _$StreakFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$StreakToJson(this);
|
||||||
|
}
|
||||||
50
lib/src/models/streak.g.dart
Normal file
50
lib/src/models/streak.g.dart
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'streak.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
Streak _$StreakFromJson(Map<String, dynamic> json) => Streak(
|
||||||
|
id: json['id'] as String,
|
||||||
|
userId: json['userId'] as String,
|
||||||
|
name: json['name'] as String,
|
||||||
|
streakType: json['streakType'] as String? ?? 'daily',
|
||||||
|
currentCount: (json['currentCount'] as num?)?.toInt() ?? 0,
|
||||||
|
longestCount: (json['longestCount'] as num?)?.toInt() ?? 0,
|
||||||
|
graceDays: (json['graceDays'] as num?)?.toInt() ?? 2,
|
||||||
|
graceUsed: (json['graceUsed'] as num?)?.toInt() ?? 0,
|
||||||
|
lastCompleted:
|
||||||
|
json['lastCompleted'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['lastCompleted'] as String),
|
||||||
|
isActive: json['isActive'] as bool? ?? true,
|
||||||
|
frozenUntil:
|
||||||
|
json['frozenUntil'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['frozenUntil'] as String),
|
||||||
|
encouragementShown: json['encouragementShown'] as bool? ?? false,
|
||||||
|
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||||
|
updatedAt:
|
||||||
|
json['updatedAt'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['updatedAt'] as String),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$StreakToJson(Streak instance) => <String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'userId': instance.userId,
|
||||||
|
'name': instance.name,
|
||||||
|
'streakType': instance.streakType,
|
||||||
|
'currentCount': instance.currentCount,
|
||||||
|
'longestCount': instance.longestCount,
|
||||||
|
'graceDays': instance.graceDays,
|
||||||
|
'graceUsed': instance.graceUsed,
|
||||||
|
'lastCompleted': instance.lastCompleted?.toIso8601String(),
|
||||||
|
'isActive': instance.isActive,
|
||||||
|
'frozenUntil': instance.frozenUntil?.toIso8601String(),
|
||||||
|
'encouragementShown': instance.encouragementShown,
|
||||||
|
'createdAt': instance.createdAt.toIso8601String(),
|
||||||
|
'updatedAt': instance.updatedAt?.toIso8601String(),
|
||||||
|
};
|
||||||
24
lib/src/models/streak_entry.dart
Normal file
24
lib/src/models/streak_entry.dart
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
part 'streak_entry.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class StreakEntry {
|
||||||
|
final String id;
|
||||||
|
final String streakId;
|
||||||
|
final DateTime completedDate;
|
||||||
|
final bool wasForgiven;
|
||||||
|
final String? note;
|
||||||
|
final DateTime createdAt;
|
||||||
|
|
||||||
|
const StreakEntry({
|
||||||
|
required this.id,
|
||||||
|
required this.streakId,
|
||||||
|
required this.completedDate,
|
||||||
|
this.wasForgiven = false,
|
||||||
|
this.note,
|
||||||
|
required this.createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory StreakEntry.fromJson(Map<String, dynamic> json) => _$StreakEntryFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$StreakEntryToJson(this);
|
||||||
|
}
|
||||||
26
lib/src/models/streak_entry.g.dart
Normal file
26
lib/src/models/streak_entry.g.dart
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'streak_entry.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
StreakEntry _$StreakEntryFromJson(Map<String, dynamic> json) => StreakEntry(
|
||||||
|
id: json['id'] as String,
|
||||||
|
streakId: json['streakId'] as String,
|
||||||
|
completedDate: DateTime.parse(json['completedDate'] as String),
|
||||||
|
wasForgiven: json['wasForgiven'] as bool? ?? false,
|
||||||
|
note: json['note'] as String?,
|
||||||
|
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$StreakEntryToJson(StreakEntry instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'streakId': instance.streakId,
|
||||||
|
'completedDate': instance.completedDate.toIso8601String(),
|
||||||
|
'wasForgiven': instance.wasForgiven,
|
||||||
|
'note': instance.note,
|
||||||
|
'createdAt': instance.createdAt.toIso8601String(),
|
||||||
|
};
|
||||||
56
lib/src/models/task.dart
Normal file
56
lib/src/models/task.dart
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
part 'task.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class Task {
|
||||||
|
final String id;
|
||||||
|
final String userId;
|
||||||
|
final String title;
|
||||||
|
final String? description;
|
||||||
|
final String status; // pending, in_progress, completed, skipped, archived
|
||||||
|
final int priority; // 0-100, ML-adjusted
|
||||||
|
final String energyLevel; // low, medium, high
|
||||||
|
final int? estimatedMinutes;
|
||||||
|
final int? actualMinutes;
|
||||||
|
final DateTime? dueAt;
|
||||||
|
final DateTime? completedAt;
|
||||||
|
final String? parentTaskId;
|
||||||
|
final double? dopamineScore; // ML-computed 0-100
|
||||||
|
final double noveltyFactor; // decays over time
|
||||||
|
final DateTime? lastInteracted;
|
||||||
|
final int timesPostponed;
|
||||||
|
final List<String> tags;
|
||||||
|
final String? category;
|
||||||
|
final String? color;
|
||||||
|
final int version;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime? updatedAt;
|
||||||
|
|
||||||
|
const Task({
|
||||||
|
required this.id,
|
||||||
|
required this.userId,
|
||||||
|
required this.title,
|
||||||
|
this.description,
|
||||||
|
this.status = 'pending',
|
||||||
|
this.priority = 0,
|
||||||
|
this.energyLevel = 'medium',
|
||||||
|
this.estimatedMinutes,
|
||||||
|
this.actualMinutes,
|
||||||
|
this.dueAt,
|
||||||
|
this.completedAt,
|
||||||
|
this.parentTaskId,
|
||||||
|
this.dopamineScore,
|
||||||
|
this.noveltyFactor = 1.0,
|
||||||
|
this.lastInteracted,
|
||||||
|
this.timesPostponed = 0,
|
||||||
|
this.tags = const [],
|
||||||
|
this.category,
|
||||||
|
this.color,
|
||||||
|
this.version = 1,
|
||||||
|
required this.createdAt,
|
||||||
|
this.updatedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Task.fromJson(Map<String, dynamic> json) => _$TaskFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$TaskToJson(this);
|
||||||
|
}
|
||||||
68
lib/src/models/task.g.dart
Normal file
68
lib/src/models/task.g.dart
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'task.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
Task _$TaskFromJson(Map<String, dynamic> json) => Task(
|
||||||
|
id: json['id'] as String,
|
||||||
|
userId: json['userId'] as String,
|
||||||
|
title: json['title'] as String,
|
||||||
|
description: json['description'] as String?,
|
||||||
|
status: json['status'] as String? ?? 'pending',
|
||||||
|
priority: (json['priority'] as num?)?.toInt() ?? 0,
|
||||||
|
energyLevel: json['energyLevel'] as String? ?? 'medium',
|
||||||
|
estimatedMinutes: (json['estimatedMinutes'] as num?)?.toInt(),
|
||||||
|
actualMinutes: (json['actualMinutes'] as num?)?.toInt(),
|
||||||
|
dueAt: json['dueAt'] == null ? null : DateTime.parse(json['dueAt'] as String),
|
||||||
|
completedAt:
|
||||||
|
json['completedAt'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['completedAt'] as String),
|
||||||
|
parentTaskId: json['parentTaskId'] as String?,
|
||||||
|
dopamineScore: (json['dopamineScore'] as num?)?.toDouble(),
|
||||||
|
noveltyFactor: (json['noveltyFactor'] as num?)?.toDouble() ?? 1.0,
|
||||||
|
lastInteracted:
|
||||||
|
json['lastInteracted'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['lastInteracted'] as String),
|
||||||
|
timesPostponed: (json['timesPostponed'] as num?)?.toInt() ?? 0,
|
||||||
|
tags:
|
||||||
|
(json['tags'] as List<dynamic>?)?.map((e) => e as String).toList() ??
|
||||||
|
const [],
|
||||||
|
category: json['category'] as String?,
|
||||||
|
color: json['color'] as String?,
|
||||||
|
version: (json['version'] as num?)?.toInt() ?? 1,
|
||||||
|
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||||
|
updatedAt:
|
||||||
|
json['updatedAt'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['updatedAt'] as String),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$TaskToJson(Task instance) => <String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'userId': instance.userId,
|
||||||
|
'title': instance.title,
|
||||||
|
'description': instance.description,
|
||||||
|
'status': instance.status,
|
||||||
|
'priority': instance.priority,
|
||||||
|
'energyLevel': instance.energyLevel,
|
||||||
|
'estimatedMinutes': instance.estimatedMinutes,
|
||||||
|
'actualMinutes': instance.actualMinutes,
|
||||||
|
'dueAt': instance.dueAt?.toIso8601String(),
|
||||||
|
'completedAt': instance.completedAt?.toIso8601String(),
|
||||||
|
'parentTaskId': instance.parentTaskId,
|
||||||
|
'dopamineScore': instance.dopamineScore,
|
||||||
|
'noveltyFactor': instance.noveltyFactor,
|
||||||
|
'lastInteracted': instance.lastInteracted?.toIso8601String(),
|
||||||
|
'timesPostponed': instance.timesPostponed,
|
||||||
|
'tags': instance.tags,
|
||||||
|
'category': instance.category,
|
||||||
|
'color': instance.color,
|
||||||
|
'version': instance.version,
|
||||||
|
'createdAt': instance.createdAt.toIso8601String(),
|
||||||
|
'updatedAt': instance.updatedAt?.toIso8601String(),
|
||||||
|
};
|
||||||
26
lib/src/models/time_estimate.dart
Normal file
26
lib/src/models/time_estimate.dart
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
part 'time_estimate.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class TimeEstimate {
|
||||||
|
final String id;
|
||||||
|
final String userId;
|
||||||
|
final String taskId;
|
||||||
|
final int estimatedMinutes;
|
||||||
|
final int? actualMinutes;
|
||||||
|
final double? accuracyRatio;
|
||||||
|
final DateTime createdAt;
|
||||||
|
|
||||||
|
const TimeEstimate({
|
||||||
|
required this.id,
|
||||||
|
required this.userId,
|
||||||
|
required this.taskId,
|
||||||
|
required this.estimatedMinutes,
|
||||||
|
this.actualMinutes,
|
||||||
|
this.accuracyRatio,
|
||||||
|
required this.createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory TimeEstimate.fromJson(Map<String, dynamic> json) => _$TimeEstimateFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$TimeEstimateToJson(this);
|
||||||
|
}
|
||||||
28
lib/src/models/time_estimate.g.dart
Normal file
28
lib/src/models/time_estimate.g.dart
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'time_estimate.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
TimeEstimate _$TimeEstimateFromJson(Map<String, dynamic> json) => TimeEstimate(
|
||||||
|
id: json['id'] as String,
|
||||||
|
userId: json['userId'] as String,
|
||||||
|
taskId: json['taskId'] as String,
|
||||||
|
estimatedMinutes: (json['estimatedMinutes'] as num).toInt(),
|
||||||
|
actualMinutes: (json['actualMinutes'] as num?)?.toInt(),
|
||||||
|
accuracyRatio: (json['accuracyRatio'] as num?)?.toDouble(),
|
||||||
|
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$TimeEstimateToJson(TimeEstimate instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'userId': instance.userId,
|
||||||
|
'taskId': instance.taskId,
|
||||||
|
'estimatedMinutes': instance.estimatedMinutes,
|
||||||
|
'actualMinutes': instance.actualMinutes,
|
||||||
|
'accuracyRatio': instance.accuracyRatio,
|
||||||
|
'createdAt': instance.createdAt.toIso8601String(),
|
||||||
|
};
|
||||||
42
lib/src/models/user.dart
Normal file
42
lib/src/models/user.dart
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
part 'user.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class User {
|
||||||
|
final String id;
|
||||||
|
final String email;
|
||||||
|
final String displayName;
|
||||||
|
final String? avatarUrl;
|
||||||
|
final String timezone;
|
||||||
|
final bool onboardingCompleted;
|
||||||
|
final int preferredTaskLoad;
|
||||||
|
final int focusSessionMinutes;
|
||||||
|
final String rewardStyle; // playful, minimal, data
|
||||||
|
final bool forgivenessEnabled;
|
||||||
|
final String subscriptionTier; // free, premium, enterprise
|
||||||
|
final DateTime? subscriptionExpires;
|
||||||
|
final DateTime? lastActiveAt;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime? updatedAt;
|
||||||
|
|
||||||
|
const User({
|
||||||
|
required this.id,
|
||||||
|
required this.email,
|
||||||
|
required this.displayName,
|
||||||
|
this.avatarUrl,
|
||||||
|
this.timezone = 'UTC',
|
||||||
|
this.onboardingCompleted = false,
|
||||||
|
this.preferredTaskLoad = 5,
|
||||||
|
this.focusSessionMinutes = 25,
|
||||||
|
this.rewardStyle = 'playful',
|
||||||
|
this.forgivenessEnabled = true,
|
||||||
|
this.subscriptionTier = 'free',
|
||||||
|
this.subscriptionExpires,
|
||||||
|
this.lastActiveAt,
|
||||||
|
required this.createdAt,
|
||||||
|
this.updatedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$UserToJson(this);
|
||||||
|
}
|
||||||
52
lib/src/models/user.g.dart
Normal file
52
lib/src/models/user.g.dart
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'user.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
User _$UserFromJson(Map<String, dynamic> json) => User(
|
||||||
|
id: json['id'] as String,
|
||||||
|
email: json['email'] as String,
|
||||||
|
displayName: json['displayName'] as String,
|
||||||
|
avatarUrl: json['avatarUrl'] as String?,
|
||||||
|
timezone: json['timezone'] as String? ?? 'UTC',
|
||||||
|
onboardingCompleted: json['onboardingCompleted'] as bool? ?? false,
|
||||||
|
preferredTaskLoad: (json['preferredTaskLoad'] as num?)?.toInt() ?? 5,
|
||||||
|
focusSessionMinutes: (json['focusSessionMinutes'] as num?)?.toInt() ?? 25,
|
||||||
|
rewardStyle: json['rewardStyle'] as String? ?? 'playful',
|
||||||
|
forgivenessEnabled: json['forgivenessEnabled'] as bool? ?? true,
|
||||||
|
subscriptionTier: json['subscriptionTier'] as String? ?? 'free',
|
||||||
|
subscriptionExpires:
|
||||||
|
json['subscriptionExpires'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['subscriptionExpires'] as String),
|
||||||
|
lastActiveAt:
|
||||||
|
json['lastActiveAt'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['lastActiveAt'] as String),
|
||||||
|
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||||
|
updatedAt:
|
||||||
|
json['updatedAt'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['updatedAt'] as String),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'email': instance.email,
|
||||||
|
'displayName': instance.displayName,
|
||||||
|
'avatarUrl': instance.avatarUrl,
|
||||||
|
'timezone': instance.timezone,
|
||||||
|
'onboardingCompleted': instance.onboardingCompleted,
|
||||||
|
'preferredTaskLoad': instance.preferredTaskLoad,
|
||||||
|
'focusSessionMinutes': instance.focusSessionMinutes,
|
||||||
|
'rewardStyle': instance.rewardStyle,
|
||||||
|
'forgivenessEnabled': instance.forgivenessEnabled,
|
||||||
|
'subscriptionTier': instance.subscriptionTier,
|
||||||
|
'subscriptionExpires': instance.subscriptionExpires?.toIso8601String(),
|
||||||
|
'lastActiveAt': instance.lastActiveAt?.toIso8601String(),
|
||||||
|
'createdAt': instance.createdAt.toIso8601String(),
|
||||||
|
'updatedAt': instance.updatedAt?.toIso8601String(),
|
||||||
|
};
|
||||||
17
lib/src/validators/task_validator.dart
Normal file
17
lib/src/validators/task_validator.dart
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
class TaskValidator {
|
||||||
|
TaskValidator._();
|
||||||
|
static String? validateTitle(String? title) {
|
||||||
|
if (title == null || title.trim().isEmpty) return 'Title is required';
|
||||||
|
if (title.length > 500) return 'Title must be 500 characters or less';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
static String? validateEstimatedMinutes(int? minutes) {
|
||||||
|
if (minutes != null && minutes < 1) return 'Estimate must be at least 1 minute';
|
||||||
|
if (minutes != null && minutes > 480) return 'Estimate cannot exceed 8 hours';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
static String? validatePriority(int? priority) {
|
||||||
|
if (priority != null && (priority < 0 || priority > 100)) return 'Priority must be 0-100';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
pubspec.yaml
Normal file
17
pubspec.yaml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
name: focusflow_shared
|
||||||
|
description: Shared models, enums, and utilities for FocusFlow ADHD app
|
||||||
|
version: 0.1.0
|
||||||
|
publish_to: 'none'
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ^3.7.0
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
json_annotation: ^4.9.0
|
||||||
|
equatable: ^2.0.7
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
build_runner: ^2.4.0
|
||||||
|
json_serializable: ^6.8.0
|
||||||
|
lints: ^5.0.0
|
||||||
|
test: ^1.24.0
|
||||||
205
test/focusflow_shared_test.dart
Normal file
205
test/focusflow_shared_test.dart
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import 'package:focusflow_shared/focusflow_shared.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Task', () {
|
||||||
|
test('creates with required fields', () {
|
||||||
|
final task = Task(
|
||||||
|
id: 'task-1',
|
||||||
|
userId: 'user-1',
|
||||||
|
title: 'Test task',
|
||||||
|
createdAt: DateTime(2024, 1, 1),
|
||||||
|
);
|
||||||
|
expect(task.id, 'task-1');
|
||||||
|
expect(task.title, 'Test task');
|
||||||
|
expect(task.status, 'pending');
|
||||||
|
expect(task.priority, 0);
|
||||||
|
expect(task.energyLevel, 'medium');
|
||||||
|
expect(task.noveltyFactor, 1.0);
|
||||||
|
expect(task.timesPostponed, 0);
|
||||||
|
expect(task.tags, isEmpty);
|
||||||
|
expect(task.version, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('serializes to JSON and back', () {
|
||||||
|
final task = Task(
|
||||||
|
id: 'task-1',
|
||||||
|
userId: 'user-1',
|
||||||
|
title: 'Test task',
|
||||||
|
priority: 50,
|
||||||
|
tags: ['focus', 'work'],
|
||||||
|
createdAt: DateTime(2024, 1, 1),
|
||||||
|
);
|
||||||
|
final json = task.toJson();
|
||||||
|
final restored = Task.fromJson(json);
|
||||||
|
expect(restored.id, task.id);
|
||||||
|
expect(restored.title, task.title);
|
||||||
|
expect(restored.priority, task.priority);
|
||||||
|
expect(restored.tags, task.tags);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Streak', () {
|
||||||
|
test('creates with defaults', () {
|
||||||
|
final streak = Streak(
|
||||||
|
id: 's-1',
|
||||||
|
userId: 'user-1',
|
||||||
|
name: 'Morning routine',
|
||||||
|
createdAt: DateTime(2024, 1, 1),
|
||||||
|
);
|
||||||
|
expect(streak.streakType, 'daily');
|
||||||
|
expect(streak.currentCount, 0);
|
||||||
|
expect(streak.graceDays, 2);
|
||||||
|
expect(streak.isActive, true);
|
||||||
|
expect(streak.encouragementShown, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('serializes to JSON and back', () {
|
||||||
|
final streak = Streak(
|
||||||
|
id: 's-1',
|
||||||
|
userId: 'user-1',
|
||||||
|
name: 'Morning routine',
|
||||||
|
currentCount: 7,
|
||||||
|
longestCount: 14,
|
||||||
|
createdAt: DateTime(2024, 1, 1),
|
||||||
|
);
|
||||||
|
final json = streak.toJson();
|
||||||
|
final restored = Streak.fromJson(json);
|
||||||
|
expect(restored.name, streak.name);
|
||||||
|
expect(restored.currentCount, 7);
|
||||||
|
expect(restored.longestCount, 14);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('User', () {
|
||||||
|
test('creates with defaults', () {
|
||||||
|
final user = User(
|
||||||
|
id: 'u-1',
|
||||||
|
email: 'test@example.com',
|
||||||
|
displayName: 'Test User',
|
||||||
|
createdAt: DateTime(2024, 1, 1),
|
||||||
|
);
|
||||||
|
expect(user.timezone, 'UTC');
|
||||||
|
expect(user.onboardingCompleted, false);
|
||||||
|
expect(user.preferredTaskLoad, 5);
|
||||||
|
expect(user.focusSessionMinutes, 25);
|
||||||
|
expect(user.rewardStyle, 'playful');
|
||||||
|
expect(user.forgivenessEnabled, true);
|
||||||
|
expect(user.subscriptionTier, 'free');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('ApiResponse', () {
|
||||||
|
test('creates success response', () {
|
||||||
|
final response = ApiResponse<String>.success('hello');
|
||||||
|
expect(response.status, 'success');
|
||||||
|
expect(response.data, 'hello');
|
||||||
|
expect(response.error, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('creates error response', () {
|
||||||
|
final response = ApiResponse<String>.error('NOT_FOUND', 'Task not found');
|
||||||
|
expect(response.status, 'error');
|
||||||
|
expect(response.data, isNull);
|
||||||
|
expect(response.error!.code, 'NOT_FOUND');
|
||||||
|
expect(response.error!.message, 'Task not found');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('TaskValidator', () {
|
||||||
|
test('validates title - empty', () {
|
||||||
|
expect(TaskValidator.validateTitle(null), 'Title is required');
|
||||||
|
expect(TaskValidator.validateTitle(''), 'Title is required');
|
||||||
|
expect(TaskValidator.validateTitle(' '), 'Title is required');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validates title - too long', () {
|
||||||
|
final longTitle = 'x' * 501;
|
||||||
|
expect(TaskValidator.validateTitle(longTitle), 'Title must be 500 characters or less');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validates title - valid', () {
|
||||||
|
expect(TaskValidator.validateTitle('Buy groceries'), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validates estimated minutes', () {
|
||||||
|
expect(TaskValidator.validateEstimatedMinutes(null), isNull);
|
||||||
|
expect(TaskValidator.validateEstimatedMinutes(0), 'Estimate must be at least 1 minute');
|
||||||
|
expect(TaskValidator.validateEstimatedMinutes(481), 'Estimate cannot exceed 8 hours');
|
||||||
|
expect(TaskValidator.validateEstimatedMinutes(30), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validates priority', () {
|
||||||
|
expect(TaskValidator.validatePriority(null), isNull);
|
||||||
|
expect(TaskValidator.validatePriority(-1), 'Priority must be 0-100');
|
||||||
|
expect(TaskValidator.validatePriority(101), 'Priority must be 0-100');
|
||||||
|
expect(TaskValidator.validatePriority(50), isNull);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Enums', () {
|
||||||
|
test('TaskStatus display names', () {
|
||||||
|
expect(TaskStatus.pending.displayName, 'To Do');
|
||||||
|
expect(TaskStatus.inProgress.displayName, 'In Progress');
|
||||||
|
expect(TaskStatus.completed.displayName, 'Done');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('TaskStatus API values', () {
|
||||||
|
expect(TaskStatus.pending.apiValue, 'pending');
|
||||||
|
expect(TaskStatus.inProgress.apiValue, 'in_progress');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('EnergyLevel display names', () {
|
||||||
|
expect(EnergyLevel.low.displayName, 'Low Energy');
|
||||||
|
expect(EnergyLevel.medium.displayName, 'Medium Energy');
|
||||||
|
expect(EnergyLevel.high.displayName, 'High Energy');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('RewardType display names', () {
|
||||||
|
expect(RewardType.animation.displayName, 'Celebration');
|
||||||
|
expect(RewardType.surprise.displayName, 'Surprise!');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('RewardStyle display names', () {
|
||||||
|
expect(RewardStyle.playful.displayName, 'Playful (animations & messages)');
|
||||||
|
expect(RewardStyle.data.displayName, 'Data-driven (stats & charts)');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('ApiConstants', () {
|
||||||
|
test('base path', () {
|
||||||
|
expect(ApiConstants.basePath, '/api/v1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('task routes', () {
|
||||||
|
expect(ApiConstants.tasks, '/api/v1/tasks');
|
||||||
|
expect(ApiConstants.task('abc'), '/api/v1/tasks/abc');
|
||||||
|
expect(ApiConstants.completeTask('abc'), '/api/v1/tasks/abc/complete');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('room routes', () {
|
||||||
|
expect(ApiConstants.rooms, '/api/v1/rooms');
|
||||||
|
expect(ApiConstants.joinRoom('r1'), '/api/v1/rooms/r1/join');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('AppLimits', () {
|
||||||
|
test('free tier limits', () {
|
||||||
|
expect(AppLimits.freeMaxActiveTasks, 15);
|
||||||
|
expect(AppLimits.freeMaxStreaks, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pagination defaults', () {
|
||||||
|
expect(AppLimits.defaultPageSize, 20);
|
||||||
|
expect(AppLimits.maxPageSize, 100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('ErrorCodes', () {
|
||||||
|
test('error code values', () {
|
||||||
|
expect(ErrorCodes.taskNotFound, 'TASK_NOT_FOUND');
|
||||||
|
expect(ErrorCodes.premiumRequired, 'PREMIUM_REQUIRED');
|
||||||
|
expect(ErrorCodes.rateLimited, 'RATE_LIMITED');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user