diff --git a/firka/lib/screens/wear/home/home_screen.dart b/firka/lib/screens/wear/home/home_screen.dart index 125ed74..4e4224e 100644 --- a/firka/lib/screens/wear/home/home_screen.dart +++ b/firka/lib/screens/wear/home/home_screen.dart @@ -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 createState() => _WearHomeScreenState(data); +} + +class _WearHomeScreenState extends State { + final WearAppInitialization data; + _WearHomeScreenState(this.data); + + List 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 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 buildBody(BuildContext context) { + var body = List.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(); + } } diff --git a/firka/lib/ui/colors.dart b/firka/lib/ui/colors.dart new file mode 100644 index 0000000..ebcbb68 --- /dev/null +++ b/firka/lib/ui/colors.dart @@ -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), +); \ No newline at end of file diff --git a/firka/lib/ui/widgets/circular_progress_indicator.dart b/firka/lib/ui/widgets/circular_progress_indicator.dart new file mode 100644 index 0000000..519bc4a --- /dev/null +++ b/firka/lib/ui/widgets/circular_progress_indicator.dart @@ -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 + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 500), + vsync: this, + ); + _animation = Tween(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; + } +} diff --git a/firka/lib/wear_main.dart b/firka/lib/wear_main.dart index a0d17b0..5b8038f 100644 --- a/firka/lib/wear_main.dart +++ b/firka/lib/wear_main.dart @@ -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'; @@ -11,11 +12,14 @@ import 'package:path_provider/path_provider.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:wear_plus/wear_plus.dart'; +import 'helpers/api/client/kreta_client.dart'; + late Isar isar; final GlobalKey navigatorKey = GlobalKey(); class WearAppInitialization { final Isar isar; + late KretaClient client; final int tokenCount; WearAppInitialization({ @@ -28,7 +32,7 @@ Future initDB() async { final dir = await getApplicationDocumentsDirectory(); return Isar.open( - [TokenModelSchema], + [TokenModelSchema, GenericCacheModelSchema, TimetableCacheModelSchema], inspector: true, directory: dir.path, ); @@ -42,6 +46,16 @@ Future 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; } @@ -49,22 +63,10 @@ void wearMain(MethodChannel platform) async { WakelockPlus.disable(); // 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 {