Add routines module and full goal management
Routines: workout, meditation and must-do routines with timed, rep and tick steps, sets and rest. A library screen, a builder with templates and drag-to-reorder steps, and a full-screen player that auto-advances timed phases, counts backgrounding and logs broken chains as partial. Goals: a goal editor (deadline, colour, category, default class, archive), routines linked to goals, and a goal field on tasks and routines. Tasks added from inside a goal are now actually attached to it via parentId. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KhQh614DMeYoQEH9SoMRUw
This commit is contained in:
@@ -1,3 +1,9 @@
|
||||
import '../../internal/application/CommitmentClass.dart';
|
||||
import '../../internal/application/RoutineKind.dart';
|
||||
import '../../internal/application/StepMode.dart';
|
||||
import '../../../utils/ObjectConvertors.dart';
|
||||
import 'RoutineStep.dart';
|
||||
|
||||
/// An ordered sequence where the chain, not the step, is the unit of
|
||||
/// completion. Breaking mid-way logs partial.
|
||||
class RoutineChain {
|
||||
@@ -5,6 +11,24 @@ class RoutineChain {
|
||||
|
||||
String title;
|
||||
|
||||
/// The goal this routine serves — "Workout" holds "Full body strength".
|
||||
/// Null for a routine that stands on its own.
|
||||
String? goalId;
|
||||
|
||||
RoutineKind kind;
|
||||
|
||||
/// Weight class, exactly as for a commitment — a non-negotiable morning
|
||||
/// routine carries non-negotiable debt when it breaks.
|
||||
CommitmentClass commitmentClass;
|
||||
|
||||
/// 1..7, Monday first. Empty means unscheduled — run on demand only.
|
||||
List<int> days;
|
||||
|
||||
/// Minutes after midnight the routine is due to start; -1 when unset.
|
||||
int startMinute;
|
||||
|
||||
String note;
|
||||
|
||||
List<RoutineStep> steps;
|
||||
|
||||
/// Index of the step reached when the chain last broke; -1 when clean.
|
||||
@@ -13,19 +37,30 @@ class RoutineChain {
|
||||
RoutineChain({
|
||||
this.id,
|
||||
this.title = "",
|
||||
this.goalId,
|
||||
this.kind = RoutineKind.Workout,
|
||||
this.commitmentClass = CommitmentClass.Standard,
|
||||
List<int>? days,
|
||||
this.startMinute = -1,
|
||||
this.note = "",
|
||||
List<RoutineStep>? steps,
|
||||
this.lastBreakIndex = -1,
|
||||
}) : steps = steps ?? <RoutineStep>[];
|
||||
}) : days = days ?? <int>[],
|
||||
steps = steps ?? <RoutineStep>[];
|
||||
|
||||
factory RoutineChain.fromJson(Map<String, dynamic> json) {
|
||||
return RoutineChain(
|
||||
id: json['id'],
|
||||
title: json['title'] ?? "",
|
||||
steps: json['steps'] == null
|
||||
? <RoutineStep>[]
|
||||
: (json['steps'] as List)
|
||||
.map((item) => RoutineStep.fromJson(item))
|
||||
.toList(),
|
||||
goalId: json['goalId'],
|
||||
kind: getRoutineKind(json['kind']),
|
||||
commitmentClass: getCommitmentClass(json['commitmentClass']),
|
||||
days: json['days'] == null
|
||||
? <int>[]
|
||||
: (json['days'] as List).map((item) => item as int).toList(),
|
||||
startMinute: json['startMinute'] ?? -1,
|
||||
note: json['note'] ?? "",
|
||||
steps: getRoutineStepList(json['steps']),
|
||||
lastBreakIndex: json['lastBreakIndex'] ?? -1,
|
||||
);
|
||||
}
|
||||
@@ -34,6 +69,12 @@ class RoutineChain {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['id'] = id;
|
||||
data['title'] = title;
|
||||
data['goalId'] = goalId;
|
||||
data['kind'] = kind.name;
|
||||
data['commitmentClass'] = commitmentClass.name;
|
||||
data['days'] = days;
|
||||
data['startMinute'] = startMinute;
|
||||
data['note'] = note;
|
||||
data['steps'] = steps.map((item) => item.toJson()).toList();
|
||||
data['lastBreakIndex'] = lastBreakIndex;
|
||||
return data;
|
||||
@@ -54,44 +95,40 @@ class RoutineChain {
|
||||
bool get complete {
|
||||
return steps.isNotEmpty && completedSteps == steps.length;
|
||||
}
|
||||
}
|
||||
|
||||
class RoutineStep {
|
||||
String? id;
|
||||
|
||||
String title;
|
||||
|
||||
int order;
|
||||
|
||||
int timerSeconds;
|
||||
|
||||
bool completed;
|
||||
|
||||
RoutineStep({
|
||||
this.id,
|
||||
this.title = "",
|
||||
this.order = 0,
|
||||
this.timerSeconds = 0,
|
||||
this.completed = false,
|
||||
});
|
||||
|
||||
factory RoutineStep.fromJson(Map<String, dynamic> json) {
|
||||
return RoutineStep(
|
||||
id: json['id'],
|
||||
title: json['title'] ?? "",
|
||||
order: json['order'] ?? 0,
|
||||
timerSeconds: json['timerSeconds'] ?? 0,
|
||||
completed: json['completed'] ?? false,
|
||||
);
|
||||
bool get scheduled {
|
||||
return days.isNotEmpty;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['id'] = id;
|
||||
data['title'] = title;
|
||||
data['order'] = order;
|
||||
data['timerSeconds'] = timerSeconds;
|
||||
data['completed'] = completed;
|
||||
return data;
|
||||
bool scheduledOn(DateTime day) {
|
||||
return days.contains(day.weekday);
|
||||
}
|
||||
|
||||
/// Every set of every step, for the "12 sets" figure.
|
||||
int get totalSets {
|
||||
int total = 0;
|
||||
for (RoutineStep step in steps) {
|
||||
total = total + step.sets;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/// Planned seconds, rest included. Rep and tick steps count their estimate.
|
||||
int get totalSeconds {
|
||||
int total = 0;
|
||||
for (RoutineStep step in steps) {
|
||||
total = total + step.plannedSeconds;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
int get timedSteps {
|
||||
return steps.where((step) => step.mode == StepMode.Timed).length;
|
||||
}
|
||||
|
||||
/// A deep copy for the builder, so cancelling an edit leaves the library
|
||||
/// untouched.
|
||||
RoutineChain copy() {
|
||||
return RoutineChain.fromJson(toJson());
|
||||
}
|
||||
}
|
||||
|
||||
75
frontend/lib/Grounded/about/external/data/RoutineStep.dart
vendored
Normal file
75
frontend/lib/Grounded/about/external/data/RoutineStep.dart
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
import '../../internal/application/StepMode.dart';
|
||||
|
||||
class RoutineStep {
|
||||
String? id;
|
||||
|
||||
String title;
|
||||
|
||||
int order;
|
||||
|
||||
StepMode mode;
|
||||
|
||||
/// Work seconds per set for timed steps; the estimate per set otherwise.
|
||||
int timerSeconds;
|
||||
|
||||
int sets;
|
||||
|
||||
/// Target per set for rep steps; 0 otherwise.
|
||||
int reps;
|
||||
|
||||
/// Rest between sets, and after the last set before the next step.
|
||||
int restSeconds;
|
||||
|
||||
/// One line shown under the clock — a form cue, a breathing pattern.
|
||||
String cue;
|
||||
|
||||
bool completed;
|
||||
|
||||
RoutineStep({
|
||||
this.id,
|
||||
this.title = "",
|
||||
this.order = 0,
|
||||
this.mode = StepMode.Timed,
|
||||
this.timerSeconds = 0,
|
||||
this.sets = 1,
|
||||
this.reps = 0,
|
||||
this.restSeconds = 0,
|
||||
this.cue = "",
|
||||
this.completed = false,
|
||||
});
|
||||
|
||||
factory RoutineStep.fromJson(Map<String, dynamic> json) {
|
||||
return RoutineStep(
|
||||
id: json['id'],
|
||||
title: json['title'] ?? "",
|
||||
order: json['order'] ?? 0,
|
||||
mode: getStepMode(json['mode']),
|
||||
timerSeconds: json['timerSeconds'] ?? 0,
|
||||
sets: json['sets'] ?? 1,
|
||||
reps: json['reps'] ?? 0,
|
||||
restSeconds: json['restSeconds'] ?? 0,
|
||||
cue: json['cue'] ?? "",
|
||||
completed: json['completed'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['id'] = id;
|
||||
data['title'] = title;
|
||||
data['order'] = order;
|
||||
data['mode'] = mode.name;
|
||||
data['timerSeconds'] = timerSeconds;
|
||||
data['sets'] = sets;
|
||||
data['reps'] = reps;
|
||||
data['restSeconds'] = restSeconds;
|
||||
data['cue'] = cue;
|
||||
data['completed'] = completed;
|
||||
return data;
|
||||
}
|
||||
|
||||
int get plannedSeconds {
|
||||
final int safeSets = sets < 1 ? 1 : sets;
|
||||
return timerSeconds * safeSets + restSeconds * safeSets;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,9 @@
|
||||
class CommitmentRequest {
|
||||
String? id;
|
||||
|
||||
/// The goal this task belongs to; null for a loose task.
|
||||
String? parentId;
|
||||
|
||||
String type;
|
||||
|
||||
String commitmentClass;
|
||||
@@ -26,6 +29,7 @@ class CommitmentRequest {
|
||||
|
||||
CommitmentRequest({
|
||||
this.id,
|
||||
this.parentId,
|
||||
this.type = "TASK",
|
||||
this.commitmentClass = "Standard",
|
||||
this.title = "",
|
||||
@@ -42,6 +46,7 @@ class CommitmentRequest {
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['id'] = id;
|
||||
data['parentId'] = parentId;
|
||||
data['type'] = type;
|
||||
data['commitmentClass'] = commitmentClass;
|
||||
data['title'] = title;
|
||||
|
||||
45
frontend/lib/Grounded/about/external/initial/RoutineLogRequest.dart
vendored
Normal file
45
frontend/lib/Grounded/about/external/initial/RoutineLogRequest.dart
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
/// One run of a routine. Sent whether the chain held or broke — a broken
|
||||
/// chain is logged as partial, never silently dropped.
|
||||
class RoutineLogRequest {
|
||||
String routineId;
|
||||
|
||||
String startedAt;
|
||||
|
||||
/// Foreground seconds only. Backgrounding pauses the run.
|
||||
int elapsedSeconds;
|
||||
|
||||
int completedSteps;
|
||||
|
||||
int totalSteps;
|
||||
|
||||
/// Step index the chain broke at; -1 when it held.
|
||||
int breakIndex;
|
||||
|
||||
int backgroundedCount;
|
||||
|
||||
int skippedCount;
|
||||
|
||||
RoutineLogRequest({
|
||||
this.routineId = "",
|
||||
this.startedAt = "",
|
||||
this.elapsedSeconds = 0,
|
||||
this.completedSteps = 0,
|
||||
this.totalSteps = 0,
|
||||
this.breakIndex = -1,
|
||||
this.backgroundedCount = 0,
|
||||
this.skippedCount = 0,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['routineId'] = routineId;
|
||||
data['startedAt'] = startedAt;
|
||||
data['elapsedSeconds'] = elapsedSeconds;
|
||||
data['completedSteps'] = completedSteps;
|
||||
data['totalSteps'] = totalSteps;
|
||||
data['breakIndex'] = breakIndex;
|
||||
data['backgroundedCount'] = backgroundedCount;
|
||||
data['skippedCount'] = skippedCount;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/// What a routine is for. The kind decides the runner's stage — a barbell
|
||||
/// clock for work, a breathing circle for stillness, a checklist for the
|
||||
/// things that simply have to get done.
|
||||
enum RoutineKind {
|
||||
/// Sets, reps and rest. Timed work auto-advances into rest.
|
||||
Workout,
|
||||
|
||||
/// Long timed holds with a breathing cue. No reps, rarely any rest.
|
||||
Meditation,
|
||||
|
||||
/// An ordered list of tasks that have to be done, in order, every time.
|
||||
MustDo,
|
||||
}
|
||||
|
||||
String kindLabel(RoutineKind value) {
|
||||
switch (value) {
|
||||
case RoutineKind.Workout:
|
||||
return "Workout";
|
||||
case RoutineKind.Meditation:
|
||||
return "Meditation";
|
||||
case RoutineKind.MustDo:
|
||||
return "Must-do";
|
||||
}
|
||||
}
|
||||
|
||||
String kindDescription(RoutineKind value) {
|
||||
switch (value) {
|
||||
case RoutineKind.Workout:
|
||||
return "Sets, reps and timed rest.";
|
||||
case RoutineKind.Meditation:
|
||||
return "Timed sits with a breathing cue.";
|
||||
case RoutineKind.MustDo:
|
||||
return "Tasks, in order, every time.";
|
||||
}
|
||||
}
|
||||
|
||||
RoutineKind getRoutineKind(String? name) {
|
||||
for (RoutineKind value in RoutineKind.values) {
|
||||
if (value.name == name) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return RoutineKind.Workout;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import '../../external/data/RoutineStep.dart';
|
||||
import 'StepMode.dart';
|
||||
|
||||
/// One beat of a running routine — a single work set or the rest after it.
|
||||
/// The runner walks a flat list of these, so "set 2 of 4" and "rest" are the
|
||||
/// same kind of thing to the clock.
|
||||
class RoutinePhase {
|
||||
/// Index of the step this phase belongs to.
|
||||
final int stepIndex;
|
||||
|
||||
final RoutineStep step;
|
||||
|
||||
/// 1-based set number within the step.
|
||||
final int set;
|
||||
|
||||
final bool rest;
|
||||
|
||||
RoutinePhase({
|
||||
required this.stepIndex,
|
||||
required this.step,
|
||||
this.set = 1,
|
||||
this.rest = false,
|
||||
});
|
||||
|
||||
/// Seconds the clock runs for. Zero means the phase waits for a tap.
|
||||
int get seconds {
|
||||
if (rest) {
|
||||
return step.restSeconds;
|
||||
}
|
||||
return step.mode == StepMode.Timed ? step.timerSeconds : 0;
|
||||
}
|
||||
|
||||
bool get countsDown {
|
||||
return seconds > 0;
|
||||
}
|
||||
|
||||
bool get lastSet {
|
||||
return set >= step.sets;
|
||||
}
|
||||
}
|
||||
212
frontend/lib/Grounded/about/internal/application/RoutineRun.dart
Normal file
212
frontend/lib/Grounded/about/internal/application/RoutineRun.dart
Normal file
@@ -0,0 +1,212 @@
|
||||
import '../../external/data/RoutineChain.dart';
|
||||
import '../../external/data/RoutineStep.dart';
|
||||
import 'RoutinePhase.dart';
|
||||
|
||||
/// A routine being run. Like [LiveSession], every clock is derived from
|
||||
/// wall-clock rather than counted by a ticker, so a screen-off period cannot
|
||||
/// lose or invent time.
|
||||
class RoutineRun {
|
||||
final RoutineChain chain;
|
||||
|
||||
final List<RoutinePhase> phases;
|
||||
|
||||
int phaseIndex = 0;
|
||||
|
||||
final DateTime startedAt;
|
||||
|
||||
/// Banked foreground seconds for the whole run.
|
||||
int _runSeconds = 0;
|
||||
|
||||
/// Banked foreground seconds for the current phase.
|
||||
int _phaseSeconds = 0;
|
||||
|
||||
/// Start of the active stretch; null while paused.
|
||||
DateTime? resumedAt;
|
||||
|
||||
/// Times the app was left mid-run. Shown afterwards, never hidden.
|
||||
int backgroundedCount = 0;
|
||||
|
||||
/// Phases skipped forward without being done.
|
||||
int skippedCount = 0;
|
||||
|
||||
RoutineRun(this.chain)
|
||||
: phases = buildPhases(chain),
|
||||
startedAt = DateTime.now();
|
||||
|
||||
/// Each set becomes a work phase; rest follows every set that has rest,
|
||||
/// except after the final set of the final step.
|
||||
static List<RoutinePhase> buildPhases(RoutineChain chain) {
|
||||
final List<RoutinePhase> result = <RoutinePhase>[];
|
||||
|
||||
for (int index = 0; index < chain.steps.length; index++) {
|
||||
final RoutineStep step = chain.steps[index];
|
||||
final int sets = step.sets < 1 ? 1 : step.sets;
|
||||
|
||||
for (int set = 1; set <= sets; set++) {
|
||||
result.add(RoutinePhase(stepIndex: index, step: step, set: set));
|
||||
|
||||
final bool finalBeat = index == chain.steps.length - 1 && set == sets;
|
||||
if (step.restSeconds > 0 && !finalBeat) {
|
||||
result.add(
|
||||
RoutinePhase(stepIndex: index, step: step, set: set, rest: true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
RoutinePhase get phase {
|
||||
return phases[phaseIndex];
|
||||
}
|
||||
|
||||
bool get running {
|
||||
return resumedAt != null;
|
||||
}
|
||||
|
||||
bool get onLastPhase {
|
||||
return phaseIndex >= phases.length - 1;
|
||||
}
|
||||
|
||||
int _live(DateTime? now) {
|
||||
if (resumedAt == null) {
|
||||
return 0;
|
||||
}
|
||||
return (now ?? DateTime.now()).difference(resumedAt!).inSeconds;
|
||||
}
|
||||
|
||||
int elapsedSeconds({DateTime? now}) {
|
||||
return _runSeconds + _live(now);
|
||||
}
|
||||
|
||||
int phaseElapsed({DateTime? now}) {
|
||||
return _phaseSeconds + _live(now);
|
||||
}
|
||||
|
||||
int phaseRemaining({DateTime? now}) {
|
||||
final int remaining = phase.seconds - phaseElapsed(now: now);
|
||||
return remaining > 0 ? remaining : 0;
|
||||
}
|
||||
|
||||
/// 0..1 through the current phase; 0 for phases that wait for a tap.
|
||||
double phaseProgress({DateTime? now}) {
|
||||
if (!phase.countsDown) {
|
||||
return 0;
|
||||
}
|
||||
final double value = phaseElapsed(now: now) / phase.seconds;
|
||||
return value > 1 ? 1 : value;
|
||||
}
|
||||
|
||||
bool phaseFinished({DateTime? now}) {
|
||||
return phase.countsDown && phaseElapsed(now: now) >= phase.seconds;
|
||||
}
|
||||
|
||||
/// Seconds left in the whole run, by plan — elapsed work in this phase
|
||||
/// counts, phases still ahead count at their planned length.
|
||||
int plannedRemaining({DateTime? now}) {
|
||||
int total = phase.countsDown ? phaseRemaining(now: now) : 0;
|
||||
for (int index = phaseIndex + 1; index < phases.length; index++) {
|
||||
total = total + phases[index].seconds;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
void pause({DateTime? now}) {
|
||||
if (resumedAt == null) {
|
||||
return;
|
||||
}
|
||||
final int live = _live(now);
|
||||
_runSeconds = _runSeconds + live;
|
||||
_phaseSeconds = _phaseSeconds + live;
|
||||
resumedAt = null;
|
||||
}
|
||||
|
||||
void resume({DateTime? now}) {
|
||||
if (resumedAt != null) {
|
||||
return;
|
||||
}
|
||||
resumedAt = now ?? DateTime.now();
|
||||
}
|
||||
|
||||
/// Bank the current phase and start the next. Returns false at the end.
|
||||
/// A step is marked complete when its last set's work phase is done.
|
||||
bool advance({bool skipped = false, DateTime? now}) {
|
||||
final DateTime moment = now ?? DateTime.now();
|
||||
final bool wasRunning = running;
|
||||
|
||||
if (!phase.rest && phase.lastSet && !skipped) {
|
||||
phase.step.completed = true;
|
||||
}
|
||||
if (skipped && !phase.rest) {
|
||||
skippedCount = skippedCount + 1;
|
||||
}
|
||||
|
||||
pause(now: moment);
|
||||
_phaseSeconds = 0;
|
||||
|
||||
if (onLastPhase) {
|
||||
return false;
|
||||
}
|
||||
|
||||
phaseIndex = phaseIndex + 1;
|
||||
if (wasRunning) {
|
||||
resume(now: moment);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Back to the start of the previous work phase. Rest is never a target.
|
||||
void back({DateTime? now}) {
|
||||
final DateTime moment = now ?? DateTime.now();
|
||||
final bool wasRunning = running;
|
||||
|
||||
pause(now: moment);
|
||||
_phaseSeconds = 0;
|
||||
|
||||
int target = phaseIndex - 1;
|
||||
while (target > 0 && phases[target].rest) {
|
||||
target = target - 1;
|
||||
}
|
||||
phaseIndex = target < 0 ? 0 : target;
|
||||
phases[phaseIndex].step.completed = false;
|
||||
|
||||
if (wasRunning) {
|
||||
resume(now: moment);
|
||||
}
|
||||
}
|
||||
|
||||
/// Jump to the first set of [stepIndex].
|
||||
void jumpTo(int stepIndex, {DateTime? now}) {
|
||||
final int target =
|
||||
phases.indexWhere((item) => item.stepIndex == stepIndex && !item.rest);
|
||||
if (target < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
final DateTime moment = now ?? DateTime.now();
|
||||
final bool wasRunning = running;
|
||||
|
||||
pause(now: moment);
|
||||
_phaseSeconds = 0;
|
||||
phaseIndex = target;
|
||||
|
||||
if (wasRunning) {
|
||||
resume(now: moment);
|
||||
}
|
||||
}
|
||||
|
||||
/// The step index the chain broke at, for a partial log; -1 when clean.
|
||||
int get breakIndex {
|
||||
return chain.complete ? -1 : phase.stepIndex;
|
||||
}
|
||||
|
||||
/// The next work phase after this one, for the "up next" line.
|
||||
RoutinePhase? get nextWork {
|
||||
for (int index = phaseIndex + 1; index < phases.length; index++) {
|
||||
if (!phases[index].rest) {
|
||||
return phases[index];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/// How a single routine step is satisfied.
|
||||
enum StepMode {
|
||||
/// A clock runs down and the step advances on its own.
|
||||
Timed,
|
||||
|
||||
/// A rep target per set; the set ends when you say it does.
|
||||
Reps,
|
||||
|
||||
/// A task. Ticked, not timed — the clock still records how long it took.
|
||||
Check,
|
||||
}
|
||||
|
||||
String stepModeLabel(StepMode value) {
|
||||
switch (value) {
|
||||
case StepMode.Timed:
|
||||
return "Timed";
|
||||
case StepMode.Reps:
|
||||
return "Reps";
|
||||
case StepMode.Check:
|
||||
return "Tick";
|
||||
}
|
||||
}
|
||||
|
||||
StepMode getStepMode(String? name) {
|
||||
for (StepMode value in StepMode.values) {
|
||||
if (value.name == name) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return StepMode.Timed;
|
||||
}
|
||||
Reference in New Issue
Block a user