finished schedules screens
This commit is contained in:
@@ -5,10 +5,16 @@ SnackBar errorSnackbar(
|
||||
Color backgroundColor = const Color.fromARGB(255, 196, 26, 14),
|
||||
Duration duration = const Duration(seconds: 3),
|
||||
}) {
|
||||
final errorText = e is String
|
||||
? e
|
||||
: (e.toString().contains(':')
|
||||
? e.toString().substring(e.toString().indexOf(':') + 1).trim()
|
||||
: e.toString());
|
||||
|
||||
return SnackBar(
|
||||
backgroundColor: Color.fromARGB(255, 196, 26, 14),
|
||||
content: Text(
|
||||
e.toString().replaceFirst(RegExp(r'^[^:]+:\s*'), ''),
|
||||
errorText,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
|
||||
@@ -1,12 +1,31 @@
|
||||
import 'package:blind_master/BlindMasterResources/error_snackbar.dart';
|
||||
import 'package:blind_master/BlindMasterResources/secure_transmissions.dart';
|
||||
import 'package:blind_master/main.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class DayTimePicker extends StatefulWidget {
|
||||
const DayTimePicker({super.key, required this.defaultTime, required this.sendSchedule});
|
||||
const DayTimePicker({
|
||||
super.key,
|
||||
required this.defaultTime,
|
||||
required this.sendSchedule,
|
||||
required this.peripheralId,
|
||||
required this.peripheralNum,
|
||||
required this.deviceId,
|
||||
this.existingSchedule,
|
||||
this.scheduleId,
|
||||
});
|
||||
|
||||
final TimeOfDay defaultTime;
|
||||
final void Function(TimeOfDay) sendSchedule;
|
||||
final int peripheralId;
|
||||
final int peripheralNum;
|
||||
final int deviceId;
|
||||
final Map<String, dynamic>? existingSchedule;
|
||||
final String? scheduleId;
|
||||
|
||||
bool get isEditing => existingSchedule != null && scheduleId != null;
|
||||
|
||||
@override
|
||||
State<DayTimePicker> createState() => _DayTimePickerState();
|
||||
}
|
||||
@@ -16,10 +35,25 @@ class _DayTimePickerState extends State<DayTimePicker> {
|
||||
double _blindPosition = 0;
|
||||
String imagePath = "";
|
||||
Set<DaysOfWeek> days = <DaysOfWeek>{};
|
||||
bool showError = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// If editing, pre-populate with existing schedule data
|
||||
if (widget.isEditing && widget.existingSchedule != null) {
|
||||
final schedule = widget.existingSchedule!;
|
||||
final hour = schedule['schedule']['hours'][0] as int;
|
||||
final minute = schedule['schedule']['minutes'][0] as int;
|
||||
scheduleTime = TimeOfDay(hour: hour, minute: minute);
|
||||
_blindPosition = (schedule['pos'] as int).toDouble();
|
||||
|
||||
// Pre-populate days
|
||||
final daysOfWeek = schedule['schedule']['daysOfWeek'] as List;
|
||||
days = daysOfWeek.map((d) => DaysOfWeek.values[d as int]).toSet();
|
||||
}
|
||||
|
||||
updateBackground();
|
||||
}
|
||||
|
||||
@@ -84,30 +118,38 @@ class _DayTimePickerState extends State<DayTimePicker> {
|
||||
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(top: MediaQuery.of(context).size.width * 0.05),
|
||||
height: MediaQuery.of(context).size.width * 0.43,
|
||||
width: MediaQuery.of(context).size.width * 0.45,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List.generate(10, (index) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
height: _blindPosition < 5 ?
|
||||
3.65 * (5 - _blindPosition)
|
||||
: 3.65 * (_blindPosition - 5),
|
||||
width: MediaQuery.of(context).size.width * 0.40, // example
|
||||
color: const Color.fromARGB(255, 121, 85, 72),
|
||||
);
|
||||
}),
|
||||
),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final containerHeight = MediaQuery.of(context).size.width * 0.43;
|
||||
final maxSlatHeight = containerHeight / 10;
|
||||
final slatHeight = _blindPosition < 5
|
||||
? maxSlatHeight * (5 - _blindPosition) / 5
|
||||
: maxSlatHeight * (_blindPosition - 5) / 5;
|
||||
|
||||
return Container(
|
||||
margin: EdgeInsets.only(top: MediaQuery.of(context).size.width * 0.05),
|
||||
height: containerHeight,
|
||||
width: MediaQuery.of(context).size.width * 0.45,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List.generate(10, (index) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
height: slatHeight,
|
||||
width: MediaQuery.of(context).size.width * 0.40,
|
||||
color: const Color.fromARGB(255, 121, 85, 72),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
// Slider on the side
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.10,
|
||||
child: RotatedBox(
|
||||
quarterTurns: -1,
|
||||
child: Slider(
|
||||
@@ -171,6 +213,7 @@ class _DayTimePickerState extends State<DayTimePicker> {
|
||||
setState(() {
|
||||
if (selected) {
|
||||
days.add(day);
|
||||
showError = false;
|
||||
} else {
|
||||
days.remove(day);
|
||||
}
|
||||
@@ -179,6 +222,18 @@ class _DayTimePickerState extends State<DayTimePicker> {
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
if (showError)
|
||||
Container(
|
||||
padding: EdgeInsets.only(top: 10),
|
||||
child: Text(
|
||||
'Please select at least one day',
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
fontSize: 14,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
@@ -196,6 +251,78 @@ class _DayTimePickerState extends State<DayTimePicker> {
|
||||
),
|
||||
)
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
if (days.isEmpty) {
|
||||
setState(() {
|
||||
showError = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Convert DaysOfWeek enum to day numbers (0=Sunday, 1=Monday, etc.)
|
||||
final daysOfWeek = days.map((day) => day.index).toList();
|
||||
|
||||
final timeToUse = scheduleTime ?? widget.defaultTime;
|
||||
|
||||
final payload = {
|
||||
'periphId': widget.peripheralId,
|
||||
'periphNum': widget.peripheralNum,
|
||||
'deviceId': widget.deviceId,
|
||||
'newPos': _blindPosition.toInt(),
|
||||
'time': {
|
||||
'hour': timeToUse.hour,
|
||||
'minute': timeToUse.minute,
|
||||
},
|
||||
'daysOfWeek': daysOfWeek,
|
||||
};
|
||||
|
||||
// Add jobId if editing
|
||||
if (widget.isEditing) {
|
||||
payload['jobId'] = widget.scheduleId!;
|
||||
}
|
||||
|
||||
final endpoint = widget.isEditing ? 'update_schedule' : 'add_schedule';
|
||||
final response = await securePost(payload, endpoint);
|
||||
|
||||
if (response == null) throw Exception("Auth Error");
|
||||
|
||||
// Handle duplicate schedule (409 Conflict)
|
||||
if (response.statusCode == 409) {
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('A schedule already exists at this time for this blind'),
|
||||
backgroundColor: Colors.orange,
|
||||
duration: Duration(seconds: 4),
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.statusCode != 201 && response.statusCode != 200) {
|
||||
throw Exception("Server Error");
|
||||
}
|
||||
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(widget.isEditing ? 'Schedule updated successfully' : 'Schedule added successfully'),
|
||||
backgroundColor: Colors.green,
|
||||
)
|
||||
);
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
|
||||
}
|
||||
},
|
||||
child: Text(widget.isEditing ? "Update" : "Add")
|
||||
)
|
||||
]
|
||||
)
|
||||
],
|
||||
|
||||
@@ -307,14 +307,20 @@ class _PeripheralScreenState extends State<PeripheralScreen> {
|
||||
}
|
||||
else {
|
||||
getImage();
|
||||
final nowUtc = DateTime.now().toUtc();
|
||||
final lastSetUtc = DateTime.parse(body['last_set']);
|
||||
final Duration difference = nowUtc.difference(lastSetUtc);
|
||||
if (!lastSetUtc.isUtc) throw Exception("Why isn't the server giving UTC?");
|
||||
final diffDays = difference.inDays > 0;
|
||||
final diffHours = difference.inHours > 0;
|
||||
final diffMins = difference.inMinutes > 0;
|
||||
lastSetMessage = "Last set ${diffDays ? '${difference.inDays.toString()} days' : diffHours ? '${difference.inHours.toString()} hours' : diffMins ? '${difference.inMinutes.toString()} minutes' : '${difference.inSeconds.toString()} seconds'} ago";
|
||||
|
||||
if (body['last_set'] != null) {
|
||||
final nowUtc = DateTime.now().toUtc();
|
||||
final lastSetUtc = DateTime.parse(body['last_set']);
|
||||
final Duration difference = nowUtc.difference(lastSetUtc);
|
||||
if (!lastSetUtc.isUtc) throw Exception("Why isn't the server giving UTC?");
|
||||
final diffDays = difference.inDays > 0;
|
||||
final diffHours = difference.inHours > 0;
|
||||
final diffMins = difference.inMinutes > 0;
|
||||
lastSetMessage = "Last set ${diffDays ? '${difference.inDays.toString()} days' : diffHours ? '${difference.inHours.toString()} hours' : diffMins ? '${difference.inMinutes.toString()} minutes' : '${difference.inSeconds.toString()} seconds'} ago";
|
||||
} else {
|
||||
lastSetMessage = "Never set";
|
||||
}
|
||||
|
||||
_blindPosition = (body['last_pos'] as int).toDouble();
|
||||
|
||||
calibrated = true;
|
||||
@@ -607,23 +613,31 @@ class _PeripheralScreenState extends State<PeripheralScreen> {
|
||||
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(top: MediaQuery.of(context).size.width * 0.05),
|
||||
height: MediaQuery.of(context).size.width * 0.68,
|
||||
width: MediaQuery.of(context).size.width * 0.7,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List.generate(10, (index) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
height: _blindPosition < 5 ?
|
||||
5.4 * (5 - _blindPosition)
|
||||
: 5.4 * (_blindPosition - 5),
|
||||
width: MediaQuery.of(context).size.width * 0.65, // example
|
||||
color: const Color.fromARGB(255, 121, 85, 72),
|
||||
);
|
||||
}),
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final containerHeight = MediaQuery.of(context).size.width * 0.68;
|
||||
final maxSlatHeight = containerHeight / 10;
|
||||
final slatHeight = _blindPosition < 5
|
||||
? maxSlatHeight * (5 - _blindPosition) / 5
|
||||
: maxSlatHeight * (_blindPosition - 5) / 5;
|
||||
|
||||
return Container(
|
||||
margin: EdgeInsets.only(top: MediaQuery.of(context).size.width * 0.05),
|
||||
height: containerHeight,
|
||||
width: MediaQuery.of(context).size.width * 0.7,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List.generate(10, (index) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
height: slatHeight,
|
||||
width: MediaQuery.of(context).size.width * 0.65,
|
||||
color: const Color.fromARGB(255, 121, 85, 72),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
)
|
||||
)
|
||||
],
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:convert';
|
||||
import 'package:blind_master/BlindMasterResources/error_snackbar.dart';
|
||||
import 'package:blind_master/BlindMasterResources/secure_transmissions.dart';
|
||||
import 'package:blind_master/BlindMasterScreens/day_time_picker.dart';
|
||||
import 'package:blind_master/main.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
@@ -28,10 +29,27 @@ class _SchedulesScreenState extends State<SchedulesScreen> {
|
||||
getSchedules();
|
||||
}
|
||||
|
||||
String translate(int pos) {
|
||||
if (pos < 2) {
|
||||
return "Close (down)";
|
||||
} else if (pos < 5) {
|
||||
return "Open (down)";
|
||||
}
|
||||
else if (pos == 5) {
|
||||
return "Open";
|
||||
}
|
||||
else if (pos < 9) {
|
||||
return "Open (up)";
|
||||
}
|
||||
else {
|
||||
return "Close (up)";
|
||||
}
|
||||
}
|
||||
|
||||
Future getSchedules() async {
|
||||
try{
|
||||
final payload = {
|
||||
"periphId": widget.deviceId
|
||||
"periphId": widget.peripheralId
|
||||
};
|
||||
final response = await securePost(payload, 'periph_schedule_list');
|
||||
if (response == null) throw Exception("no response!");
|
||||
@@ -100,23 +118,66 @@ class _SchedulesScreenState extends State<SchedulesScreen> {
|
||||
),
|
||||
);
|
||||
},
|
||||
onDismissed: (direction) {
|
||||
// TODO Actually delete the schedule
|
||||
// deleteDevice(device['id'], i);
|
||||
onDismissed: (direction) async {
|
||||
final scheduleId = schedule['id'].toString();
|
||||
try {
|
||||
final payload = {'jobId': scheduleId};
|
||||
final response = await securePost(payload, 'delete_schedule');
|
||||
|
||||
if (response == null) throw Exception("Auth Error");
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception("Failed to delete schedule");
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Schedule deleted successfully'),
|
||||
backgroundColor: Colors.green,
|
||||
)
|
||||
);
|
||||
} catch (e) {
|
||||
if (mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e));
|
||||
} finally {
|
||||
// Refresh the list regardless of success/failure
|
||||
if (mounted) getSchedules();
|
||||
}
|
||||
},
|
||||
child: Card(
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.blinds),
|
||||
title: Text("${schedule['pos']} every ${schedule['schedule']['daysOfWeek']} at ${schedule['schedule']['hours']}:${schedule['schedule']['minutes']}"),
|
||||
trailing: const Icon(Icons.arrow_forward_ios_rounded),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => Placeholder(),
|
||||
// TODO open popup for schedule setter.
|
||||
),
|
||||
).then((_) { getSchedules(); });
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final pos = translate(schedule['pos']);
|
||||
final days = (schedule['schedule']['daysOfWeek'] as List)
|
||||
.map((d) => DaysOfWeek.values[d].name)
|
||||
.join(', ');
|
||||
final hour24 = schedule['schedule']['hours'][0] as int;
|
||||
final minute = schedule['schedule']['minutes'][0].toString().padLeft(2, '0');
|
||||
final period = hour24 >= 12 ? 'PM' : 'AM';
|
||||
final hour12 = hour24 == 0 ? 12 : (hour24 > 12 ? hour24 - 12 : hour24);
|
||||
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.blinds),
|
||||
title: Text("$pos at $hour12:$minute $period"),
|
||||
subtitle: Text(days),
|
||||
trailing: const Icon(Icons.arrow_forward_ios_rounded),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return DayTimePicker(
|
||||
defaultTime: TimeOfDay(hour: 12, minute: 0),
|
||||
sendSchedule: sendSchedule,
|
||||
peripheralId: widget.peripheralId,
|
||||
peripheralNum: widget.peripheralNum,
|
||||
deviceId: widget.deviceId,
|
||||
existingSchedule: schedule,
|
||||
scheduleId: schedule['id'].toString(),
|
||||
);
|
||||
}
|
||||
).then((_) { getSchedules(); });
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -136,9 +197,15 @@ class _SchedulesScreenState extends State<SchedulesScreen> {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext dialogContext) { // Use dialogContext for navigation within the dialog
|
||||
return DayTimePicker(defaultTime: TimeOfDay(hour: 12, minute: 0), sendSchedule: sendSchedule);
|
||||
return DayTimePicker(
|
||||
defaultTime: TimeOfDay(hour: 12, minute: 0),
|
||||
sendSchedule: sendSchedule,
|
||||
peripheralId: widget.peripheralId,
|
||||
peripheralNum: widget.peripheralNum,
|
||||
deviceId: widget.deviceId,
|
||||
);
|
||||
}
|
||||
);
|
||||
).then((_) { getSchedules(); });
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -340,10 +340,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
version: "1.17.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -505,10 +505,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
||||
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.6"
|
||||
version: "0.7.7"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
Reference in New Issue
Block a user