fuck it we're going live

master
gitea 2 months ago
parent dbcc5e9521
commit 09cc51c059

@ -1,6 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="decky_app"
android:label="omotomo"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 54 KiB

File diff suppressed because it is too large Load Diff

@ -12,7 +12,7 @@ class DeckyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Decky - Practice Engine',
title: 'omotomo',
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: ThemeMode.dark,

@ -81,7 +81,6 @@ class _AttemptScreenState extends State<AttemptScreen> {
late PageController _pageController;
Timer? _timer;
int _remainingSeconds = 0;
int _startTime = 0;
@override
void initState() {
@ -148,7 +147,6 @@ class _AttemptScreenState extends State<AttemptScreen> {
_remainingSeconds = (_deck!.config.timeLimitSeconds! - elapsedSeconds).clamp(0, _deck!.config.timeLimitSeconds!);
}
} else {
_startTime = DateTime.now().millisecondsSinceEpoch;
_remainingSeconds = _deck!.config.timeLimitSeconds!;
}
_startTimer();
@ -223,6 +221,39 @@ class _AttemptScreenState extends State<AttemptScreen> {
/// Returns true if at least one question has been answered (progress has been made)
bool get _hasAnyProgress => _answers.isNotEmpty || _manualOverrides.isNotEmpty;
/// Returns true if there are unanswered questions
bool get _hasUnansweredQuestions {
if (_attempt == null) return false;
for (final question in _attempt!.questions) {
if (!_answers.containsKey(question.id)) {
return true;
}
}
return false;
}
/// Returns the index of the first unanswered question, or null if all are answered
int? get _firstUnansweredQuestionIndex {
if (_attempt == null) return null;
for (int i = 0; i < _attempt!.questions.length; i++) {
if (!_answers.containsKey(_attempt!.questions[i].id)) {
return i;
}
}
return null;
}
/// Returns the index of the next unanswered question after current, or null
int? get _nextUnansweredQuestionIndex {
if (_attempt == null) return null;
for (int i = _currentQuestionIndex + 1; i < _attempt!.questions.length; i++) {
if (!_answers.containsKey(_attempt!.questions[i].id)) {
return i;
}
}
return null;
}
void _goToPreviousQuestion() {
if (_currentQuestionIndex > 0) {
// Save current answer if any
@ -316,6 +347,82 @@ class _AttemptScreenState extends State<AttemptScreen> {
}
}
void _completeAttemptWithUnanswered() async {
// Ask for confirmation
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Complete Attempt?'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('There are still unanswered questions.'),
const SizedBox(height: 8),
Text(
'Are you sure you want to complete this attempt?',
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Complete Anyway'),
),
],
),
);
if (confirmed == true && mounted) {
_completeAttempt();
}
}
void _jumpToFirstUnanswered() {
// Save current answer if any
if (_hasAnswer) {
if (_hasMultipleCorrect) {
_answers[_currentQuestion.id] = _selectedAnswerIndices.toList()..sort();
} else {
_answers[_currentQuestion.id] = _selectedAnswerIndex!;
}
}
final firstUnanswered = _firstUnansweredQuestionIndex;
if (firstUnanswered != null && _pageController.hasClients) {
_pageController.animateToPage(
firstUnanswered,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
}
void _goToNextUnanswered() {
// Save current answer if any
if (_hasAnswer) {
if (_hasMultipleCorrect) {
_answers[_currentQuestion.id] = _selectedAnswerIndices.toList()..sort();
} else {
_answers[_currentQuestion.id] = _selectedAnswerIndex!;
}
}
final nextUnanswered = _nextUnansweredQuestionIndex;
if (nextUnanswered != null && _pageController.hasClients) {
_pageController.animateToPage(
nextUnanswered,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
}
void _saveForLater() {
if (_deck == null || _attempt == null) return;
@ -448,12 +555,19 @@ class _AttemptScreenState extends State<AttemptScreen> {
appBar: AppBar(
title: Text('Attempt - ${_deck!.title}'),
actions: [
// Show "Jump to First Unanswered" if there are unanswered questions
if (_hasUnansweredQuestions)
IconButton(
onPressed: _jumpToFirstUnanswered,
icon: const Icon(Icons.skip_next),
tooltip: 'Jump to first unanswered question',
),
// Only show "Continue Later" button if there's progress
if (_hasAnyProgress)
TextButton.icon(
IconButton(
onPressed: _saveForLater,
icon: const Icon(Icons.pause),
label: const Text('Continue Later'),
tooltip: 'Continue Later',
),
],
),
@ -614,8 +728,14 @@ class _AttemptScreenState extends State<AttemptScreen> {
}
_answers[question.id] = selectedIndices.toList()..sort();
} else {
selectedIndex = answerIndex;
_answers[question.id] = selectedIndex;
// Toggle: if already selected, deselect; otherwise select
if (selectedIndex == answerIndex) {
selectedIndex = null;
_answers.remove(question.id);
} else {
selectedIndex = answerIndex;
_answers[question.id] = selectedIndex;
}
}
// Update current question state if viewing it
if (index == _currentQuestionIndex) {
@ -707,10 +827,11 @@ class _AttemptScreenState extends State<AttemptScreen> {
],
),
),
); // Close AnimatedBuilder
},
pageSnapping: true,
),
),
pageSnapping: true,
),
),
// Navigation Buttons
Container(
@ -753,31 +874,9 @@ class _AttemptScreenState extends State<AttemptScreen> {
const SizedBox(width: 12),
// Next/Complete Button
Expanded(
child: FilledButton.icon(
onPressed: _isLastQuestion
? (_hasAnswer ? _submitAnswer : null)
: _goToNextQuestion,
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 24),
minimumSize: const Size(0, 56),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
icon: Icon(
_isLastQuestion ? Icons.check_circle : Icons.arrow_forward,
size: 24,
color: _isLastQuestion ? Colors.green : null,
),
label: Text(
_isLastQuestion ? 'Complete' : 'Next',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
letterSpacing: 0.5,
),
),
),
child: _isLastQuestion
? _buildCompleteButton()
: _buildNextButton(),
),
],
),
@ -788,5 +887,65 @@ class _AttemptScreenState extends State<AttemptScreen> {
),
);
}
Widget _buildCompleteButton() {
final hasUnanswered = _hasUnansweredQuestions;
// Allow completing if current question is answered OR if there are unanswered questions (to show warning)
final canComplete = _hasAnswer || hasUnanswered;
return FilledButton.icon(
onPressed: canComplete
? (hasUnanswered ? _completeAttemptWithUnanswered : _submitAnswer)
: null,
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 24),
minimumSize: const Size(0, 56),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
icon: Icon(
hasUnanswered ? Icons.warning : Icons.check_circle,
size: 24,
color: hasUnanswered ? null : Colors.green,
),
label: const Text(
'Complete',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
letterSpacing: 0.5,
),
),
);
}
Widget _buildNextButton() {
final nextUnanswered = _nextUnansweredQuestionIndex;
final hasUnansweredAhead = nextUnanswered != null;
return FilledButton.icon(
onPressed: hasUnansweredAhead ? _goToNextUnanswered : _goToNextQuestion,
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 24),
minimumSize: const Size(0, 56),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
icon: const Icon(
Icons.arrow_forward,
size: 24,
),
label: const Text(
'Next',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
letterSpacing: 0.5,
),
),
);
}
}

@ -221,7 +221,7 @@ class _DeckListScreenState extends State<DeckListScreen> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Decky'),
title: const Text('omotomo'),
actions: [
IconButton(
icon: const Icon(Icons.add),

Loading…
Cancel
Save

Powered by TurnKey Linux.