Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updates #1

Merged
merged 6 commits into from
Jan 19, 2025
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
5 changes: 4 additions & 1 deletion android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<application
android:label="TestLine App"
android:label="Quixzy (beta)"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
Expand Down
Binary file added assets/fonts/Montserrat-Black.ttf
Binary file not shown.
Binary file added assets/fonts/Montserrat-BlackItalic.ttf
Binary file not shown.
Binary file added assets/fonts/Montserrat-Bold.ttf
Binary file not shown.
Binary file added assets/fonts/Montserrat-BoldItalic.ttf
Binary file not shown.
Binary file added assets/fonts/Montserrat-ExtraBold.ttf
Binary file not shown.
Binary file added assets/fonts/Montserrat-ExtraBoldItalic.ttf
Binary file not shown.
Binary file added assets/fonts/Montserrat-ExtraLight.ttf
Binary file not shown.
Binary file added assets/fonts/Montserrat-ExtraLightItalic.ttf
Binary file not shown.
Binary file added assets/fonts/Montserrat-Italic.ttf
Binary file not shown.
Binary file added assets/fonts/Montserrat-Light.ttf
Binary file not shown.
Binary file added assets/fonts/Montserrat-LightItalic.ttf
Binary file not shown.
Binary file added assets/fonts/Montserrat-Medium.ttf
Binary file not shown.
Binary file added assets/fonts/Montserrat-MediumItalic.ttf
Binary file not shown.
Binary file added assets/fonts/Montserrat-Regular.ttf
Binary file not shown.
Binary file added assets/fonts/Montserrat-SemiBold.ttf
Binary file not shown.
Binary file added assets/fonts/Montserrat-SemiBoldItalic.ttf
Binary file not shown.
Binary file added assets/fonts/Montserrat-Thin.ttf
Binary file not shown.
Binary file added assets/fonts/Montserrat-ThinItalic.ttf
Binary file not shown.
Binary file added assets/sfx/error_sound.mp3
Binary file not shown.
Binary file added assets/sfx/sucess_effect.mp3
Binary file not shown.
374 changes: 80 additions & 294 deletions lib/screens/quiz_attempts_review_page.dart

Large diffs are not rendered by default.

25 changes: 22 additions & 3 deletions lib/screens/quiz_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import '../widgets/quiz_app_bar.dart';
import '../widgets/question_card.dart';
import '../widgets/quiz_bottom_bar.dart';
import '../service/proctor_service.dart';
import '../service/feedback_service.dart';
import 'quiz_review_page.dart';
import 'quiz_start_page.dart';

Expand All @@ -29,16 +30,33 @@ class _QuizScreenState extends State<QuizScreen> with WidgetsBindingObserver {
Question? get currentQuestion => widget.quiz.questions?[currentQuestionIndex];
final Map<int, int> questionAnswers = {};
final ProctorService _proctorService = ProctorService();
final FeedbackService _feedbackService = FeedbackService();
bool _isTestTerminated = false;

@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_initializeProctoring();
_initializeServices();
_startTimer();
}

Future<void> _initializeServices() async {
try {
await _feedbackService.initialize();
await _initializeProctoring();
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to initialize services: $e'),
backgroundColor: Colors.red,
),
);
}
}
}

Future<void> _initializeProctoring() async {
try {
await _proctorService.initialize(
Expand Down Expand Up @@ -117,6 +135,7 @@ class _QuizScreenState extends State<QuizScreen> with WidgetsBindingObserver {
WidgetsBinding.instance.removeObserver(this);
questionTimer?.cancel();
_proctorService.dispose();
_feedbackService.dispose();
super.dispose();
}

Expand Down Expand Up @@ -205,8 +224,8 @@ class _QuizScreenState extends State<QuizScreen> with WidgetsBindingObserver {
);
}

return WillPopScope(
onWillPop: () async => false,
return PopScope(
canPop: false,
child: Scaffold(
body: SafeArea(
child: Column(
Expand Down
58 changes: 47 additions & 11 deletions lib/screens/quiz_start_page.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:animated_text_kit/animated_text_kit.dart';
import '../models/quiz_model.dart';
import 'quiz_screen.dart';

Expand All @@ -16,31 +18,44 @@ class QuizStartPage extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
quiz.title ?? 'Quiz',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
color: Colors.teal,
fontWeight: FontWeight.bold,
),
AnimatedTextKit(
animatedTexts: [
TypewriterAnimatedText(
quiz.title ?? 'Quiz',
textStyle:
Theme.of(context).textTheme.headlineMedium?.copyWith(
color: Colors.teal,
fontWeight: FontWeight.bold,
),
speed: const Duration(milliseconds: 100),
),
],
totalRepeatCount: 1,
displayFullTextOnTap: true,
),
const SizedBox(height: 8),
Text(
quiz.topic ?? '',
style: Theme.of(context).textTheme.bodyLarge,
),
)
.animate()
.fadeIn(delay: 500.ms, duration: 500.ms)
.slideX(begin: -0.2, end: 0),
const SizedBox(height: 32),
_buildInfoCard(
context,
title: 'Duration',
value: '${quiz.duration ?? 30} seconds per question',
icon: Icons.timer,
delay: 0,
),
const SizedBox(height: 16),
_buildInfoCard(
context,
title: 'Questions',
value: '${quiz.questions?.length ?? 0} questions',
icon: Icons.quiz,
delay: 200,
),
const SizedBox(height: 16),
_buildInfoCard(
Expand All @@ -49,6 +64,7 @@ class QuizStartPage extends StatelessWidget {
value:
'+${quiz.correct_answer_marks} for correct, -${quiz.negative_marks} for incorrect',
icon: Icons.grade,
delay: 400,
),
const Spacer(),
SizedBox(
Expand All @@ -57,8 +73,21 @@ class QuizStartPage extends StatelessWidget {
onPressed: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => QuizScreen(quiz: quiz),
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
QuizScreen(quiz: quiz),
transitionsBuilder:
(context, animation, secondaryAnimation, child) {
const begin = Offset(1.0, 0.0);
const end = Offset.zero;
const curve = Curves.easeInOutCubic;
var tween = Tween(begin: begin, end: end)
.chain(CurveTween(curve: curve));
var offsetAnimation = animation.drive(tween);
return SlideTransition(
position: offsetAnimation, child: child);
},
transitionDuration: const Duration(milliseconds: 500),
),
);
},
Expand All @@ -74,7 +103,10 @@ class QuizStartPage extends StatelessWidget {
style: TextStyle(fontSize: 18),
),
),
),
)
.animate()
.fadeIn(delay: 800.ms, duration: 500.ms)
.slideY(begin: 0.2, end: 0),
],
),
),
Expand All @@ -87,6 +119,7 @@ class QuizStartPage extends StatelessWidget {
required String title,
required String value,
required IconData icon,
required int delay,
}) {
return Container(
padding: const EdgeInsets.all(16),
Expand Down Expand Up @@ -125,6 +158,9 @@ class QuizStartPage extends StatelessWidget {
),
],
),
);
)
.animate()
.fadeIn(delay: delay.ms, duration: 500.ms)
.slideX(begin: 0.2, end: 0);
}
}
157 changes: 157 additions & 0 deletions lib/service/feedback_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import 'package:flutter/services.dart';
import 'package:just_audio/just_audio.dart';
import 'package:flutter/foundation.dart';
import 'package:audio_session/audio_session.dart';

class FeedbackService {
static final FeedbackService _instance = FeedbackService._internal();
factory FeedbackService() => _instance;
FeedbackService._internal();

AudioPlayer? _successPlayer;
AudioPlayer? _errorPlayer;
bool _isSoundEnabled = true;
bool _isHapticEnabled = true;
bool _isInitialized = false;

// Initialize audio files
Future<void> initialize() async {
if (_isInitialized) return;

try {
// Create new instances of AudioPlayer
_successPlayer = AudioPlayer();
_errorPlayer = AudioPlayer();

// Configure audio session
final session = await AudioSession.instance;
await session.configure(const AudioSessionConfiguration(
avAudioSessionCategory: AVAudioSessionCategory.playback,
androidAudioAttributes: AndroidAudioAttributes(
contentType: AndroidAudioContentType.sonification,
usage: AndroidAudioUsage.assistanceSonification,
),
));

// Load audio files
await Future.wait([
_successPlayer!.setAsset('assets/sfx/sucess_effect.mp3'),
_errorPlayer!.setAsset('assets/sfx/error_sound.mp3'),
]);

// Set volume and other properties
await Future.wait([
_successPlayer!.setVolume(0.5),
_errorPlayer!.setVolume(0.5),
]);

_isInitialized = true;
} catch (e) {
debugPrint('Error initializing audio: $e');
// Reset state on error
_isInitialized = false;
await dispose();
}
}

// Play haptic feedback based on feedback type
Future<void> _playHapticFeedback(bool isSuccess) async {
if (!_isHapticEnabled) return;

try {
if (isSuccess) {
await HapticFeedback.lightImpact();
await Future.delayed(const Duration(milliseconds: 100));
await HapticFeedback.lightImpact();
} else {
await HapticFeedback.heavyImpact();
await Future.delayed(const Duration(milliseconds: 200));
await HapticFeedback.heavyImpact();
}
} catch (e) {
debugPrint('Error playing haptic feedback: $e');
}
}

// Play success sound and vibration
Future<void> playSuccess() async {
if (!_isInitialized || _successPlayer == null) {
try {
await initialize();
} catch (e) {
debugPrint('Failed to initialize on playSuccess: $e');
return;
}
}

// Play haptic feedback first for better synchronization
await _playHapticFeedback(true);

if (_isSoundEnabled && _isInitialized && _successPlayer != null) {
try {
await _successPlayer!.seek(Duration.zero);
await _successPlayer!.play();
} catch (e) {
debugPrint('Error playing success sound: $e');
// Try to reinitialize on error
_isInitialized = false;
}
}
}

// Play error sound and vibration
Future<void> playError() async {
if (!_isInitialized || _errorPlayer == null) {
try {
await initialize();
} catch (e) {
debugPrint('Failed to initialize on playError: $e');
return;
}
}

// Play haptic feedback first for better synchronization
await _playHapticFeedback(false);

if (_isSoundEnabled && _isInitialized && _errorPlayer != null) {
try {
await _errorPlayer!.seek(Duration.zero);
await _errorPlayer!.play();
} catch (e) {
debugPrint('Error playing error sound: $e');
// Try to reinitialize on error
_isInitialized = false;
}
}
}

// Toggle sound effects
void toggleSound() {
_isSoundEnabled = !_isSoundEnabled;
}

// Toggle haptic feedback
void toggleHaptic() {
_isHapticEnabled = !_isHapticEnabled;
}

// Dispose resources
Future<void> dispose() async {
_isInitialized = false;
try {
await Future.wait([
_successPlayer?.dispose() ?? Future.value(),
_errorPlayer?.dispose() ?? Future.value(),
]);
_successPlayer = null;
_errorPlayer = null;
} catch (e) {
debugPrint('Error disposing audio players: $e');
}
}

// Getters for current state
bool get isSoundEnabled => _isSoundEnabled;
bool get isHapticEnabled => _isHapticEnabled;
bool get isInitialized => _isInitialized;
}
Loading