Add files via upload
This commit is contained in:
156
filcnaplo_premium/lib/ui/mobile/goal_planner/goal_input.dart
Normal file
156
filcnaplo_premium/lib/ui/mobile/goal_planner/goal_input.dart
Normal file
@@ -0,0 +1,156 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class GoalInput extends StatelessWidget {
|
||||
const GoalInput({Key? key, required this.currentAverage, required this.value, required this.onChanged}) : super(key: key);
|
||||
|
||||
final double currentAverage;
|
||||
final double value;
|
||||
final void Function(double value) onChanged;
|
||||
|
||||
void offsetToValue(Offset offset, Size size) {
|
||||
double v = ((offset.dx / size.width * 4 + 1) * 10).round() / 10;
|
||||
v = v.clamp(1.5, 5);
|
||||
v = v.clamp(((currentAverage * 10).round() / 10), 5);
|
||||
setValue(v);
|
||||
}
|
||||
|
||||
void setValue(double v) {
|
||||
if (v != value) {
|
||||
HapticFeedback.lightImpact();
|
||||
}
|
||||
onChanged(v);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<int> presets = [2, 3, 4, 5];
|
||||
presets = presets.where((e) => gradeToAvg(e) > currentAverage).toList();
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
LayoutBuilder(builder: (context, size) {
|
||||
return GestureDetector(
|
||||
onTapDown: (details) {
|
||||
offsetToValue(details.localPosition, size.biggest);
|
||||
},
|
||||
onHorizontalDragUpdate: (details) {
|
||||
offsetToValue(details.localPosition, size.biggest);
|
||||
},
|
||||
child: SizedBox(
|
||||
height: 32.0,
|
||||
width: double.infinity,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 20.0),
|
||||
child: CustomPaint(
|
||||
painter: GoalSliderPainter(value: (value - 1) / 4),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 12.0),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: presets.map((e) {
|
||||
final pv = (value * 10).round() / 10;
|
||||
final selected = gradeToAvg(e) == pv;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(99.0),
|
||||
color: gradeColor(e).withOpacity(selected ? 1.0 : 0.2),
|
||||
border: Border.all(color: gradeColor(e), width: 4),
|
||||
),
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(99.0),
|
||||
onTap: () => setValue(gradeToAvg(e)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 24.0),
|
||||
child: Text(
|
||||
e.toString(),
|
||||
style: TextStyle(
|
||||
color: selected ? Colors.white : gradeColor(e),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 24.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GoalSliderPainter extends CustomPainter {
|
||||
final double value;
|
||||
|
||||
GoalSliderPainter({required this.value});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final radius = size.height / 2;
|
||||
const cpadding = 4;
|
||||
final rect = Rect.fromLTWH(0, 0, size.width + radius, size.height);
|
||||
final vrect = Rect.fromLTWH(0, 0, size.width * value + radius, size.height);
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
rect,
|
||||
const Radius.circular(99.0),
|
||||
),
|
||||
Paint()..color = Colors.black.withOpacity(.1),
|
||||
);
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
vrect,
|
||||
const Radius.circular(99.0),
|
||||
),
|
||||
Paint()
|
||||
..shader = const LinearGradient(colors: [
|
||||
Color(0xffFF3B30),
|
||||
Color(0xffFF9F0A),
|
||||
Color(0xffFFD60A),
|
||||
Color(0xff34C759),
|
||||
Color(0xff247665),
|
||||
]).createShader(rect),
|
||||
);
|
||||
canvas.drawOval(
|
||||
Rect.fromCircle(center: Offset(size.width * value, size.height / 2), radius: radius - cpadding),
|
||||
Paint()..color = Colors.white,
|
||||
);
|
||||
for (int i = 1; i < 4; i++) {
|
||||
canvas.drawOval(
|
||||
Rect.fromCircle(center: Offset(size.width / 4 * i, size.height / 2), radius: 4),
|
||||
Paint()..color = Colors.white.withOpacity(.5),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(GoalSliderPainter oldDelegate) {
|
||||
return oldDelegate.value != value;
|
||||
}
|
||||
}
|
||||
|
||||
double gradeToAvg(int grade) {
|
||||
return grade - 0.5;
|
||||
}
|
||||
|
||||
Color gradeColor(int grade) {
|
||||
return [
|
||||
const Color(0xffFF3B30),
|
||||
const Color(0xffFF9F0A),
|
||||
const Color(0xffFFD60A),
|
||||
const Color(0xff34C759),
|
||||
const Color(0xff247665),
|
||||
].elementAt(grade.clamp(1, 5) - 1);
|
||||
}
|
||||
172
filcnaplo_premium/lib/ui/mobile/goal_planner/goal_planner.dart
Normal file
172
filcnaplo_premium/lib/ui/mobile/goal_planner/goal_planner.dart
Normal file
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* Maintainer: DarK
|
||||
* Translated from C version
|
||||
* Minimal Working Fixed @ 2022.12.25
|
||||
* ##Please do NOT modify if you don't know whats going on##
|
||||
*
|
||||
* Issue: #59
|
||||
*
|
||||
* Future changes / ideas:
|
||||
* - `best` should be configurable
|
||||
*/
|
||||
import 'dart:math';
|
||||
import 'package:filcnaplo_kreta_api/models/category.dart';
|
||||
import 'package:filcnaplo_kreta_api/models/grade.dart';
|
||||
import 'package:filcnaplo_kreta_api/models/subject.dart';
|
||||
import 'package:flutter/foundation.dart' show listEquals;
|
||||
|
||||
/// Generate list of grades that achieve the wanted goal.
|
||||
/// After generating possible options, it (when doing so would NOT result in empty list) filters with two criteria:
|
||||
/// - Plan should not contain more than 15 grades
|
||||
/// - Plan should not contain only one type of grade
|
||||
///
|
||||
/// **Usage**:
|
||||
///
|
||||
/// ```dart
|
||||
/// List<int> GoalPlanner(double goal, List<Grade> grades).solve().plan
|
||||
/// ```
|
||||
class GoalPlanner {
|
||||
final double goal;
|
||||
final List<Grade> grades;
|
||||
List<Plan> plans = [];
|
||||
GoalPlanner(this.goal, this.grades);
|
||||
|
||||
bool _allowed(int grade) => grade > goal;
|
||||
|
||||
void _generate(Generator g) {
|
||||
// Exit condition 1: Generator has working plan.
|
||||
if (g.currentAvg.avg >= goal) {
|
||||
plans.add(Plan(g.plan));
|
||||
return;
|
||||
}
|
||||
// Exit condition 2: Generator plan will never work.
|
||||
if (!_allowed(g.gradeToAdd)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = g.max; i >= 0; i--) {
|
||||
int newGradeToAdd = g.gradeToAdd - 1;
|
||||
List<int> newPlan = GoalPlannerHelper._addToList<int>(g.plan, g.gradeToAdd, i);
|
||||
|
||||
Avg newAvg = GoalPlannerHelper._addToAvg(g.currentAvg, g.gradeToAdd, i);
|
||||
int newN = GoalPlannerHelper.howManyNeeded(
|
||||
newGradeToAdd,
|
||||
grades +
|
||||
newPlan
|
||||
.map((e) => Grade(
|
||||
id: '',
|
||||
date: DateTime(0),
|
||||
value: GradeValue(e, '', '', 100),
|
||||
teacher: '',
|
||||
description: '',
|
||||
form: '',
|
||||
groupId: '',
|
||||
type: GradeType.midYear,
|
||||
subject: Subject.fromJson({}),
|
||||
mode: Category.fromJson({}),
|
||||
seenDate: DateTime(0),
|
||||
writeDate: DateTime(0),
|
||||
))
|
||||
.toList(),
|
||||
goal);
|
||||
|
||||
_generate(Generator(newGradeToAdd, newN, newAvg, newPlan));
|
||||
}
|
||||
}
|
||||
|
||||
List<Plan> solve() {
|
||||
_generate(
|
||||
Generator(
|
||||
5,
|
||||
GoalPlannerHelper.howManyNeeded(
|
||||
5,
|
||||
grades,
|
||||
goal,
|
||||
),
|
||||
Avg(GoalPlannerHelper.averageEvals(grades), GoalPlannerHelper.weightSum(grades)),
|
||||
[],
|
||||
),
|
||||
);
|
||||
|
||||
// Calculate Statistics
|
||||
for (var e in plans) {
|
||||
e.sum = e.plan.fold(0, (int a, b) => a + b);
|
||||
e.avg = e.sum / e.plan.length;
|
||||
e.sigma = sqrt(e.plan.map((i) => pow(i - e.avg, 2)).fold(0, (num a, b) => a + b) / e.plan.length);
|
||||
}
|
||||
|
||||
// filter without aggression
|
||||
if (plans.where((e) => e.plan.length < 30).isNotEmpty) {
|
||||
plans.removeWhere((e) => !(e.plan.length < 30));
|
||||
}
|
||||
if (plans.where((e) => e.sigma > 1).isNotEmpty) {
|
||||
plans.removeWhere((e) => !(e.sigma > 1));
|
||||
}
|
||||
|
||||
return plans;
|
||||
}
|
||||
}
|
||||
|
||||
class Avg {
|
||||
final double avg;
|
||||
final double n;
|
||||
|
||||
Avg(this.avg, this.n);
|
||||
}
|
||||
|
||||
class Generator {
|
||||
final int gradeToAdd;
|
||||
final int max;
|
||||
final Avg currentAvg;
|
||||
final List<int> plan;
|
||||
|
||||
Generator(this.gradeToAdd, this.max, this.currentAvg, this.plan);
|
||||
}
|
||||
|
||||
class Plan {
|
||||
final List<int> plan;
|
||||
int sum = 0;
|
||||
double avg = 0;
|
||||
int med = 0; // currently
|
||||
int mod = 0; // unused
|
||||
double sigma = 0;
|
||||
|
||||
Plan(this.plan);
|
||||
|
||||
@override
|
||||
bool operator ==(other) => other is Plan && listEquals(plan, other.plan);
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hashAll(plan);
|
||||
}
|
||||
|
||||
class GoalPlannerHelper {
|
||||
static Avg _addToAvg(Avg base, int grade, int n) => Avg((base.avg * base.n + grade * n) / (base.n + n), base.n + n);
|
||||
|
||||
static List<T> _addToList<T>(List<T> l, T e, int n) {
|
||||
if (n == 0) return l;
|
||||
List<T> tmp = l;
|
||||
for (int i = 0; i < n; i++) {
|
||||
tmp = tmp + [e];
|
||||
}
|
||||
return tmp;
|
||||
}
|
||||
|
||||
static int howManyNeeded(int grade, List<Grade> base, double goal) {
|
||||
double avg = averageEvals(base);
|
||||
double wsum = weightSum(base);
|
||||
if (avg >= goal) return 0;
|
||||
if (grade * 1.0 == goal) return -1;
|
||||
int candidate = (wsum * (avg - goal) / (goal - grade)).floor();
|
||||
return (candidate * grade + avg * wsum) / (candidate + wsum) < goal ? candidate + 1 : candidate;
|
||||
}
|
||||
|
||||
static double averageEvals(List<Grade> grades, {bool finalAvg = false}) {
|
||||
double average =
|
||||
grades.map((e) => e.value.value * e.value.weight / 100.0).fold(0.0, (double a, double b) => a + b) / weightSum(grades, finalAvg: finalAvg);
|
||||
return average.isNaN ? 0.0 : average;
|
||||
}
|
||||
|
||||
static double weightSum(List<Grade> grades, {bool finalAvg = false}) =>
|
||||
grades.map((e) => finalAvg ? 1 : e.value.weight / 100).fold(0, (a, b) => a + b);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_input.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class GradeDisplay extends StatelessWidget {
|
||||
const GradeDisplay({Key? key, required this.grade}) : super(key: key);
|
||||
|
||||
final int grade;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: gradeColor(grade).withOpacity(.3),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
grade.toInt().toString(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 22.0,
|
||||
color: gradeColor(grade),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
126
filcnaplo_premium/lib/ui/mobile/goal_planner/route_option.dart
Normal file
126
filcnaplo_premium/lib/ui/mobile/goal_planner/route_option.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_planner.dart';
|
||||
import 'package:filcnaplo_premium/ui/mobile/goal_planner/grade_display.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum RouteMark { recommended, fastest }
|
||||
|
||||
class RouteOption extends StatelessWidget {
|
||||
const RouteOption({Key? key, required this.plan, this.mark, this.selected = false, required this.onSelected}) : super(key: key);
|
||||
|
||||
final Plan plan;
|
||||
final RouteMark? mark;
|
||||
final bool selected;
|
||||
final void Function() onSelected;
|
||||
|
||||
Widget markLabel() {
|
||||
const style = TextStyle(fontWeight: FontWeight.bold);
|
||||
|
||||
switch (mark!) {
|
||||
case RouteMark.recommended:
|
||||
return const Text("Recommended", style: style);
|
||||
case RouteMark.fastest:
|
||||
return const Text("Fastest", style: style);
|
||||
}
|
||||
}
|
||||
|
||||
Color markColor() {
|
||||
switch (mark) {
|
||||
case RouteMark.recommended:
|
||||
return const Color(0xff6a63d4);
|
||||
case RouteMark.fastest:
|
||||
return const Color(0xffe9d524);
|
||||
default:
|
||||
return Colors.teal;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget> gradeWidgets = [];
|
||||
|
||||
for (int i = 5; i > 1; i--) {
|
||||
final count = plan.plan.where((e) => e == i).length;
|
||||
|
||||
if (count > 4) {
|
||||
gradeWidgets.add(Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
"${count}x",
|
||||
style: TextStyle(
|
||||
fontSize: 22.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black.withOpacity(.7),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4.0),
|
||||
GradeDisplay(grade: i),
|
||||
],
|
||||
));
|
||||
} else {
|
||||
gradeWidgets.addAll(List.generate(count, (_) => GradeDisplay(grade: i)));
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
gradeWidgets.add(SizedBox(
|
||||
height: 36.0,
|
||||
width: 32.0,
|
||||
child: Center(child: Icon(Icons.add, color: Colors.black.withOpacity(.5))),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
gradeWidgets.removeLast();
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12.0),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Card(
|
||||
surfaceTintColor: selected ? markColor().withOpacity(.2) : Colors.white,
|
||||
margin: EdgeInsets.zero,
|
||||
elevation: 5,
|
||||
shadowColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
side: selected ? BorderSide(color: markColor(), width: 4.0) : BorderSide.none,
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
onTap: onSelected,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0, bottom: 16.0, left: 20.0, right: 12.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (mark != null) ...[
|
||||
Chip(
|
||||
label: markLabel(),
|
||||
visualDensity: VisualDensity.compact,
|
||||
backgroundColor: selected ? markColor() : Colors.transparent,
|
||||
labelPadding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
labelStyle: TextStyle(color: selected ? Colors.white : null),
|
||||
shape: StadiumBorder(
|
||||
side: BorderSide(
|
||||
color: markColor(),
|
||||
width: 3.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6.0),
|
||||
],
|
||||
Wrap(
|
||||
spacing: 4.0,
|
||||
runSpacing: 8.0,
|
||||
children: gradeWidgets,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
209
filcnaplo_premium/lib/ui/mobile/goal_planner/test.dart
Normal file
209
filcnaplo_premium/lib/ui/mobile/goal_planner/test.dart
Normal file
@@ -0,0 +1,209 @@
|
||||
import 'package:filcnaplo_kreta_api/models/grade.dart';
|
||||
import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_input.dart';
|
||||
import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_planner.dart';
|
||||
import 'package:filcnaplo_premium/ui/mobile/goal_planner/route_option.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum PlanResult {
|
||||
available, // There are possible solutions
|
||||
unreachable, // The solutions are too hard don't even try
|
||||
unsolvable, // There are no solutions
|
||||
reached, // Goal already reached
|
||||
}
|
||||
|
||||
class GoalPlannerTest extends StatefulWidget {
|
||||
const GoalPlannerTest({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<GoalPlannerTest> createState() => _GoalPlannerTestState();
|
||||
}
|
||||
|
||||
class _GoalPlannerTestState extends State<GoalPlannerTest> {
|
||||
double goalValue = 4.0;
|
||||
List<Grade> grades = [];
|
||||
|
||||
Plan? recommended;
|
||||
Plan? fastest;
|
||||
Plan? selectedRoute;
|
||||
List<Plan> otherPlans = [];
|
||||
|
||||
PlanResult getResult() {
|
||||
final currentAvg = GoalPlannerHelper.averageEvals(grades);
|
||||
|
||||
recommended = null;
|
||||
fastest = null;
|
||||
otherPlans = [];
|
||||
|
||||
if (currentAvg >= goalValue) return PlanResult.reached;
|
||||
|
||||
final planner = GoalPlanner(goalValue, grades);
|
||||
final plans = planner.solve();
|
||||
|
||||
plans.sort((a, b) => (a.avg - (2 * goalValue + 5) / 3).abs().compareTo(b.avg - (2 * goalValue + 5) / 3));
|
||||
|
||||
try {
|
||||
final singleSolution = plans.every((e) => e.sigma == 0);
|
||||
recommended = plans.where((e) => singleSolution ? true : e.sigma > 0).first;
|
||||
plans.removeWhere((e) => e == recommended);
|
||||
} catch (_) {}
|
||||
|
||||
plans.sort((a, b) => a.plan.length.compareTo(b.plan.length));
|
||||
|
||||
try {
|
||||
fastest = plans.removeAt(0);
|
||||
} catch (_) {}
|
||||
|
||||
if ((recommended?.plan.length ?? 0) - (fastest?.plan.length ?? 0) >= 3) {
|
||||
recommended = fastest;
|
||||
}
|
||||
|
||||
if (recommended == null) {
|
||||
recommended = null;
|
||||
fastest = null;
|
||||
otherPlans = [];
|
||||
selectedRoute = null;
|
||||
return PlanResult.unsolvable;
|
||||
}
|
||||
|
||||
if (recommended!.plan.length > 10) {
|
||||
recommended = null;
|
||||
fastest = null;
|
||||
otherPlans = [];
|
||||
selectedRoute = null;
|
||||
return PlanResult.unreachable;
|
||||
}
|
||||
|
||||
otherPlans = List.from(plans);
|
||||
|
||||
return PlanResult.available;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final currentAvg = GoalPlannerHelper.averageEvals(grades);
|
||||
|
||||
final result = getResult();
|
||||
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
children: [
|
||||
const Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: BackButton(),
|
||||
),
|
||||
const SizedBox(height: 12.0),
|
||||
const Text(
|
||||
"Set a goal",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 20.0,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4.0),
|
||||
Text(
|
||||
goalValue.toString(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 42.0,
|
||||
color: gradeColor(goalValue.round()),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
const Text(
|
||||
"Pick a route",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 20.0,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12.0),
|
||||
if (recommended != null)
|
||||
RouteOption(
|
||||
plan: recommended!,
|
||||
mark: RouteMark.recommended,
|
||||
selected: selectedRoute == recommended!,
|
||||
onSelected: () => setState(() {
|
||||
selectedRoute = recommended;
|
||||
}),
|
||||
),
|
||||
if (fastest != null && fastest != recommended)
|
||||
RouteOption(
|
||||
plan: fastest!,
|
||||
mark: RouteMark.fastest,
|
||||
selected: selectedRoute == fastest!,
|
||||
onSelected: () => setState(() {
|
||||
selectedRoute = fastest;
|
||||
}),
|
||||
),
|
||||
...otherPlans.map((e) => RouteOption(
|
||||
plan: e,
|
||||
selected: selectedRoute == e,
|
||||
onSelected: () => setState(() {
|
||||
selectedRoute = e;
|
||||
}),
|
||||
)),
|
||||
if (result != PlanResult.available) Text(result.name),
|
||||
],
|
||||
),
|
||||
),
|
||||
bottomSheet: MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeBottom: false,
|
||||
removeTop: true,
|
||||
child: Container(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(top: 24.0),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color.fromARGB(255, 215, 255, 242),
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24.0)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(.1),
|
||||
blurRadius: 8.0,
|
||||
)
|
||||
]),
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
GoalInput(
|
||||
value: goalValue,
|
||||
currentAverage: currentAvg,
|
||||
onChanged: (v) => setState(() {
|
||||
selectedRoute = null;
|
||||
goalValue = v;
|
||||
}),
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: RawMaterialButton(
|
||||
onPressed: () {},
|
||||
fillColor: const Color(0xff01342D),
|
||||
shape: const StadiumBorder(),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: const Text(
|
||||
"Track it!",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user