mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-07-04 13:03:11 +02:00
fix: restore per-rep breaks with audio/vibration
Breaks belong after each rep (circle tap), not removed entirely. Restored break_banner, audioplayers, vibration, and break_end.mp3 asset. Break triggers on every circle tap except the last one; 3 min on success, 5 min on failure; sound + vibration fires when the countdown ends. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
da269c537a
commit
735f900bf8
BIN
stronglift_replacement/workout_app/assets/sounds/break_end.mp3
Normal file
BIN
stronglift_replacement/workout_app/assets/sounds/break_end.mp3
Normal file
Binary file not shown.
@ -1,18 +1,25 @@
|
||||
/// Active workout screen: warmup, back-button protection,
|
||||
/// Active workout screen: per-rep breaks, warmup, back-button protection,
|
||||
/// and crash-safe session persistence.
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:vibration/vibration.dart';
|
||||
import 'package:workout_app/models/exercise.dart';
|
||||
import 'package:workout_app/models/exercise_result.dart';
|
||||
import 'package:workout_app/models/set_result.dart';
|
||||
import 'package:workout_app/models/workout_session.dart';
|
||||
import 'package:workout_app/services/storage_service.dart';
|
||||
import 'package:workout_app/services/sync_service.dart';
|
||||
import 'package:workout_app/widgets/break_banner.dart';
|
||||
import 'package:workout_app/widgets/exercise_tile.dart';
|
||||
import 'package:workout_app/widgets/workout_summary_dialog.dart';
|
||||
|
||||
const _successBreakSecs = 180; // 3 min after successful rep
|
||||
const _failBreakSecs = 300; // 5 min after failed rep
|
||||
const _warmupBreakSecs = 180; // 3 min after warmup
|
||||
|
||||
class WorkoutScreen extends StatefulWidget {
|
||||
const WorkoutScreen({
|
||||
super.key,
|
||||
@ -39,6 +46,18 @@ class _WorkoutScreenState extends State<WorkoutScreen> {
|
||||
late Timer _elapsedTimer;
|
||||
Duration _elapsed = Duration.zero;
|
||||
|
||||
// Break state
|
||||
int _breakRemaining = 0;
|
||||
int _breakDurationSecs = 0;
|
||||
DateTime? _breakStartTime;
|
||||
Timer? _breakTimer;
|
||||
String _breakLabel = '';
|
||||
int _breakForExIdx = -1;
|
||||
int _breakForRepIdx = -1; // -1 = warmup break
|
||||
|
||||
bool get _inBreak => _breakRemaining > 0;
|
||||
|
||||
final _audio = AudioPlayer();
|
||||
final _sync = SyncService();
|
||||
bool _finished = false;
|
||||
|
||||
@ -78,11 +97,29 @@ class _WorkoutScreenState extends State<WorkoutScreen> {
|
||||
.map((row) => (row as List).cast<int>())
|
||||
.toList();
|
||||
_warmupTapped = (s['warmupTapped'] as List).cast<bool>();
|
||||
|
||||
final breakEndMs = s['breakEndMs'] as int? ?? 0;
|
||||
final breakDur = s['breakDurationSecs'] as int? ?? 0;
|
||||
if (breakEndMs > 0 && breakDur > 0) {
|
||||
final endTime = DateTime.fromMillisecondsSinceEpoch(breakEndMs);
|
||||
final remaining = endTime.difference(DateTime.now()).inSeconds;
|
||||
if (remaining > 0) {
|
||||
_breakForExIdx = s['breakForExIdx'] as int? ?? -1;
|
||||
_breakForRepIdx = s['breakForRepIdx'] as int? ?? -1;
|
||||
_breakLabel = s['breakLabel'] as String? ?? 'Rest';
|
||||
_breakDurationSecs = breakDur;
|
||||
_breakStartTime = endTime.subtract(Duration(seconds: breakDur));
|
||||
_breakRemaining = remaining;
|
||||
_breakTimer = Timer.periodic(const Duration(seconds: 1), _tickBreak);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_elapsedTimer.cancel();
|
||||
_breakTimer?.cancel();
|
||||
_audio.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -95,6 +132,15 @@ class _WorkoutScreenState extends State<WorkoutScreen> {
|
||||
'tapped': _tapped,
|
||||
'doneReps': _doneReps,
|
||||
'warmupTapped': _warmupTapped,
|
||||
'breakForExIdx': _breakForExIdx,
|
||||
'breakForRepIdx': _breakForRepIdx,
|
||||
'breakLabel': _breakLabel,
|
||||
'breakDurationSecs': _breakDurationSecs,
|
||||
'breakEndMs': _breakStartTime != null
|
||||
? _breakStartTime!
|
||||
.add(Duration(seconds: _breakDurationSecs))
|
||||
.millisecondsSinceEpoch
|
||||
: 0,
|
||||
});
|
||||
}
|
||||
|
||||
@ -108,33 +154,142 @@ class _WorkoutScreenState extends State<WorkoutScreen> {
|
||||
|
||||
bool get _allSetsCompleted => _tapped.every((row) => row.every((t) => t));
|
||||
|
||||
bool _isLastUntappedCircle(int exIdx, int repIdx) {
|
||||
int remaining = 0;
|
||||
for (int i = 0; i < widget.exercises.length; i++) {
|
||||
for (int s = 0; s < widget.exercises[i].sets; s++) {
|
||||
if (!_tapped[i][s]) remaining++;
|
||||
}
|
||||
}
|
||||
return remaining == 1;
|
||||
}
|
||||
|
||||
// ── Interaction ────────────────────────────────────────────────────────────
|
||||
|
||||
void _tapCircle(int exIdx, int setIdx) {
|
||||
void _tapCircle(int exIdx, int repIdx) {
|
||||
if (_finished) return;
|
||||
|
||||
final wasNotTapped = !_tapped[exIdx][repIdx];
|
||||
if (wasNotTapped && _inBreak) return;
|
||||
|
||||
setState(() {
|
||||
if (!_tapped[exIdx][setIdx]) {
|
||||
_tapped[exIdx][setIdx] = true;
|
||||
if (wasNotTapped) {
|
||||
_tapped[exIdx][repIdx] = true;
|
||||
} else {
|
||||
// Subsequent taps decrement reps (records actual reps done).
|
||||
_doneReps[exIdx][setIdx] =
|
||||
(_doneReps[exIdx][setIdx] - 1).clamp(0, 999);
|
||||
_doneReps[exIdx][repIdx] =
|
||||
(_doneReps[exIdx][repIdx] - 1).clamp(0, 999);
|
||||
_recomputeBreakIfNeeded(exIdx, repIdx);
|
||||
}
|
||||
});
|
||||
|
||||
if (wasNotTapped) {
|
||||
final isLast = _isLastUntappedCircle(exIdx, repIdx);
|
||||
if (!isLast) {
|
||||
final succeeded =
|
||||
_doneReps[exIdx][repIdx] >= widget.exercises[exIdx].reps;
|
||||
_startBreak(
|
||||
succeeded ? _successBreakSecs : _failBreakSecs,
|
||||
succeeded
|
||||
? 'Rest (3 min — well done!)'
|
||||
: 'Rest (5 min — keep going!)',
|
||||
exIdx,
|
||||
repIdx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_saveActiveSession();
|
||||
}
|
||||
|
||||
void _tapWarmup(int exIdx) {
|
||||
if (_finished || _warmupTapped[exIdx]) return;
|
||||
setState(() => _warmupTapped[exIdx] = true);
|
||||
if (!_inBreak) {
|
||||
_startBreak(_warmupBreakSecs, 'Warmup rest (3 min)', exIdx, -1);
|
||||
}
|
||||
_saveActiveSession();
|
||||
}
|
||||
|
||||
void _resetCircle(int exIdx, int setIdx) {
|
||||
void _resetCircle(int exIdx, int repIdx) {
|
||||
if (_finished) return;
|
||||
setState(() {
|
||||
_tapped[exIdx][setIdx] = false;
|
||||
_doneReps[exIdx][setIdx] = widget.exercises[exIdx].reps;
|
||||
_tapped[exIdx][repIdx] = false;
|
||||
_doneReps[exIdx][repIdx] = widget.exercises[exIdx].reps;
|
||||
});
|
||||
if (_breakForExIdx == exIdx && _breakForRepIdx == repIdx) {
|
||||
_cancelBreak();
|
||||
}
|
||||
_saveActiveSession();
|
||||
}
|
||||
|
||||
// ── Break management ───────────────────────────────────────────────────────
|
||||
|
||||
void _startBreak(int secs, String label, int exIdx, int repIdx) {
|
||||
_breakTimer?.cancel();
|
||||
setState(() {
|
||||
_breakDurationSecs = secs;
|
||||
_breakRemaining = secs;
|
||||
_breakLabel = label;
|
||||
_breakForExIdx = exIdx;
|
||||
_breakForRepIdx = repIdx;
|
||||
_breakStartTime = DateTime.now();
|
||||
});
|
||||
_breakTimer = Timer.periodic(const Duration(seconds: 1), _tickBreak);
|
||||
}
|
||||
|
||||
void _tickBreak(Timer t) {
|
||||
setState(() => _breakRemaining--);
|
||||
if (_breakRemaining <= 0) {
|
||||
t.cancel();
|
||||
_onBreakFinished();
|
||||
}
|
||||
}
|
||||
|
||||
void _cancelBreak() {
|
||||
_breakTimer?.cancel();
|
||||
setState(() {
|
||||
_breakRemaining = 0;
|
||||
_breakForExIdx = -1;
|
||||
_breakForRepIdx = -1;
|
||||
_breakStartTime = null;
|
||||
});
|
||||
}
|
||||
|
||||
void _skipBreak() {
|
||||
_cancelBreak();
|
||||
_saveActiveSession();
|
||||
}
|
||||
|
||||
/// If the user reduces reps on the rep that triggered the current break,
|
||||
/// switch from 3-min to 5-min (or vice versa).
|
||||
void _recomputeBreakIfNeeded(int exIdx, int repIdx) {
|
||||
if (!_inBreak) return;
|
||||
if (_breakForExIdx != exIdx || _breakForRepIdx != repIdx) return;
|
||||
if (_breakForRepIdx == -1) return; // warmup break, never recompute
|
||||
|
||||
final succeeded =
|
||||
_doneReps[exIdx][repIdx] >= widget.exercises[exIdx].reps;
|
||||
final newDuration = succeeded ? _successBreakSecs : _failBreakSecs;
|
||||
if (newDuration == _breakDurationSecs) return;
|
||||
|
||||
final elapsed = DateTime.now().difference(_breakStartTime!).inSeconds;
|
||||
final newRemaining = (newDuration - elapsed).clamp(0, newDuration);
|
||||
|
||||
_breakDurationSecs = newDuration;
|
||||
_breakRemaining = newRemaining;
|
||||
_breakLabel =
|
||||
succeeded ? 'Rest (3 min — well done!)' : 'Rest (5 min — keep going!)';
|
||||
}
|
||||
|
||||
Future<void> _onBreakFinished() async {
|
||||
await _audio.play(AssetSource('sounds/break_end.mp3')).catchError((_) {});
|
||||
final hasVibrator = await Vibration.hasVibrator() == true;
|
||||
if (hasVibrator) Vibration.vibrate(duration: 800);
|
||||
setState(() {
|
||||
_breakForExIdx = -1;
|
||||
_breakForRepIdx = -1;
|
||||
_breakStartTime = null;
|
||||
});
|
||||
_saveActiveSession();
|
||||
}
|
||||
@ -153,12 +308,15 @@ class _WorkoutScreenState extends State<WorkoutScreen> {
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Cancel', style: TextStyle(color: Colors.white70)),
|
||||
child:
|
||||
const Text('Cancel', style: TextStyle(color: Colors.white70)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child:
|
||||
const Text('Finish', style: TextStyle(color: Colors.greenAccent)),
|
||||
child: const Text(
|
||||
'Finish',
|
||||
style: TextStyle(color: Colors.greenAccent),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -201,6 +359,7 @@ class _WorkoutScreenState extends State<WorkoutScreen> {
|
||||
|
||||
Future<void> _finishWorkout() async {
|
||||
_elapsedTimer.cancel();
|
||||
_breakTimer?.cancel();
|
||||
setState(() => _finished = true);
|
||||
|
||||
final endTime = DateTime.now();
|
||||
@ -301,19 +460,31 @@ class _WorkoutScreenState extends State<WorkoutScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ListView.separated(
|
||||
padding: const EdgeInsets.all(12),
|
||||
itemCount: widget.exercises.length,
|
||||
separatorBuilder: (_, _) => const SizedBox(height: 8),
|
||||
itemBuilder: (_, i) => ExerciseTile(
|
||||
exercise: widget.exercises[i],
|
||||
tapped: _tapped[i],
|
||||
doneReps: _doneReps[i],
|
||||
warmupTapped: _warmupTapped[i],
|
||||
onTapCircle: (s) => _tapCircle(i, s),
|
||||
onLongPressCircle: (s) => _resetCircle(i, s),
|
||||
onTapWarmup: () => _tapWarmup(i),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
if (_inBreak)
|
||||
BreakBanner(
|
||||
breakRemaining: _breakRemaining,
|
||||
breakLabel: _breakLabel,
|
||||
onSkip: _skipBreak,
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.all(12),
|
||||
itemCount: widget.exercises.length,
|
||||
separatorBuilder: (_, _) => const SizedBox(height: 8),
|
||||
itemBuilder: (_, i) => ExerciseTile(
|
||||
exercise: widget.exercises[i],
|
||||
tapped: _tapped[i],
|
||||
doneReps: _doneReps[i],
|
||||
warmupTapped: _warmupTapped[i],
|
||||
onTapCircle: (s) => _tapCircle(i, s),
|
||||
onLongPressCircle: (s) => _resetCircle(i, s),
|
||||
onTapWarmup: () => _tapWarmup(i),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -0,0 +1,63 @@
|
||||
/// Countdown banner displayed at the top of the workout screen during a rest.
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BreakBanner extends StatelessWidget {
|
||||
const BreakBanner({
|
||||
super.key,
|
||||
required this.breakRemaining,
|
||||
required this.breakLabel,
|
||||
required this.onSkip,
|
||||
});
|
||||
|
||||
final int breakRemaining;
|
||||
final String breakLabel;
|
||||
final VoidCallback onSkip;
|
||||
|
||||
String _fmt(int secs) {
|
||||
final m = (secs ~/ 60).toString().padLeft(2, '0');
|
||||
final s = (secs % 60).toString().padLeft(2, '0');
|
||||
return '$m:$s';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Colors.indigo.shade900,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
breakLabel,
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 12),
|
||||
),
|
||||
Text(
|
||||
_fmt(breakRemaining),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: onSkip,
|
||||
child: const Text(
|
||||
'Skip',
|
||||
style: TextStyle(color: Colors.white70),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -17,6 +17,62 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.1"
|
||||
audioplayers:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: audioplayers
|
||||
sha256: "1d0c1b1f2095e59080e2d5046639096417a86687d89778da41b0c9a06d683dfd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.7.0"
|
||||
audioplayers_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_android
|
||||
sha256: "60a6728277228413a85755bd3ffd6fab98f6555608923813ce383b190a360605"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.2.1"
|
||||
audioplayers_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_darwin
|
||||
sha256: c994b3bb3a921e4904ac40e013fbc94488e824fd7c1de6326f549943b0b44a91
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.4.0"
|
||||
audioplayers_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_linux
|
||||
sha256: f75bce1ce864170ef5e6a2c6a61cd3339e1a17ce11e99a25bae4474ea491d001
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.1"
|
||||
audioplayers_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_platform_interface
|
||||
sha256: "0e2f6a919ab56d0fec272e801abc07b26ae7f31980f912f24af4748763e5a656"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.1.1"
|
||||
audioplayers_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_web
|
||||
sha256: "24a6f258062bd7da8cb2157e83fccb9816a08dd306cbaaa24f9813d071470545"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.2.1"
|
||||
audioplayers_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_windows
|
||||
sha256: "95f875a96c88c3dbbcb608d4f8288e300b0113d256a81d0b3197fcc18f0dc91a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.3.1"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -65,6 +121,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
device_info_plus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: device_info_plus
|
||||
sha256: "6a642e1daa10190af89ba6cb6386c0df7d071a3592080bfe1e44faa63ae1df65"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "13.1.0"
|
||||
device_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: device_info_plus_platform_interface
|
||||
sha256: "04b173a92e2d9161dfead145667037c8d834db725ce2e7b942bfe18fd2f45a46"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.0"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -81,6 +153,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
ffi_leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi_leak_tracker
|
||||
sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.2"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -89,6 +169,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@ -120,6 +208,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http
|
||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.0"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
jni:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -517,6 +621,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: uuid
|
||||
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.3"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -525,6 +637,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
vibration:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: vibration
|
||||
sha256: "9bb06614c69260f8bd11c80fe01ed7988905cf00e3417d656c2647e41f261d87"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.8"
|
||||
vibration_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vibration_platform_interface
|
||||
sha256: "258c273268f8aa40c88d29741137c536874a738779b92ddb8aa32ed093721ec5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.2"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -541,6 +669,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: ba6f4bba816c8d7e3c1580e170f3786d216951cc6b94babc3b814c08d2cb2738
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
win32_registry:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32_registry
|
||||
sha256: "73b1d78920a9d6e03f8b4e43e612b87bf3152a0e5c5e5150267762b7c4116904"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@ -14,6 +14,8 @@ dependencies:
|
||||
sqflite: ^2.4.2
|
||||
path_provider: ^2.1.5
|
||||
shared_preferences: ^2.5.3
|
||||
audioplayers: ^6.4.0
|
||||
vibration: ^3.1.0
|
||||
permission_handler: ^12.0.0
|
||||
|
||||
dev_dependencies:
|
||||
@ -23,3 +25,5 @@ dev_dependencies:
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
assets:
|
||||
- assets/sounds/break_end.mp3
|
||||
|
||||
Loading…
Reference in New Issue
Block a user