import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_sound/flutter_sound.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:sendtrain/database/database.dart'; import 'package:sendtrain/models/action_model.dart'; import 'package:vibration/vibration.dart'; class ActionTimer with ChangeNotifier { ActionModel? actionModel; double _progress = 0; int _currentTime = 0; final List _scrollControllers = []; final FlutterSoundPlayer _mPlayer = FlutterSoundPlayer(); ActionTimer(); Map get state => actionModel?.state ?? _stateConstructor(); ActionStatus get status => actionModel?.status ?? ActionStatus.pending; bool get started => status == ActionStatus.started; bool get paused => status == ActionStatus.paused; bool get pending => status == ActionStatus.pending; bool get complete => status == ActionStatus.complete; bool get available => paused | pending; List get sets => actionModel!.sets; List get items => actionModel!.items; Set get currentSet => sets[state['currentSet']]; Reps get currentRep => currentSet.reps[state['currentRep']]; Item get currentAction => allActions[state['currentAction']]; int get currentTime => _currentTime; dynamic get currentValue => currentAction.valueType == RepType.time ? currentTime : currentAction.value; List get allActions => actionModel?.allItems ?? []; String get repType => actionModel!.action.repType == RepType.time ? 'Seconds' : 'Reps'; int? get repLength => currentRep.value; int? get repCount => currentRep.count; dynamic get repValue => actionModel!.action.repType == RepType.time ? repLength : repCount; double get progress => _progress; int get totalTime => actionModel!.totalTime; Timer? _periodicTimer; Map _stateConstructor() { return { 'currentSet': 0, 'currentRep': 0, 'currentTime': 0, 'currentAction': 0 }; } void setup(ActionModel actionModel, ItemScrollController scrollController, [bool resetOnLoad = true]) async { _scrollControllers.clear(); _scrollControllers.add(scrollController); if (resetOnLoad) { if (this.actionModel == actionModel) { reset(); _scrollControllers.add(scrollController); } this.actionModel = actionModel; setAction(currentAction.id); } } Future pause() async => await actionModel?.updateStatus(ActionStatus.paused).whenComplete(() { _periodicTimer?.cancel(); notifyListeners(); // _mPlayer.stopPlayer(); // Be careful : you must `close` the audio session when you have finished with it. }); Future start() async { await actionModel!.updateStatus(ActionStatus.started); await _mPlayer.openPlayer(); Uint8List? countTone; Uint8List? finishTone; await rootBundle .load('assets/audio/count_tone.mp3') .then((data) => countTone = data.buffer.asUint8List()); await rootBundle .load('assets/audio/count_finish.mp3') .then((data) => finishTone = data.buffer.asUint8List()); // start timer if (_periodicTimer == null || _periodicTimer!.isActive == false) { _periodicTimer = Timer.periodic(const Duration(seconds: 1), (Timer timer) async { switch (currentAction.valueType) { case RepType.count: break; case RepType.time: _currentTime--; if (_currentTime <= 3 && _currentTime != 0) { await _mPlayer .startPlayer(fromDataBuffer: countTone, codec: Codec.mp3) .then((duration) async { if (await Vibration.hasVibrator()) { Vibration.vibrate(duration: 250); } }); } if (_currentTime == 0) { // move to next action await _mPlayer .startPlayer(fromDataBuffer: finishTone, codec: Codec.mp3) .then((duration) async { if (await Vibration.hasVibrator()) { Vibration.vibrate(duration: 250); } }); await setAction(state['currentAction'] + 1); } await updateProgress(); notifyListeners(); } }); } notifyListeners(); } Future close() async => await actionModel! .updateStatus(ActionStatus.complete) .whenComplete(() async { _periodicTimer!.cancel(); _mPlayer.closePlayer(); notifyListeners(); }); Future reset() async { await actionModel?.updateStatus(ActionStatus.pending); await actionModel?.updateState(json.encode(_stateConstructor())); _periodicTimer?.cancel(); _progress = 0; _scrollControllers.clear(); _mPlayer.closePlayer(); notifyListeners(); } Future clear() async { await reset(); } double timeUsed() { Iterable usedItems = allActions.getRange(0, state['currentAction']); return usedItems.fold(0.0, (p, c) => p + c.value!); } double totalComplete() { Iterable usedItems = allActions.getRange(0, state['currentAction']); return usedItems.length / allActions.length; } updateProgress() { double repUsed = (currentAction.value - currentTime) / currentAction.value; _progress = totalComplete() + ((repUsed < 0 ? 0 : repUsed) / allActions.length); notifyListeners(); } setAction(int actionNum, [bool isManual = false]) async { if (actionNum < allActions.length) { Item item = allActions[actionNum]; Map newState = state; newState['currentAction'] = actionNum; newState['currentSet'] = item.parentId; newState['currentRep'] = item.id; newState['currentTime'] = _currentTime = item.value!; await actionModel! .updateState(json.encode(newState)) .whenComplete(() async { // if manual select, pause next action if (isManual) { await pause(); await updateProgress(); } int index = currentAction.parentId != null ? currentAction.parentId! : currentAction.id; if (_scrollControllers.isNotEmpty) { for (int i = 0; i < _scrollControllers.length; i++) { ItemScrollController sc = _scrollControllers[i]; sc.scrollTo( index: index, duration: Duration(milliseconds: 500), curve: Curves.easeInOutCubic); } } // _scrollController?.scrollTo( // index: index, // duration: Duration(milliseconds: 500), // curve: Curves.easeInOutCubic); }); } else { await actionModel?.updateStatus(ActionStatus.complete).whenComplete(() { _periodicTimer?.cancel(); notifyListeners(); }); } notifyListeners(); } }