Skip to content
This repository was archived by the owner on Dec 9, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 109 additions & 33 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## Project Overview

**Parachute** is a cross-platform Flutter voice recording application designed for seamless background operation and crystal-clear audio capture. The app is currently in prototype phase with placeholder implementations for audio recording functionality.
**Parachute** is a cross-platform Flutter voice recording application with file-based syncing, crystal-clear audio capture, and AI-powered transcription via OpenAI Whisper API.

## Development Commands

### Build and Run

```bash
# Get dependencies
flutter pub get
Expand All @@ -19,69 +20,144 @@ flutter run
# Run on specific platform
flutter run -d ios
flutter run -d android
flutter run -d chrome

# Build for release
flutter build apk
flutter build ios
flutter build web
```

### Code Quality

```bash
# Run static analysis
# Run static analysis (0 errors expected)
flutter analyze

# Run tests (when available)
# Run tests (27 tests)
flutter test

# Run specific test suites
flutter test test/models/
flutter test test/utils/
flutter test test/repositories/

# Format code
dart format lib/
```

### Icons Generation
```bash
# Generate app icons from assets/icons/dreamflow_icon.jpg
flutter pub run flutter_launcher_icons
## Architecture

The app follows **Flutter 2025 best practices** with clean architecture:

### Layered Structure

```
Domain Layer
└── Models (lib/models/recording.dart)
└── Validators (lib/utils/validators.dart)

## Architecture
Data Layer
└── Repositories (lib/repositories/recording_repository.dart)
└── Services (lib/services/)

### Core Services (Singleton Pattern)
State Management
└── Riverpod Providers (lib/providers/service_providers.dart)

1. **AudioService** (`lib/services/audio_service.dart`): Handles all audio recording operations. Currently uses placeholder implementations with TODO comments for actual audio package integrations (`flutter_sound`, `permission_handler`).
Presentation Layer
└── Screens (lib/screens/)
└── Widgets (lib/widgets/)
```

2. **StorageService** (`lib/services/storage_service.dart`): Manages recording persistence. Currently uses in-memory storage with TODO comments for `shared_preferences` integration.
### Core Services

### Data Model
All services use **Riverpod dependency injection** (no singletons):

- **Recording** (`lib/models/recording.dart`): Core data model with JSON serialization, containing fields for id, title, filePath, timestamp, duration, tags, transcript, and file size.
1. **AudioService** (`lib/services/audio_service.dart`)
- Audio recording/playback using `record` and `just_audio` packages
- Auto-initialized via `audioServiceProvider`
- Proper disposal handling

### Screen Flow
2. **StorageService** (`lib/services/storage_service.dart`)
- File-based storage with markdown metadata
- Syncs via user-configurable folder (iCloud, Syncthing, etc.)
- Auto-initialized via `storageServiceProvider`

1. **HomeScreen** → Lists all recordings, entry point to recording
2. **RecordingScreen** → Active recording interface with pause/resume/stop controls
3. **PostRecordingScreen** → Post-recording metadata entry (title, tags)
4. **RecordingDetailScreen** → View/edit individual recording details
3. **WhisperService** (`lib/services/whisper_service.dart`)
- OpenAI Whisper API integration for transcription
- API key management via StorageService
- Accessed via `whisperServiceProvider`

4. **RecordingRepository** (`lib/repositories/recording_repository.dart`)
- Repository pattern for data access
- Clean CRUD API
- Accessed via `recordingRepositoryProvider`

### State Management

The app uses StatefulWidget for local state management. Services use singleton pattern for app-wide state sharing.
- **Riverpod** for dependency injection and state management
- Screens extend `ConsumerStatefulWidget`
- Services injected via `ref.read(serviceProvider)`
- No singleton pattern - all dependencies managed by Riverpod

### Data Model

- **Recording** (`lib/models/recording.dart`)
- Validated model with assertions
- JSON serialization with null safety
- Computed properties for formatting

### Screen Flow

1. **HomeScreen** → Lists recordings, refreshes on resume
2. **RecordingScreen** → Active recording with pause/resume/stop
3. **PostRecordingScreen** → Add title, tags, transcribe
4. **RecordingDetailScreen** → View/edit/delete recordings
5. **SettingsScreen** → Configure API key and sync folder

### Package Dependencies

Current dependencies with pending integrations:
- `flutter_sound`: Audio recording/playback (TODO: uncommented in services)
- `permission_handler`: Microphone permissions (TODO: uncommented)
- `path_provider`: File system access (TODO: uncommented)
- `shared_preferences`: Persistent storage (TODO: uncommented)
- `google_fonts`: Typography styling (active)
- `cupertino_icons`: iOS-style icons (active)
**Active packages:**

- `flutter_riverpod ^2.6.1` - State management
- `record ^6.1.2` - Audio recording
- `just_audio ^0.9.42` - Audio playback
- `path_provider ^2.0.0` - File system access
- `shared_preferences ^2.0.0` - Settings persistence
- `http ^1.2.0` - Whisper API calls
- `google_fonts ^6.1.0` - Typography

**Dev packages:**

- `flutter_lints ^6.0.0` - Comprehensive linting
- `build_runner` & `riverpod_generator` - Code generation (future)

## Testing

**27 tests** covering:

- Recording model (validation, serialization)
- Input validators (title, API key, tags)
- Repository pattern

Run tests:

```bash
flutter test
```

## Important Notes

- The app name is "parachute" (lowercase in package name)
- Audio functionality currently returns mock data - actual recording packages need to be integrated
- Sample recordings are automatically created on first launch for demo purposes
- Using Flutter 3.35.3 with Dart 3.9.2
- Linting configured via `flutter_lints` package
- App uses **Riverpod** - access services via `ref.read(serviceProvider)`
- Recordings stored as `.m4a` (audio) + `.md` (metadata)
- Sample recordings created on first launch
- Transcription requires OpenAI API key (configured in Settings)
- File-based sync for cross-device support
- Global error boundaries configured in main.dart
- Production-ready with comprehensive validation

## Code Style

- Use `debugPrint()` not `print()`
- Use `withValues(alpha:)` not `withOpacity()`
- Always check `mounted` before using `BuildContext` after async
- Input validation required for user-facing fields
- Prefer `ConsumerStatefulWidget` for screens
46 changes: 45 additions & 1 deletion analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,48 @@
include: package:flutter_lints/flutter.yaml

linter:
rules:
rules:
# Style rules
- prefer_const_constructors
- prefer_const_literals_to_create_immutables
- prefer_const_declarations
- prefer_final_fields
- prefer_final_locals
- sort_constructors_first
- sort_unnamed_constructors_first
- unnecessary_const
- unnecessary_new

# Code quality
- avoid_print
- always_declare_return_types
- avoid_unnecessary_containers
- avoid_empty_else
- avoid_relative_lib_imports
- avoid_returning_null_for_void
- avoid_types_as_parameter_names
- cancel_subscriptions
- close_sinks
- directives_ordering
- prefer_is_empty
- prefer_is_not_empty
- require_trailing_commas
- unnecessary_brace_in_string_interps
- use_key_in_widget_constructors

# Error prevention
- no_duplicate_case_values
- valid_regexps
- unnecessary_statements

analyzer:
errors:
# Treat missing returns as errors
missing_return: error
# Treat missing required parameters as errors
missing_required_param: error
# Allow TODO comments
todo: ignore
exclude:
- "**/*.g.dart"
- "**/*.freezed.dart"
35 changes: 33 additions & 2 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,40 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:parachute/theme.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:parachute/screens/home_screen.dart';
import 'package:parachute/theme.dart';

void main() {
runApp(const MyApp());
// Set up global error handling
FlutterError.onError = (FlutterErrorDetails details) {
// Log the error
FlutterError.presentError(details);

// In production, you could send to crash reporting service
if (kReleaseMode) {
// TODO: Send to crash reporting (Firebase Crashlytics, Sentry, etc.)
debugPrint('Error caught in release mode: ${details.exception}');
}
};

// Catch errors not caught by Flutter
PlatformDispatcher.instance.onError = (error, stack) {
debugPrint('Uncaught error: $error');
debugPrint('Stack trace: $stack');

// In production, send to crash reporting
if (kReleaseMode) {
// TODO: Send to crash reporting
}

return true; // Prevents error from propagating
};

runApp(
const ProviderScope(
child: MyApp(),
),
);
}

class MyApp extends StatelessWidget {
Expand Down
48 changes: 26 additions & 22 deletions lib/models/recording.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'dart:convert';

class Recording {
final String id;
Expand All @@ -19,29 +18,34 @@ class Recording {
required this.tags,
required this.transcript,
required this.fileSizeKB,
});
}) : assert(id.isNotEmpty, 'Recording ID cannot be empty'),
assert(title.isNotEmpty, 'Recording title cannot be empty'),
assert(filePath.isNotEmpty, 'Recording file path cannot be empty'),
assert(duration >= Duration.zero, 'Duration must be non-negative'),
assert(fileSizeKB >= 0, 'File size must be non-negative');

Map<String, dynamic> toJson() => {
'id': id,
'title': title,
'filePath': filePath,
'timestamp': timestamp.toIso8601String(),
'duration': duration.inMilliseconds,
'tags': tags,
'transcript': transcript,
'fileSizeKB': fileSizeKB,
};
'id': id,
'title': title,
'filePath': filePath,
'timestamp': timestamp.toIso8601String(),
'duration': duration.inMilliseconds,
'tags': tags,
'transcript': transcript,
'fileSizeKB': fileSizeKB,
};

factory Recording.fromJson(Map<String, dynamic> json) => Recording(
id: json['id'],
title: json['title'],
filePath: json['filePath'],
timestamp: DateTime.parse(json['timestamp']),
duration: Duration(milliseconds: json['duration']),
tags: List<String>.from(json['tags']),
transcript: json['transcript'],
fileSizeKB: json['fileSizeKB'],
);
id: json['id'] as String? ?? '',
title: json['title'] as String? ?? 'Untitled',
filePath: json['filePath'] as String? ?? '',
timestamp: DateTime.tryParse(json['timestamp'] as String? ?? '') ??
DateTime.now(),
duration: Duration(milliseconds: json['duration'] as int? ?? 0),
tags: (json['tags'] as List<dynamic>?)?.cast<String>() ?? [],
transcript: json['transcript'] as String? ?? '',
fileSizeKB: (json['fileSizeKB'] as num?)?.toDouble() ?? 0.0,
);

String get durationString {
final minutes = duration.inMinutes;
Expand All @@ -59,7 +63,7 @@ class Recording {
String get timeAgo {
final now = DateTime.now();
final difference = now.difference(timestamp);

if (difference.inDays > 0) {
return '${difference.inDays}d ago';
} else if (difference.inHours > 0) {
Expand All @@ -70,4 +74,4 @@ class Recording {
return 'Just now';
}
}
}
}
Loading