diff --git a/.gitignore b/.gitignore index e802407..5762234 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,7 @@ dist/ downloads/ eggs/ .eggs/ -lib/ +# lib/ - commented out for Flutter compatibility lib64/ parts/ sdist/ @@ -65,3 +65,57 @@ coverage.xml # Mac .DS_Store + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# 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 + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Flutter.podspec +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/ephemeral/ +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* diff --git a/README.md b/README.md index 29ab470..7b45c36 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,15 @@ A focused Modern Standard Arabic learning platform designed to take learners from foundational script and pronunciation to advanced professional fluency over a 40-day journey. The program offers modular daily lessons through interactive audio‑visual materials, YouTube video demonstrations, real‑world conversation practice, and culturally relevant topics—ideal for travelers, professionals, and global citizens. +**Now available as a Flutter mobile application!** 📱 + ## Technical Skills Demonstrated -### Web Development -- Interactive, responsive web interface using HTML5, CSS3, and modern JavaScript -- Dynamic content updates and micro‑interactions for enhanced user engagement -- YouTube API integration for embedded instructional videos -- Mobile‑first responsive design using media queries and grid layouts +### Flutter/Mobile Development +- Cross-platform mobile application (iOS & Android) using Flutter & Dart +- Native performance with smooth animations and transitions +- Offline-first architecture with local asset management +- Material Design UI with custom theming and RTL support ### Python Development - Automated content generation scripts for lesson materials @@ -42,23 +44,25 @@ A focused Modern Standard Arabic learning platform designed to take learners fro ## Project Structure -- `index.html`: Main dashboard with daily progress overview -- `day.html`: Daily lesson interface with audio, text, and exercises -- `supplementary.html`: Additional learning resources and practice materials -- `css/`: Stylesheets for the web interface - - `styles.css`: Main stylesheet - - `video-player.css`: Styles for the YouTube video player - - `native-speaker.css`: Styles for native speaker content -- `js/`: JavaScript functionality and interactive features - - `script.js`: Core application logic - - `video-loader.js`: Loads YouTube videos for lessons - - `video-loader-supplementary.js`: Loads supplementary YouTube videos -- `audio_files/`: - - Supplementary audio files in `supplementary/` -- `text_files/`: - - Supplementary text files in `supplementary/` +### Flutter Application +- `lib/`: Flutter/Dart source code + - `main.dart`: Application entry point + - `models/`: Data models (LessonDay, SupplementaryContent, VideoInfo) + - `screens/`: UI screens (Dashboard, Lesson, Supplementary) + - `services/`: Business logic (ContentService, ProgressService) + - `utils/`: Utilities (AppTheme, constants) +- `pubspec.yaml`: Flutter project configuration and dependencies +- `analysis_options.yaml`: Dart/Flutter linting rules + +### Assets & Content +- `audio_files/`: MP3 audio files for lessons + - `supplementary/`: Supplementary audio files +- `text_files/`: Lesson content in three languages + - `supplementary/`: Supplementary text files - `videos.json`: YouTube video IDs for each daily lesson - `videos_supplementary.json`: IDs for supplementary videos + +### Content Generation (Python) - Python content generation scripts: - `arabic_phrases_days_01_07.py`: Phrases for days 1-7 - `arabic_phrases_days_08_14.py`: Phrases for days 8-14 @@ -69,6 +73,11 @@ A focused Modern Standard Arabic learning platform designed to take learners fro - `video_search.py`: Tool for searching relevant videos - `requirements.txt`: Python package dependencies +### Legacy Web Version +- `index.html`, `day.html`, `supplementary.html`: Original web interface +- `css/`: Web stylesheets +- `js/`: Web JavaScript files + ## Course Structure (40 Days) ### Foundations (Days 1–7) @@ -133,14 +142,58 @@ A focused Modern Standard Arabic learning platform designed to take learners fro ## Development Setup -### Requirements +### Flutter Application Setup + +#### Requirements +- Flutter SDK 3.0.0 or higher +- Dart SDK 3.0.0 or higher +- Android Studio / Xcode (for mobile deployment) +- An emulator or physical device + +#### Installation +1. Clone the repository: + ```bash + git clone https://github.com/dbsectrainer/Arabic-Pathways.git + cd Arabic-Pathways + ``` + +2. Install Flutter dependencies: + ```bash + flutter pub get + ``` + +3. Run the app: + ```bash + # On a connected device or emulator + flutter run + + # For specific platforms + flutter run -d android + flutter run -d ios + ``` + +#### Building for Production +```bash +# Android APK +flutter build apk + +# Android App Bundle +flutter build appbundle + +# iOS +flutter build ios +``` + +### Content Generation (Python) + +#### Requirements - Python 3.12+ - Required Python packages: ```bash pip install gtts edge-tts pandas ``` -### Generate Lessons +#### Generate Lessons ```bash # Generate content for days 1-40 python arabic_phrases_days_01_07.py @@ -153,36 +206,36 @@ python arabic_phrases_days_31_40.py python arabic_phrases_supplementary.py ``` -### Run the Site +### Legacy Web Version Open `index.html` in any modern browser—no server setup required. ## Usage Guide -1. Launch the dashboard and select your current day -2. Listen to English explanations and native Arabic pronunciations -3. Read along with transliteration and Arabic script -4. Complete interactive exercises and script tracing drills -5. Watch YouTube demonstrations for cultural and pronunciation context -6. Earn daily badges and track your fluency progress - -## Storage -Uses localStorage to save: -- Completed lessons and last visited day -- Audio playback preferences (e.g., speed, loop) -- Custom notes and bookmarks +1. Launch the app and view the dashboard with all 40 days +2. Select any day to start learning +3. Switch between Arabic, transliteration, and English views +4. Listen to native Arabic pronunciation and English explanations +5. Watch embedded YouTube videos for visual learning +6. Mark lessons complete to track your progress +7. Access supplementary content for additional practice ## Features -- 40 days of Arabic phrases with audio and transliteration -- Supplementary materials -- Video and audio search -## Getting Started -1. Clone the repo -2. Install requirements: `pip install -r requirements.txt` -3. Run scripts as needed +### Mobile App Features +- **Offline Support**: All lessons and audio work without internet +- **Progress Tracking**: Track completed lessons with persistent storage +- **Trilingual Display**: Toggle between Arabic, transliteration, and English +- **Audio Playback**: Built-in audio player for pronunciation practice +- **Video Integration**: Embedded YouTube videos for each lesson +- **Responsive Design**: Works on phones and tablets +- **RTL Support**: Proper right-to-left text rendering for Arabic ## Running Tests ```bash +# Python tests pytest + +# Flutter tests +flutter test ``` ## Contributing diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..d992f76 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,13 @@ +include: package:flutter_lints/flutter.yaml + +linter: + rules: + - always_declare_return_types + - avoid_print + - prefer_const_constructors + - prefer_const_declarations + - prefer_const_literals_to_create_immutables + - prefer_final_fields + - prefer_final_locals + - sort_child_properties_last + - use_key_in_widget_constructors diff --git a/fonts/README.md b/fonts/README.md new file mode 100644 index 0000000..dd75dc2 --- /dev/null +++ b/fonts/README.md @@ -0,0 +1,39 @@ +# Fonts Setup + +This Flutter application uses two font families: + +## Required Fonts + +### 1. Poppins +- Download from [Google Fonts](https://fonts.google.com/specimen/Poppins) +- Required weights: + - `Poppins-Regular.ttf` (400) + - `Poppins-Bold.ttf` (700) + +### 2. Noto Sans Arabic +- Download from [Google Fonts](https://fonts.google.com/noto/specimen/Noto+Sans+Arabic) +- Required weights: + - `NotoSansArabic-Regular.ttf` (400) + - `NotoSansArabic-Bold.ttf` (700) + +## Installation + +1. Download the font files from the links above +2. Place them in this `fonts/` directory with the exact names listed +3. The fonts should be automatically detected by Flutter via `pubspec.yaml` + +## Alternative: System Fonts + +If you prefer not to bundle fonts, you can modify `lib/utils/app_theme.dart` to use system fonts instead by removing or commenting out the `fontFamily` properties. + +## Font File Structure + +After setup, your directory should look like: +``` +fonts/ +├── README.md (this file) +├── Poppins-Regular.ttf +├── Poppins-Bold.ttf +├── NotoSansArabic-Regular.ttf +└── NotoSansArabic-Bold.ttf +``` diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..f6facb6 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'screens/dashboard_screen.dart'; +import 'utils/app_theme.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + + // Set preferred orientations + SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + ]); + + runApp(const ArabicPathwaysApp()); +} + +class ArabicPathwaysApp extends StatelessWidget { + const ArabicPathwaysApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Arabic Pathways', + theme: AppTheme.lightTheme, + debugShowCheckedModeBanner: false, + home: const DashboardScreen(), + ); + } +} diff --git a/lib/models/lesson_day.dart b/lib/models/lesson_day.dart new file mode 100644 index 0000000..e0c6234 --- /dev/null +++ b/lib/models/lesson_day.dart @@ -0,0 +1,39 @@ +class LessonDay { + final int dayNumber; + final String title; + final String arabicContent; + final String transliterationContent; + final String englishContent; + final String arabicAudioPath; + final String englishAudioPath; + final String? videoId; + final String level; + + LessonDay({ + required this.dayNumber, + required this.title, + required this.arabicContent, + required this.transliterationContent, + required this.englishContent, + required this.arabicAudioPath, + required this.englishAudioPath, + this.videoId, + required this.level, + }); + + String get levelName { + if (dayNumber <= 7) return 'Level 1: Foundations'; + if (dayNumber <= 14) return 'Level 2: Essential Daily Phrases'; + if (dayNumber <= 22) return 'Level 3: Cultural & Social'; + if (dayNumber <= 30) return 'Level 4: Professional Communication'; + return 'Level 5: Advanced Fluency'; + } + + String get levelColor { + if (dayNumber <= 7) return '#4A90E2'; + if (dayNumber <= 14) return '#50C878'; + if (dayNumber <= 22) return '#FF6B6B'; + if (dayNumber <= 30) return '#9B59B6'; + return '#F39C12'; + } +} diff --git a/lib/models/supplementary_content.dart b/lib/models/supplementary_content.dart new file mode 100644 index 0000000..65f8fde --- /dev/null +++ b/lib/models/supplementary_content.dart @@ -0,0 +1,21 @@ +class SupplementaryContent { + final String id; + final String title; + final String arabicContent; + final String transliterationContent; + final String englishContent; + final String arabicAudioPath; + final String englishAudioPath; + final String? videoId; + + SupplementaryContent({ + required this.id, + required this.title, + required this.arabicContent, + required this.transliterationContent, + required this.englishContent, + required this.arabicAudioPath, + required this.englishAudioPath, + this.videoId, + }); +} diff --git a/lib/models/video_info.dart b/lib/models/video_info.dart new file mode 100644 index 0000000..8ebeff2 --- /dev/null +++ b/lib/models/video_info.dart @@ -0,0 +1,32 @@ +import 'dart:convert'; + +class VideoInfo { + final String day; + final String videoId; + + VideoInfo({ + required this.day, + required this.videoId, + }); + + factory VideoInfo.fromJson(Map json) { + return VideoInfo( + day: json['day'] as String, + videoId: json['videoId'] as String, + ); + } + + Map toJson() { + return { + 'day': day, + 'videoId': videoId, + }; + } +} + +class VideoDatabase { + static Map parseVideosJson(String jsonString) { + final Map decoded = json.decode(jsonString); + return decoded.map((key, value) => MapEntry(key, value.toString())); + } +} diff --git a/lib/screens/dashboard_screen.dart b/lib/screens/dashboard_screen.dart new file mode 100644 index 0000000..3581ee1 --- /dev/null +++ b/lib/screens/dashboard_screen.dart @@ -0,0 +1,322 @@ +import 'package:flutter/material.dart'; +import '../services/progress_service.dart'; +import '../services/content_service.dart'; +import '../utils/app_theme.dart'; +import 'lesson_screen.dart'; +import 'supplementary_screen.dart'; + +class DashboardScreen extends StatefulWidget { + const DashboardScreen({super.key}); + + @override + State createState() => _DashboardScreenState(); +} + +class _DashboardScreenState extends State { + final ProgressService _progressService = ProgressService(); + final ContentService _contentService = ContentService(); + Set _completedDays = {}; + double _progressPercentage = 0; + int _currentPage = 0; + final int _daysPerPage = 10; + + @override + void initState() { + super.initState(); + _loadProgress(); + } + + Future _loadProgress() async { + final completed = await _progressService.getCompletedDays(); + final percentage = await _progressService.getProgressPercentage(); + setState(() { + _completedDays = completed; + _progressPercentage = percentage; + }); + } + + void _navigateToLesson(int dayNumber) async { + final result = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => LessonScreen(dayNumber: dayNumber), + ), + ); + if (result == true) { + _loadProgress(); + } + } + + void _navigateToSupplementary() { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const SupplementaryScreen(), + ), + ); + } + + @override + Widget build(BuildContext context) { + final startDay = _currentPage * _daysPerPage + 1; + final endDay = (startDay + _daysPerPage - 1).clamp(1, 40); + + return Scaffold( + appBar: AppBar( + title: const Text('Arabic Pathways'), + actions: [ + IconButton( + icon: const Icon(Icons.library_books), + onPressed: _navigateToSupplementary, + tooltip: 'Supplementary Content', + ), + ], + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildHeader(), + const SizedBox(height: 24), + _buildProgressSection(), + const SizedBox(height: 24), + _buildLevelIndicators(), + const SizedBox(height: 24), + _buildDayGrid(startDay, endDay), + const SizedBox(height: 16), + _buildPagination(), + ], + ), + ), + ); + } + + Widget _buildHeader() { + return Card( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + const Icon( + Icons.school, + size: 48, + color: AppTheme.primaryColor, + ), + const SizedBox(height: 12), + Text( + 'Master Arabic in 40 Days', + style: Theme.of(context).textTheme.displayMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + 'Structured lessons from basics to fluency', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.grey[600], + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ); + } + + Widget _buildProgressSection() { + return Card( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Your Progress', + style: Theme.of(context).textTheme.titleLarge, + ), + Text( + '${_progressPercentage.toStringAsFixed(0)}%', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: AppTheme.primaryColor, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 12), + LinearProgressIndicator( + value: _progressPercentage / 100, + backgroundColor: Colors.grey[200], + valueColor: const AlwaysStoppedAnimation( + AppTheme.primaryColor, + ), + minHeight: 10, + ), + const SizedBox(height: 8), + Text( + '${_completedDays.length} of 40 days completed', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.grey[600], + ), + ), + ], + ), + ), + ); + } + + Widget _buildLevelIndicators() { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Levels', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 12), + _buildLevelIndicator('Level 1: Foundations', AppTheme.primaryColor, '1-7'), + _buildLevelIndicator('Level 2: Essential Daily', AppTheme.secondaryColor, '8-14'), + _buildLevelIndicator('Level 3: Cultural & Social', AppTheme.accentColor, '15-22'), + _buildLevelIndicator('Level 4: Professional', AppTheme.purpleColor, '23-30'), + _buildLevelIndicator('Level 5: Advanced Fluency', AppTheme.orangeColor, '31-40'), + ], + ), + ), + ); + } + + Widget _buildLevelIndicator(String title, Color color, String days) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + Container( + width: 12, + height: 12, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 8), + Expanded(child: Text(title)), + Text( + 'Days $days', + style: TextStyle(color: Colors.grey[600], fontSize: 12), + ), + ], + ), + ); + } + + Widget _buildDayGrid(int startDay, int endDay) { + return GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 1.5, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + itemCount: endDay - startDay + 1, + itemBuilder: (context, index) { + final dayNumber = startDay + index; + return _buildDayCard(dayNumber); + }, + ); + } + + Widget _buildDayCard(int dayNumber) { + final isCompleted = _completedDays.contains(dayNumber); + final color = AppTheme.getLevelColor(dayNumber); + + return InkWell( + onTap: () => _navigateToLesson(dayNumber), + borderRadius: BorderRadius.circular(12), + child: Card( + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: color.withOpacity(0.3), + width: 2, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + 'Day $dayNumber', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + if (isCompleted) + const Icon( + Icons.check_circle, + color: AppTheme.secondaryColor, + size: 20, + ), + ], + ), + const SizedBox(height: 8), + const Icon( + Icons.play_circle_outline, + size: 32, + color: Colors.grey, + ), + ], + ), + ), + ), + ); + } + + Widget _buildPagination() { + final totalPages = (40 / _daysPerPage).ceil(); + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + icon: const Icon(Icons.chevron_left), + onPressed: _currentPage > 0 + ? () => setState(() => _currentPage--) + : null, + ), + Text( + 'Page ${_currentPage + 1} of $totalPages', + style: Theme.of(context).textTheme.bodyMedium, + ), + IconButton( + icon: const Icon(Icons.chevron_right), + onPressed: _currentPage < totalPages - 1 + ? () => setState(() => _currentPage++) + : null, + ), + ], + ); + } +} diff --git a/lib/screens/lesson_screen.dart b/lib/screens/lesson_screen.dart new file mode 100644 index 0000000..201f71e --- /dev/null +++ b/lib/screens/lesson_screen.dart @@ -0,0 +1,304 @@ +import 'package:flutter/material.dart'; +import 'package:audioplayers/audioplayers.dart'; +import 'package:youtube_player_flutter/youtube_player_flutter.dart'; +import '../models/lesson_day.dart'; +import '../services/content_service.dart'; +import '../services/progress_service.dart'; +import '../utils/app_theme.dart'; + +class LessonScreen extends StatefulWidget { + final int dayNumber; + + const LessonScreen({super.key, required this.dayNumber}); + + @override + State createState() => _LessonScreenState(); +} + +class _LessonScreenState extends State { + final ContentService _contentService = ContentService(); + final ProgressService _progressService = ProgressService(); + final AudioPlayer _audioPlayer = AudioPlayer(); + + LessonDay? _lesson; + bool _isLoading = true; + bool _isCompleted = false; + String _selectedLanguage = 'arabic'; + bool _isPlayingArabic = false; + bool _isPlayingEnglish = false; + YoutubePlayerController? _videoController; + + @override + void initState() { + super.initState(); + _loadLesson(); + } + + Future _loadLesson() async { + final lesson = await _contentService.loadDay(widget.dayNumber); + final completed = await _progressService.getCompletedDays(); + + if (lesson.videoId != null && lesson.videoId!.isNotEmpty) { + _videoController = YoutubePlayerController( + initialVideoId: lesson.videoId!, + flags: const YoutubePlayerFlags( + autoPlay: false, + mute: false, + ), + ); + } + + setState(() { + _lesson = lesson; + _isCompleted = completed.contains(widget.dayNumber); + _isLoading = false; + }); + } + + Future _toggleCompletion() async { + if (_isCompleted) { + await _progressService.markDayIncomplete(widget.dayNumber); + } else { + await _progressService.markDayComplete(widget.dayNumber); + } + setState(() { + _isCompleted = !_isCompleted; + }); + } + + Future _playAudio(String audioPath, bool isArabic) async { + try { + await _audioPlayer.stop(); + await _audioPlayer.play(AssetSource(audioPath)); + setState(() { + if (isArabic) { + _isPlayingArabic = true; + _isPlayingEnglish = false; + } else { + _isPlayingArabic = false; + _isPlayingEnglish = true; + } + }); + + _audioPlayer.onPlayerComplete.listen((_) { + setState(() { + _isPlayingArabic = false; + _isPlayingEnglish = false; + }); + }); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error playing audio: $e')), + ); + } + } + + @override + void dispose() { + _audioPlayer.dispose(); + _videoController?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (_isLoading) { + return Scaffold( + appBar: AppBar(title: Text('Day ${widget.dayNumber}')), + body: const Center(child: CircularProgressIndicator()), + ); + } + + final lesson = _lesson!; + final levelColor = AppTheme.getLevelColor(widget.dayNumber); + + return Scaffold( + appBar: AppBar( + title: Text('Day ${widget.dayNumber}'), + backgroundColor: levelColor, + actions: [ + IconButton( + icon: Icon(_isCompleted ? Icons.check_circle : Icons.circle_outlined), + onPressed: _toggleCompletion, + tooltip: _isCompleted ? 'Mark Incomplete' : 'Mark Complete', + ), + ], + ), + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildLevelBanner(lesson, levelColor), + _buildLanguageSelector(), + _buildContent(lesson), + const SizedBox(height: 16), + _buildAudioControls(lesson), + const SizedBox(height: 16), + if (_videoController != null) _buildVideoPlayer(), + const SizedBox(height: 24), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + Navigator.pop(context, true); + }, + backgroundColor: levelColor, + child: const Icon(Icons.arrow_back), + ), + ); + } + + Widget _buildLevelBanner(LessonDay lesson, Color color) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + border: Border(bottom: BorderSide(color: color, width: 2)), + ), + child: Column( + children: [ + Text( + lesson.levelName, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: color, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 4), + Text( + 'Day ${widget.dayNumber} of 40', + style: TextStyle( + fontSize: 14, + color: Colors.grey[700], + ), + ), + ], + ), + ); + } + + Widget _buildLanguageSelector() { + return Container( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Expanded(child: _buildLanguageButton('Arabic', 'arabic')), + const SizedBox(width: 8), + Expanded(child: _buildLanguageButton('Transliteration', 'transliteration')), + const SizedBox(width: 8), + Expanded(child: _buildLanguageButton('English', 'english')), + ], + ), + ); + } + + Widget _buildLanguageButton(String label, String value) { + final isSelected = _selectedLanguage == value; + return ElevatedButton( + onPressed: () => setState(() => _selectedLanguage = value), + style: ElevatedButton.styleFrom( + backgroundColor: isSelected ? AppTheme.primaryColor : Colors.grey[300], + foregroundColor: isSelected ? Colors.white : Colors.black87, + padding: const EdgeInsets.symmetric(vertical: 12), + ), + child: Text(label, style: const TextStyle(fontSize: 12)), + ); + } + + Widget _buildContent(LessonDay lesson) { + String content; + TextDirection direction = TextDirection.ltr; + String fontFamily = 'Poppins'; + + switch (_selectedLanguage) { + case 'arabic': + content = lesson.arabicContent; + direction = TextDirection.rtl; + fontFamily = 'NotoSansArabic'; + break; + case 'transliteration': + content = lesson.transliterationContent; + break; + case 'english': + content = lesson.englishContent; + break; + default: + content = lesson.arabicContent; + } + + return Container( + padding: const EdgeInsets.all(16), + child: Card( + child: Padding( + padding: const EdgeInsets.all(20), + child: Text( + content, + style: TextStyle( + fontSize: _selectedLanguage == 'arabic' ? 20 : 16, + height: 1.8, + fontFamily: fontFamily, + ), + textDirection: direction, + ), + ), + ), + ); + } + + Widget _buildAudioControls(LessonDay lesson) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: () => _playAudio(lesson.arabicAudioPath, true), + icon: Icon(_isPlayingArabic ? Icons.stop : Icons.play_arrow), + label: Text(_isPlayingArabic ? 'Stop Arabic' : 'Play Arabic'), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.primaryColor, + padding: const EdgeInsets.symmetric(vertical: 16), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton.icon( + onPressed: () => _playAudio(lesson.englishAudioPath, false), + icon: Icon(_isPlayingEnglish ? Icons.stop : Icons.play_arrow), + label: Text(_isPlayingEnglish ? 'Stop English' : 'Play English'), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.secondaryColor, + padding: const EdgeInsets.symmetric(vertical: 16), + ), + ), + ), + ], + ), + ); + } + + Widget _buildVideoPlayer() { + return Container( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Video Lesson', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 12), + YoutubePlayer( + controller: _videoController!, + showVideoProgressIndicator: true, + progressIndicatorColor: AppTheme.primaryColor, + ), + ], + ), + ); + } +} diff --git a/lib/screens/supplementary_screen.dart b/lib/screens/supplementary_screen.dart new file mode 100644 index 0000000..6efa280 --- /dev/null +++ b/lib/screens/supplementary_screen.dart @@ -0,0 +1,348 @@ +import 'package:flutter/material.dart'; +import 'package:audioplayers/audioplayers.dart'; +import 'package:youtube_player_flutter/youtube_player_flutter.dart'; +import '../models/supplementary_content.dart'; +import '../services/content_service.dart'; +import '../utils/app_theme.dart'; + +class SupplementaryScreen extends StatefulWidget { + const SupplementaryScreen({super.key}); + + @override + State createState() => _SupplementaryScreenState(); +} + +class _SupplementaryScreenState extends State { + final ContentService _contentService = ContentService(); + final AudioPlayer _audioPlayer = AudioPlayer(); + + String? _selectedContentId; + SupplementaryContent? _currentContent; + bool _isLoading = false; + String _selectedLanguage = 'arabic'; + bool _isPlayingArabic = false; + bool _isPlayingEnglish = false; + YoutubePlayerController? _videoController; + + @override + void dispose() { + _audioPlayer.dispose(); + _videoController?.dispose(); + super.dispose(); + } + + Future _loadContent(String contentId) async { + setState(() { + _isLoading = true; + _selectedContentId = contentId; + }); + + final content = await _contentService.loadSupplementaryContent(contentId); + + YoutubePlayerController? controller; + if (content.videoId != null && content.videoId!.isNotEmpty) { + controller = YoutubePlayerController( + initialVideoId: content.videoId!, + flags: const YoutubePlayerFlags( + autoPlay: false, + mute: false, + ), + ); + } + + setState(() { + _currentContent = content; + _videoController?.dispose(); + _videoController = controller; + _isLoading = false; + }); + } + + Future _playAudio(String audioPath, bool isArabic) async { + try { + await _audioPlayer.stop(); + await _audioPlayer.play(AssetSource(audioPath)); + setState(() { + if (isArabic) { + _isPlayingArabic = true; + _isPlayingEnglish = false; + } else { + _isPlayingArabic = false; + _isPlayingEnglish = true; + } + }); + + _audioPlayer.onPlayerComplete.listen((_) { + setState(() { + _isPlayingArabic = false; + _isPlayingEnglish = false; + }); + }); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error playing audio: $e')), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Supplementary Content'), + ), + body: Column( + children: [ + _buildContentList(), + if (_isLoading) + const Expanded( + child: Center(child: CircularProgressIndicator()), + ) + else if (_currentContent != null) + Expanded( + child: SingleChildScrollView( + child: _buildContentView(_currentContent!), + ), + ) + else + Expanded( + child: Center( + child: Text( + 'Select a topic to start learning', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Colors.grey, + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildContentList() { + final contentIds = _contentService.getSupplementaryContentIds(); + return Container( + height: 120, + padding: const EdgeInsets.all(16), + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: contentIds.length, + itemBuilder: (context, index) { + final contentId = contentIds[index]; + final isSelected = contentId == _selectedContentId; + return _buildContentCard(contentId, isSelected); + }, + ), + ); + } + + Widget _buildContentCard(String contentId, bool isSelected) { + final icons = { + 'daily_life': Icons.home, + 'emotions': Icons.favorite, + 'education': Icons.school, + 'hobbies': Icons.sports_soccer, + 'comparisons': Icons.compare_arrows, + }; + + final titles = { + 'daily_life': 'Daily Life', + 'emotions': 'Emotions', + 'education': 'Education', + 'hobbies': 'Hobbies', + 'comparisons': 'Comparisons', + }; + + return Container( + width: 140, + margin: const EdgeInsets.only(right: 12), + child: InkWell( + onTap: () => _loadContent(contentId), + borderRadius: BorderRadius.circular(12), + child: Card( + elevation: isSelected ? 8 : 2, + color: isSelected ? AppTheme.primaryColor : null, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icons[contentId] ?? Icons.book, + size: 32, + color: isSelected ? Colors.white : AppTheme.primaryColor, + ), + const SizedBox(height: 8), + Text( + titles[contentId] ?? contentId, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: isSelected ? Colors.white : Colors.black87, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildContentView(SupplementaryContent content) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppTheme.primaryColor.withOpacity(0.1), + border: Border( + bottom: BorderSide(color: AppTheme.primaryColor, width: 2), + ), + ), + child: Text( + content.title, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: AppTheme.primaryColor, + ), + textAlign: TextAlign.center, + ), + ), + _buildLanguageSelector(), + _buildContent(content), + const SizedBox(height: 16), + _buildAudioControls(content), + const SizedBox(height: 16), + if (_videoController != null) _buildVideoPlayer(), + const SizedBox(height: 24), + ], + ); + } + + Widget _buildLanguageSelector() { + return Container( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Expanded(child: _buildLanguageButton('Arabic', 'arabic')), + const SizedBox(width: 8), + Expanded(child: _buildLanguageButton('Transliteration', 'transliteration')), + const SizedBox(width: 8), + Expanded(child: _buildLanguageButton('English', 'english')), + ], + ), + ); + } + + Widget _buildLanguageButton(String label, String value) { + final isSelected = _selectedLanguage == value; + return ElevatedButton( + onPressed: () => setState(() => _selectedLanguage = value), + style: ElevatedButton.styleFrom( + backgroundColor: isSelected ? AppTheme.primaryColor : Colors.grey[300], + foregroundColor: isSelected ? Colors.white : Colors.black87, + padding: const EdgeInsets.symmetric(vertical: 12), + ), + child: Text(label, style: const TextStyle(fontSize: 12)), + ); + } + + Widget _buildContent(SupplementaryContent content) { + String contentText; + TextDirection direction = TextDirection.ltr; + String fontFamily = 'Poppins'; + + switch (_selectedLanguage) { + case 'arabic': + contentText = content.arabicContent; + direction = TextDirection.rtl; + fontFamily = 'NotoSansArabic'; + break; + case 'transliteration': + contentText = content.transliterationContent; + break; + case 'english': + contentText = content.englishContent; + break; + default: + contentText = content.arabicContent; + } + + return Container( + padding: const EdgeInsets.all(16), + child: Card( + child: Padding( + padding: const EdgeInsets.all(20), + child: Text( + contentText, + style: TextStyle( + fontSize: _selectedLanguage == 'arabic' ? 20 : 16, + height: 1.8, + fontFamily: fontFamily, + ), + textDirection: direction, + ), + ), + ), + ); + } + + Widget _buildAudioControls(SupplementaryContent content) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: () => _playAudio(content.arabicAudioPath, true), + icon: Icon(_isPlayingArabic ? Icons.stop : Icons.play_arrow), + label: Text(_isPlayingArabic ? 'Stop Arabic' : 'Play Arabic'), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.primaryColor, + padding: const EdgeInsets.symmetric(vertical: 16), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton.icon( + onPressed: () => _playAudio(content.englishAudioPath, false), + icon: Icon(_isPlayingEnglish ? Icons.stop : Icons.play_arrow), + label: Text(_isPlayingEnglish ? 'Stop English' : 'Play English'), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.secondaryColor, + padding: const EdgeInsets.symmetric(vertical: 16), + ), + ), + ), + ], + ), + ); + } + + Widget _buildVideoPlayer() { + return Container( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Video Lesson', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 12), + YoutubePlayer( + controller: _videoController!, + showVideoProgressIndicator: true, + progressIndicatorColor: AppTheme.primaryColor, + ), + ], + ), + ); + } +} diff --git a/lib/services/content_service.dart b/lib/services/content_service.dart new file mode 100644 index 0000000..f4eee73 --- /dev/null +++ b/lib/services/content_service.dart @@ -0,0 +1,126 @@ +import 'dart:convert'; +import 'package:flutter/services.dart'; +import '../models/lesson_day.dart'; +import '../models/supplementary_content.dart'; + +class ContentService { + // Cache for loaded content + final Map _dayCache = {}; + final Map _supplementaryCache = {}; + Map? _videos; + Map? _supplementaryVideos; + + Future _loadTextFile(String path) async { + try { + return await rootBundle.loadString(path); + } catch (e) { + return ''; + } + } + + Future> _loadVideos() async { + if (_videos != null) return _videos!; + try { + final jsonString = await rootBundle.loadString('videos.json'); + final Map decoded = json.decode(jsonString); + _videos = decoded.map((key, value) => MapEntry(key, value.toString())); + return _videos!; + } catch (e) { + _videos = {}; + return _videos!; + } + } + + Future> _loadSupplementaryVideos() async { + if (_supplementaryVideos != null) return _supplementaryVideos!; + try { + final jsonString = await rootBundle.loadString('videos_supplementary.json'); + final Map decoded = json.decode(jsonString); + _supplementaryVideos = decoded.map((key, value) => MapEntry(key, value.toString())); + return _supplementaryVideos!; + } catch (e) { + _supplementaryVideos = {}; + return _supplementaryVideos!; + } + } + + Future loadDay(int dayNumber) async { + if (_dayCache.containsKey(dayNumber)) { + return _dayCache[dayNumber]!; + } + + final videos = await _loadVideos(); + final arabicContent = await _loadTextFile('text_files/day${dayNumber}_ar.txt'); + final transliterationContent = await _loadTextFile('text_files/day${dayNumber}_transliteration.txt'); + final englishContent = await _loadTextFile('text_files/day${dayNumber}_en.txt'); + + final day = LessonDay( + dayNumber: dayNumber, + title: 'Day $dayNumber', + arabicContent: arabicContent, + transliterationContent: transliterationContent, + englishContent: englishContent, + arabicAudioPath: 'audio_files/day${dayNumber}_ar.mp3', + englishAudioPath: 'audio_files/day${dayNumber}_en.mp3', + videoId: videos['day$dayNumber'], + level: '', + ); + + _dayCache[dayNumber] = day; + return day; + } + + Future> loadAllDays() async { + final days = []; + for (int i = 1; i <= 40; i++) { + days.add(await loadDay(i)); + } + return days; + } + + Future loadSupplementaryContent(String contentId) async { + if (_supplementaryCache.containsKey(contentId)) { + return _supplementaryCache[contentId]!; + } + + final videos = await _loadSupplementaryVideos(); + final arabicContent = await _loadTextFile('text_files/supplementary/${contentId}_ar.txt'); + final transliterationContent = await _loadTextFile('text_files/supplementary/${contentId}_transliteration.txt'); + final englishContent = await _loadTextFile('text_files/supplementary/${contentId}_en.txt'); + + final content = SupplementaryContent( + id: contentId, + title: _getSupplementaryTitle(contentId), + arabicContent: arabicContent, + transliterationContent: transliterationContent, + englishContent: englishContent, + arabicAudioPath: 'audio_files/supplementary/${contentId}_ar.mp3', + englishAudioPath: 'audio_files/supplementary/${contentId}_en.mp3', + videoId: videos[contentId], + ); + + _supplementaryCache[contentId] = content; + return content; + } + + String _getSupplementaryTitle(String id) { + switch (id) { + case 'daily_life': + return 'Daily Life Conversations'; + case 'emotions': + return 'Emotions & Feelings'; + case 'education': + return 'Education'; + case 'hobbies': + return 'Hobbies & Interests'; + case 'comparisons': + return 'Comparative Phrases'; + default: + return id; + } + } + + List getSupplementaryContentIds() { + return ['daily_life', 'emotions', 'education', 'hobbies', 'comparisons']; + } +} diff --git a/lib/services/progress_service.dart b/lib/services/progress_service.dart new file mode 100644 index 0000000..d1adbc3 --- /dev/null +++ b/lib/services/progress_service.dart @@ -0,0 +1,53 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +class ProgressService { + static const String _completedDaysKey = 'completed_days'; + static const String _currentDayKey = 'current_day'; + + Future> getCompletedDays() async { + final prefs = await SharedPreferences.getInstance(); + final completedList = prefs.getStringList(_completedDaysKey) ?? []; + return completedList.map((e) => int.parse(e)).toSet(); + } + + Future markDayComplete(int dayNumber) async { + final prefs = await SharedPreferences.getInstance(); + final completed = await getCompletedDays(); + completed.add(dayNumber); + await prefs.setStringList( + _completedDaysKey, + completed.map((e) => e.toString()).toList(), + ); + } + + Future markDayIncomplete(int dayNumber) async { + final prefs = await SharedPreferences.getInstance(); + final completed = await getCompletedDays(); + completed.remove(dayNumber); + await prefs.setStringList( + _completedDaysKey, + completed.map((e) => e.toString()).toList(), + ); + } + + Future getCurrentDay() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getInt(_currentDayKey) ?? 1; + } + + Future setCurrentDay(int dayNumber) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setInt(_currentDayKey, dayNumber); + } + + Future getProgressPercentage() async { + final completed = await getCompletedDays(); + return (completed.length / 40) * 100; + } + + Future resetProgress() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_completedDaysKey); + await prefs.setInt(_currentDayKey, 1); + } +} diff --git a/lib/utils/app_theme.dart b/lib/utils/app_theme.dart new file mode 100644 index 0000000..a6d5a56 --- /dev/null +++ b/lib/utils/app_theme.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; + +class AppTheme { + static const Color primaryColor = Color(0xFF4A90E2); + static const Color secondaryColor = Color(0xFF50C878); + static const Color accentColor = Color(0xFFFF6B6B); + static const Color purpleColor = Color(0xFF9B59B6); + static const Color orangeColor = Color(0xFFF39C12); + + static ThemeData get lightTheme { + return ThemeData( + primaryColor: primaryColor, + scaffoldBackgroundColor: Colors.white, + fontFamily: 'Poppins', + colorScheme: const ColorScheme.light( + primary: primaryColor, + secondary: secondaryColor, + ), + appBarTheme: const AppBarTheme( + backgroundColor: primaryColor, + foregroundColor: Colors.white, + elevation: 0, + centerTitle: true, + titleTextStyle: TextStyle( + fontFamily: 'Poppins', + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + cardTheme: CardTheme( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: primaryColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + textTheme: const TextTheme( + displayLarge: TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + displayMedium: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + bodyLarge: TextStyle( + fontSize: 16, + color: Colors.black87, + ), + bodyMedium: TextStyle( + fontSize: 14, + color: Colors.black87, + ), + ), + ); + } + + static Color getLevelColor(int dayNumber) { + if (dayNumber <= 7) return primaryColor; + if (dayNumber <= 14) return secondaryColor; + if (dayNumber <= 22) return accentColor; + if (dayNumber <= 30) return purpleColor; + return orangeColor; + } + + static String getLevelName(int dayNumber) { + if (dayNumber <= 7) return 'Level 1: Foundations'; + if (dayNumber <= 14) return 'Level 2: Essential Daily Phrases'; + if (dayNumber <= 22) return 'Level 3: Cultural & Social'; + if (dayNumber <= 30) return 'Level 4: Professional Communication'; + return 'Level 5: Advanced Fluency'; + } +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..0aa7fb3 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,67 @@ +name: arabic_pathways +description: A 40-day structured Arabic language learning application +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + + # UI Components + cupertino_icons: ^1.0.6 + + # Audio Playback + audioplayers: ^5.2.1 + + # Video Playback + youtube_player_flutter: ^8.1.2 + + # Local Storage + shared_preferences: ^2.2.2 + + # State Management + provider: ^6.1.1 + + # JSON Handling + json_annotation: ^4.8.1 + + # HTTP (for future API calls if needed) + http: ^1.1.2 + + # Path Provider (for file access) + path_provider: ^2.1.1 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.1 + json_serializable: ^6.7.1 + build_runner: ^2.4.7 + +flutter: + uses-material-design: true + + # Assets + assets: + - audio_files/ + - audio_files/supplementary/ + - text_files/ + - text_files/supplementary/ + - videos.json + - videos_supplementary.json + + # Fonts + fonts: + - family: Poppins + fonts: + - asset: fonts/Poppins-Regular.ttf + - asset: fonts/Poppins-Bold.ttf + weight: 700 + - family: NotoSansArabic + fonts: + - asset: fonts/NotoSansArabic-Regular.ttf + - asset: fonts/NotoSansArabic-Bold.ttf + weight: 700