wearos home screen

This commit is contained in:
4831c0 2025-04-01 12:46:22 +02:00
parent 442ba79445
commit 47ba1a1bba
Signed by: 4831c0
GPG Key ID: 3F97EDDF98E45AA4
4 changed files with 355 additions and 32 deletions

View File

@ -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:firka/wear_main.dart';
import 'package:flutter/material.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; final WearAppInitialization data;
const WearHomeScreen(this.data, {super.key}); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var body = buildBody(context);
return Scaffold( return Scaffold(
appBar: AppBar( backgroundColor: defaultColors.ambientBackgroundColor,
title: const Text('Stub'),
centerTitle: true,
),
body: Center( body: Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: body,
const Text(
'Home',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
],
), ),
), ),
); );
} }
@override
void dispose() {
super.dispose();
timer?.cancel();
}
} }

30
firka/lib/ui/colors.dart Normal file
View 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),
);

View 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;
}
}

View File

@ -1,7 +1,8 @@
import 'dart:async'; 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/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/home/home_screen.dart';
import 'package:firka/screens/wear/login/login_screen.dart'; import 'package:firka/screens/wear/login/login_screen.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -10,11 +11,14 @@ import 'package:isar/isar.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:wear_plus/wear_plus.dart'; import 'package:wear_plus/wear_plus.dart';
import 'helpers/api/client/kreta_client.dart';
late Isar isar; late Isar isar;
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
class WearAppInitialization { class WearAppInitialization {
final Isar isar; final Isar isar;
late KretaClient client;
final int tokenCount; final int tokenCount;
WearAppInitialization({ WearAppInitialization({
@ -27,7 +31,7 @@ Future<Isar> initDB() async {
final dir = await getApplicationDocumentsDirectory(); final dir = await getApplicationDocumentsDirectory();
return Isar.open( return Isar.open(
[TokenModelSchema], [TokenModelSchema, GenericCacheModelSchema, TimetableCacheModelSchema],
inspector: true, inspector: true,
directory: dir.path, directory: dir.path,
); );
@ -41,27 +45,25 @@ Future<WearAppInitialization> initializeApp() async {
tokenCount: await isar.tokenModels.count() 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; return init;
} }
void wearMain(MethodChannel platform) async { void wearMain(MethodChannel platform) async {
// TODO: fix the error handling currently not pushing to the error page // TODO: fix the error handling currently not pushing to the error page
runZonedGuarded(() async { WidgetsFlutterBinding.ensureInitialized();
WidgetsFlutterBinding.ensureInitialized();
// Run App Initialization // Run App Initialization
runApp(WearInitializationScreen()); runApp(WearInitializationScreen());
}, (error, stackTrace) {
debugPrint('Caught error: $error');
debugPrint('Stack trace: $stackTrace');
navigatorKey.currentState?.push(
MaterialPageRoute(
builder: (context) => WearErrorPage(exception: error.toString()),
),
);
});
} }
class WearInitializationScreen extends StatelessWidget { class WearInitializationScreen extends StatelessWidget {