wearos home screen
This commit is contained in:
parent
442ba79445
commit
47ba1a1bba
@ -1,33 +1,223 @@
|
||||
// ignore_for_file: avoid_print
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:firka/helpers/api/model/timetable.dart';
|
||||
import 'package:firka/wear_main.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class WearHomeScreen extends StatelessWidget {
|
||||
import '../../../ui/colors.dart';
|
||||
import '../../../ui/widgets/circular_progress_indicator.dart';
|
||||
|
||||
class WearHomeScreen extends StatefulWidget {
|
||||
final WearAppInitialization data;
|
||||
const WearHomeScreen(this.data, {super.key});
|
||||
|
||||
@override
|
||||
State<WearHomeScreen> createState() => _WearHomeScreenState(data);
|
||||
}
|
||||
|
||||
class _WearHomeScreenState extends State<WearHomeScreen> {
|
||||
final WearAppInitialization data;
|
||||
_WearHomeScreenState(this.data);
|
||||
|
||||
List<Lesson> today = List.empty(growable: true);
|
||||
String apiError = "";
|
||||
DateTime now = DateTime.now();
|
||||
Timer? timer;
|
||||
bool init = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
now = DateTime.now();
|
||||
|
||||
timer = Timer.periodic(Duration(seconds: 5), (timer) {
|
||||
setState(() {
|
||||
now = DateTime.now();
|
||||
});
|
||||
});
|
||||
initStateAsync();
|
||||
}
|
||||
|
||||
Future<void> initStateAsync() async {
|
||||
var kreta = data.client;
|
||||
|
||||
now = DateTime.now();
|
||||
var todayStart = now.subtract(Duration(hours: now.hour, minutes: now.minute
|
||||
, seconds: now.second));
|
||||
var todayEnd = todayStart.add(Duration(hours: 23, minutes: 59));
|
||||
var classes = await kreta.getTimeTable(todayStart, todayEnd);
|
||||
|
||||
setState(() {
|
||||
if (classes.response != null) today = classes.response!;
|
||||
if (classes.statusCode != 200) {
|
||||
apiError = "Unexpected status : ${classes.statusCode}";
|
||||
}
|
||||
if (classes.err != null) apiError = classes.err!;
|
||||
|
||||
init = true;
|
||||
});
|
||||
}
|
||||
|
||||
List<Widget> buildBody(BuildContext context) {
|
||||
var body = List<Widget>.empty(growable: true);
|
||||
if (!init) {
|
||||
return body;
|
||||
}
|
||||
|
||||
if (today.isEmpty && apiError != "") {
|
||||
body.add(Text(
|
||||
apiError,
|
||||
style: TextStyle(color: defaultColors.secondaryText, fontSize: 18),
|
||||
textAlign: TextAlign.center,
|
||||
));
|
||||
|
||||
return body;
|
||||
}
|
||||
if (today.isEmpty) {
|
||||
body.add(Text(
|
||||
"You don't have any classes today",
|
||||
style: TextStyle(color: defaultColors.secondaryText, fontSize: 18),
|
||||
textAlign: TextAlign.center,
|
||||
));
|
||||
|
||||
return body;
|
||||
}
|
||||
if (now.isAfter(today.last.end)) {
|
||||
body.add(Text(
|
||||
"You don't have any more classes today",
|
||||
style: TextStyle(color: defaultColors.secondaryText, fontSize: 18),
|
||||
textAlign: TextAlign.center,
|
||||
));
|
||||
|
||||
return body;
|
||||
}
|
||||
if (now.isAfter(today.first.start)
|
||||
&& now.isBefore(today.last.end)) {
|
||||
Lesson? currentLesson;
|
||||
Lesson? lastLesson; // last as in the last lesson that you've been to
|
||||
Lesson? next;
|
||||
Duration? currentBreak;
|
||||
Duration? currentBreakProgress;
|
||||
for (int i = 0; i < today.length; i++) {
|
||||
var lesson = today[i];
|
||||
if (now.isAfter(lesson.start) && now.isBefore(lesson.end)) {
|
||||
currentLesson = lesson;
|
||||
break;
|
||||
}
|
||||
if (now.isAfter(lesson.end)) {
|
||||
if (lastLesson == null) {
|
||||
lastLesson = lesson;
|
||||
if (i < today.length) next = today[i+1];
|
||||
} else {
|
||||
if (lesson.end.isAfter(lastLesson.end)) {
|
||||
lastLesson = lesson;
|
||||
if (i < today.length) next = today[i+1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lastLesson != null && next != null) {
|
||||
currentBreak = next.start.difference(lastLesson.end);
|
||||
currentBreakProgress = next.start.difference(now);
|
||||
}
|
||||
|
||||
if (currentLesson == null) {
|
||||
if (currentBreak == null) {
|
||||
throw Exception("currentBreak == null");
|
||||
}
|
||||
if (currentBreakProgress == null) {
|
||||
throw Exception("currentBreakProgress == null");
|
||||
}
|
||||
|
||||
body.add(CustomPaint(
|
||||
painter: CircularProgressPainter(
|
||||
progress: currentBreakProgress.inMilliseconds / currentBreak.inMilliseconds,
|
||||
screenSize: MediaQuery.of(context).size,
|
||||
strokeWidth: 4,
|
||||
color: defaultColors.radiusColor
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Center(
|
||||
child: Text(
|
||||
"Break",
|
||||
style: TextStyle(color: defaultColors.secondaryText, fontSize: 20),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
"${currentBreakProgress.inMinutes} "
|
||||
"min${currentBreakProgress.inMinutes == 1 ? '' : 's'} left",
|
||||
style: TextStyle(color: defaultColors.secondaryText, fontSize: 16),
|
||||
),
|
||||
),
|
||||
]
|
||||
)
|
||||
));
|
||||
|
||||
return body;
|
||||
} else {
|
||||
var duration = currentLesson.start.difference(currentLesson.end);
|
||||
var elapsed = currentLesson.start.difference(now);
|
||||
var timeLeft = currentLesson.end.difference(now);
|
||||
|
||||
body.add(CustomPaint(
|
||||
painter: CircularProgressPainter(
|
||||
progress: elapsed.inMilliseconds / duration.inMilliseconds,
|
||||
screenSize: MediaQuery
|
||||
.of(context)
|
||||
.size,
|
||||
strokeWidth: 4,
|
||||
color: defaultColors.radiusColor
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Center(
|
||||
child: Text(
|
||||
currentLesson.name,
|
||||
style: TextStyle(
|
||||
color: defaultColors.secondaryText, fontSize: 20),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
"${timeLeft.inMinutes} "
|
||||
"min${timeLeft.inMinutes == 1 ? '' : 's'} left",
|
||||
style: TextStyle(
|
||||
color: defaultColors.secondaryText, fontSize: 16),
|
||||
),
|
||||
),
|
||||
]
|
||||
)
|
||||
));
|
||||
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
throw Exception("unexpected state");
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var body = buildBody(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Stub'),
|
||||
centerTitle: true,
|
||||
),
|
||||
backgroundColor: defaultColors.ambientBackgroundColor,
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
'Home',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
children: body,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
timer?.cancel();
|
||||
}
|
||||
}
|
||||
|
30
firka/lib/ui/colors.dart
Normal file
30
firka/lib/ui/colors.dart
Normal file
@ -0,0 +1,30 @@
|
||||
import 'dart:ui';
|
||||
|
||||
class WearColors {
|
||||
|
||||
Color backgroundColor;
|
||||
Color ambientBackgroundColor;
|
||||
Color radiusColor;
|
||||
Color primaryText;
|
||||
Color secondaryText;
|
||||
Color tertiaryText;
|
||||
|
||||
WearColors({
|
||||
required this.backgroundColor,
|
||||
required this.ambientBackgroundColor,
|
||||
required this.radiusColor,
|
||||
required this.primaryText,
|
||||
required this.secondaryText,
|
||||
required this.tertiaryText
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
WearColors defaultColors = WearColors(
|
||||
backgroundColor: Color(0xff0c1201),
|
||||
ambientBackgroundColor: Color(0xff000000),
|
||||
radiusColor: Color(0xffa6dc22),
|
||||
primaryText: Color(0xffcbee71),
|
||||
secondaryText: Color(0xffe9f7cb),
|
||||
tertiaryText: Color(0xffa7b290),
|
||||
);
|
101
firka/lib/ui/widgets/circular_progress_indicator.dart
Normal file
101
firka/lib/ui/widgets/circular_progress_indicator.dart
Normal file
@ -0,0 +1,101 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CircularProgressIndicator extends StatefulWidget {
|
||||
final double progress;
|
||||
final double strokeWidth;
|
||||
final Color color;
|
||||
final Size screenSize;
|
||||
|
||||
CircularProgressIndicator({
|
||||
required this.progress,
|
||||
required this.screenSize,
|
||||
this.strokeWidth = 8.0,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
_CircularProgressIndicatorState createState() => _CircularProgressIndicatorState();
|
||||
}
|
||||
|
||||
class _CircularProgressIndicatorState extends State<CircularProgressIndicator>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
vsync: this,
|
||||
);
|
||||
_animation = Tween<double>(begin: 0.0, end: widget.progress).animate(_controller);
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _animation,
|
||||
builder: (context, child) {
|
||||
return CustomPaint(
|
||||
painter: CircularProgressPainter(
|
||||
progress: _animation.value,
|
||||
strokeWidth: widget.strokeWidth,
|
||||
color: widget.color,
|
||||
screenSize: widget.screenSize,
|
||||
),
|
||||
child: SizedBox.expand(), // Fill the entire screen
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CircularProgressPainter extends CustomPainter {
|
||||
final double progress;
|
||||
final double strokeWidth;
|
||||
final Color color;
|
||||
final Size screenSize;
|
||||
|
||||
CircularProgressPainter({
|
||||
required this.progress,
|
||||
required this.strokeWidth,
|
||||
required this.color,
|
||||
required this.screenSize,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final center = Offset(screenSize.width / 2, screenSize.height / 7.4);
|
||||
final radius = min(screenSize.width, screenSize.height) / 2 - strokeWidth / 2;
|
||||
final startAngle = -pi / 2;
|
||||
final sweepAngle = 2 * pi * progress;
|
||||
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = strokeWidth;
|
||||
|
||||
canvas.drawArc(
|
||||
Rect.fromCircle(center: center, radius: radius),
|
||||
startAngle,
|
||||
sweepAngle,
|
||||
false,
|
||||
paint,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) {
|
||||
return true;
|
||||
}
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:firka/helpers/db/models/generic_cache_model.dart';
|
||||
import 'package:firka/helpers/db/models/timetable_cache_model.dart';
|
||||
import 'package:firka/helpers/db/models/token_model.dart';
|
||||
import 'package:firka/pages/error/wear_error_page.dart';
|
||||
import 'package:firka/screens/wear/home/home_screen.dart';
|
||||
import 'package:firka/screens/wear/login/login_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -10,11 +11,14 @@ import 'package:isar/isar.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:wear_plus/wear_plus.dart';
|
||||
|
||||
import 'helpers/api/client/kreta_client.dart';
|
||||
|
||||
late Isar isar;
|
||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
class WearAppInitialization {
|
||||
final Isar isar;
|
||||
late KretaClient client;
|
||||
final int tokenCount;
|
||||
|
||||
WearAppInitialization({
|
||||
@ -27,7 +31,7 @@ Future<Isar> initDB() async {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
|
||||
return Isar.open(
|
||||
[TokenModelSchema],
|
||||
[TokenModelSchema, GenericCacheModelSchema, TimetableCacheModelSchema],
|
||||
inspector: true,
|
||||
directory: dir.path,
|
||||
);
|
||||
@ -41,27 +45,25 @@ Future<WearAppInitialization> initializeApp() async {
|
||||
tokenCount: await isar.tokenModels.count()
|
||||
);
|
||||
|
||||
resetOldTimeTableCache(isar);
|
||||
|
||||
// TODO: Account selection
|
||||
if (init.tokenCount > 0) {
|
||||
init.client = KretaClient(
|
||||
(await isar.tokenModels.where().findFirst())!,
|
||||
isar
|
||||
);
|
||||
}
|
||||
|
||||
return init;
|
||||
}
|
||||
|
||||
void wearMain(MethodChannel platform) async {
|
||||
// TODO: fix the error handling currently not pushing to the error page
|
||||
runZonedGuarded(() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Run App Initialization
|
||||
runApp(WearInitializationScreen());
|
||||
|
||||
}, (error, stackTrace) {
|
||||
debugPrint('Caught error: $error');
|
||||
debugPrint('Stack trace: $stackTrace');
|
||||
|
||||
navigatorKey.currentState?.push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => WearErrorPage(exception: error.toString()),
|
||||
),
|
||||
);
|
||||
});
|
||||
// Run App Initialization
|
||||
runApp(WearInitializationScreen());
|
||||
}
|
||||
|
||||
class WearInitializationScreen extends StatelessWidget {
|
||||
|
Loading…
x
Reference in New Issue
Block a user