firka_wear: centralize app state, add logging, remove unused routes, add Bloc for sync state

This commit is contained in:
2026-03-02 15:48:11 +01:00
parent 613db488b1
commit b986d8b660
9 changed files with 238 additions and 178 deletions

View File

@@ -0,0 +1,45 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:isar_community/isar.dart';
import 'package:logging/logging.dart';
import 'package:firka_wear/l10n/app_localizations.dart';
import 'package:firka_wear/services/wear_sync_store.dart';
late final Logger logger;
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
late WearAppInitialization initData;
bool initDone = false;
final dio = Dio();
class DeviceInfo {
String model;
String versionRelease;
String versionSdkInt;
DeviceInfo(this.model, this.versionRelease, this.versionSdkInt);
@override
String toString() {
return "DeviceInfo(model = \"$model\", versionRelease = \"$versionRelease\""
", versionSdkInt = \"$versionSdkInt\"";
}
}
class WearAppInitialization {
final Isar isar;
final WearSyncStore syncStore;
final int tokenCount;
final AppLocalizations l10n;
final DeviceInfo devInfo;
WearAppInitialization({
required this.isar,
required this.syncStore,
required this.tokenCount,
required this.l10n,
required this.devInfo,
});
}

View File

@@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
import 'package:isar_community/isar.dart';
import 'package:path_provider/path_provider.dart';
import 'package:firka_wear/app/app_state.dart';
import 'package:firka_wear/data/models/generic_cache_model.dart';
import 'package:firka_wear/data/models/homework_cache_model.dart';
import 'package:firka_wear/data/models/timetable_cache_model.dart';
@@ -12,42 +13,9 @@ import 'package:firka_wear/l10n/app_localizations.dart';
import 'package:firka_wear/l10n/app_localizations_de.dart';
import 'package:firka_wear/l10n/app_localizations_en.dart';
import 'package:firka_wear/l10n/app_localizations_hu.dart';
import 'package:flutter/material.dart';
import 'package:firka_wear/services/wear_sync_store.dart';
Isar? isarInit;
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
class DeviceInfo {
String model;
String versionRelease;
String versionSdkInt;
DeviceInfo(this.model, this.versionRelease, this.versionSdkInt);
@override
String toString() {
return "DeviceInfo(model = \"$model\", versionRelease = \"$versionRelease\""
", versionSdkInt = \"$versionSdkInt\"";
}
}
class WearAppInitialization {
final Isar isar;
final WearSyncStore syncStore;
final int tokenCount;
final AppLocalizations l10n;
final DeviceInfo devInfo;
WearAppInitialization({
required this.isar,
required this.syncStore,
required this.tokenCount,
required this.l10n,
required this.devInfo,
});
}
Future<Isar> initDB() async {
if (isarInit != null) return isarInit!;

View File

@@ -1,8 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:wear_plus/wear_plus.dart';
import 'package:firka_wear/app/app_state.dart';
import 'package:firka_wear/app/initialization.dart';
import 'package:firka_wear/core/bloc/wear_sync_cubit.dart';
import 'package:firka_wear/l10n/app_localizations.dart';
import 'package:firka_wear/ui/theme/style.dart';
import 'package:firka_wear/ui/wear/screens/home/home_screen.dart';
@@ -44,37 +47,33 @@ class WearInitializationScreen extends StatelessWidget {
);
}
Widget screen;
assert(snapshot.data != null);
var data = snapshot.data!;
initData = snapshot.data!;
initDone = true;
if (snapshot.data!.tokenCount == 0) {
screen = WearLoginScreen(data, key: ValueKey('wearLoginScreen'));
} else {
screen = WearHomeScreen(data, key: ValueKey('wearHomeScreen'));
}
final data = initData;
final screen = data.tokenCount == 0
? WearLoginScreen(data, key: ValueKey('wearLoginScreen'))
: WearHomeScreen(data, key: ValueKey('wearHomeScreen'));
return MaterialApp(
key: ValueKey('firkaWearApp'),
title: 'Firka',
navigatorKey: navigatorKey,
theme: ThemeData(
primarySwatch: Colors.lightGreen,
visualDensity: VisualDensity.adaptivePlatformDensity,
return BlocProvider(
create: (_) => WearSyncCubit(),
child: MaterialApp(
key: ValueKey('firkaWearApp'),
title: 'Firka',
navigatorKey: navigatorKey,
theme: ThemeData(
primarySwatch: Colors.lightGreen,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
localizationsDelegates: [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
home: screen,
),
localizationsDelegates: [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
home: screen,
routes: {
'/login': (context) =>
WearLoginScreen(data, key: ValueKey('wearLoginScreen')),
'/home': (context) =>
WearHomeScreen(data, key: ValueKey('wearHomeScreen')),
},
);
}

View File

@@ -0,0 +1,15 @@
import 'package:flutter_bloc/flutter_bloc.dart';
class WearSyncState {
final bool isSyncing;
const WearSyncState({this.isSyncing = false});
}
class WearSyncCubit extends Cubit<WearSyncState> {
WearSyncCubit() : super(const WearSyncState());
void setSyncing(bool value) {
emit(WearSyncState(isSyncing: value));
}
}

View File

@@ -1,14 +1,15 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:firka_wear/app/app_state.dart';
import 'package:firka_wear/app/initialization_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:logging/logging.dart';
import 'package:permission_handler/permission_handler.dart';
final dio = Dio();
void main() async {
logger = Logger('FirkaWear');
dio.options.connectTimeout = Duration(seconds: 5);
dio.options.receiveTimeout = Duration(seconds: 3);
dio.options.validateStatus = (status) => status != null && status < 500;

View File

@@ -129,8 +129,9 @@ class WearSyncStore {
final m = date.month;
final d = date.day;
return _timetable
.where((l) =>
l.start.year == y && l.start.month == m && l.start.day == d)
.where(
(l) => l.start.year == y && l.start.month == m && l.start.day == d,
)
.toList()
..sort((a, b) => a.start.compareTo(b.start));
}

View File

@@ -5,12 +5,14 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_arc_text/flutter_arc_text.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kreta_api/kreta_api.dart';
import 'package:watch_connectivity/watch_connectivity.dart';
import 'package:wear_plus/wear_plus.dart';
import 'package:firka_wear/app/initialization.dart';
import 'package:firka_wear/app/app_state.dart';
import 'package:firka_wear/core/bloc/wear_sync_cubit.dart';
import 'package:firka_wear/core/debug_helper.dart';
import 'package:firka_wear/core/extensions.dart';
import 'package:firka_wear/l10n/app_localizations.dart';
@@ -40,10 +42,16 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
final platform = MethodChannel('firka.app/main');
final watch = WatchConnectivity();
StreamSubscription? _messageSub;
bool _syncing = false;
WearSyncCubit? _syncCubit;
bool disposed = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_syncCubit ??= context.read<WearSyncCubit>();
}
@override
void initState() {
super.initState();
@@ -68,7 +76,7 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
void _onSyncData(Map<String, dynamic> msg) async {
if (disposed) return;
setState(() => _syncing = true);
_syncCubit?.setSyncing(true);
try {
final lastSyncAt = msg['lastSyncAt'] != null
? DateTime.parse(msg['lastSyncAt'] as String)
@@ -95,7 +103,7 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
today = data.syncStore.getLessonsForDate(now);
});
} finally {
if (!disposed) setState(() => _syncing = false);
if (!disposed) _syncCubit?.setSyncing(false);
}
}
@@ -355,80 +363,84 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
);
}
return Scaffold(
backgroundColor: mode == WearMode.active
? wearStyle.colors.background
: wearStyle.colors.backgroundAmoled,
body: Stack(
children: [
Center(child: titleBar),
Center(
child: Column(
children: [
WatchShape(
builder: (context, shape, child) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[child!],
);
},
child: AmbientMode(
builder: (context, mode, child) {
if (this.mode != mode) {
Timer(Duration(milliseconds: 100), () {
setState(() {
this.mode = mode;
});
});
}
return BlocBuilder<WearSyncCubit, WearSyncState>(
builder: (context, syncState) {
return Scaffold(
backgroundColor: mode == WearMode.active
? wearStyle.colors.background
: wearStyle.colors.backgroundAmoled,
body: Stack(
children: [
Center(child: titleBar),
Center(
child: Column(
children: [
WatchShape(
builder: (context, shape, child) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[child!],
);
},
child: AmbientMode(
builder: (context, mode, child) {
if (this.mode != mode) {
Timer(Duration(milliseconds: 100), () {
setState(() {
this.mode = mode;
});
});
}
var (body, padding) = buildBody(context, mode);
var (body, padding) = buildBody(context, mode);
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
padding: EdgeInsets.only(top: padding),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [...body],
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
padding: EdgeInsets.only(top: padding),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [...body],
),
),
],
);
},
),
),
],
),
),
if (syncState.isSyncing)
Positioned.fill(
child: Container(
color: wearStyle.colors.background.withValues(alpha: 0.8),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
width: 32,
height: 32,
child: CircularProgressIndicator(strokeWidth: 2),
),
SizedBox(height: 12.h),
Text(
AppLocalizations.of(context)!.wear_syncing,
style: wearStyle.fonts.B_16R.apply(
color: wearStyle.colors.textPrimary,
),
),
],
);
},
),
),
),
),
],
),
],
),
if (_syncing)
Positioned.fill(
child: Container(
color: wearStyle.colors.background.withValues(alpha: 0.8),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
width: 32,
height: 32,
child: CircularProgressIndicator(strokeWidth: 2),
),
SizedBox(height: 12.h),
Text(
AppLocalizations.of(context)!.wear_syncing,
style: wearStyle.fonts.B_16R.apply(
color: wearStyle.colors.textPrimary,
),
),
],
),
),
),
),
],
),
);
},
);
}

View File

@@ -1,20 +1,20 @@
// ignore_for_file: avoid_print
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kreta_api/kreta_api.dart';
import 'package:watch_connectivity/watch_connectivity.dart';
import 'package:wear_plus/wear_plus.dart';
import 'package:firka_wear/app/initialization.dart';
import 'package:firka_wear/app/app_state.dart' as app_state;
import 'package:firka_wear/core/bloc/wear_sync_cubit.dart';
import 'package:firka_wear/data/models/token_model.dart';
import 'package:firka_wear/ui/theme/style.dart';
import 'package:firka_wear/ui/wear/screens/home/home_screen.dart';
class WearLoginScreen extends StatefulWidget {
final WearAppInitialization data;
final app_state.WearAppInitialization data;
const WearLoginScreen(this.data, {super.key});
@override
@@ -22,16 +22,22 @@ class WearLoginScreen extends StatefulWidget {
}
class _WearLoginScreen extends State<WearLoginScreen> {
WearAppInitialization get initData => widget.data;
app_state.WearAppInitialization get initData => widget.data;
bool init = false;
bool isPaired = false;
bool isReachable = false;
bool isMessageSending = false;
bool isMessageSent = false;
bool isSyncing = false;
final watch = WatchConnectivity();
late Timer connectionTimer;
WearSyncCubit? _syncCubit;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_syncCubit ??= context.read<WearSyncCubit>();
}
@override
void initState() {
@@ -45,13 +51,13 @@ class _WearLoginScreen extends State<WearLoginScreen> {
: raw;
var id = msg["id"];
debugPrint("[Phone -> Watch]: $id");
app_state.logger.fine("[Phone -> Watch]: $id");
switch (id) {
case "init_data":
() async {
if (!mounted) return;
setState(() => isSyncing = true);
_syncCubit?.setSyncing(true);
try {
final auth = msg["auth"] as Map<dynamic, dynamic>?;
if (auth == null) return;
@@ -91,14 +97,22 @@ class _WearLoginScreen extends State<WearLoginScreen> {
'data': jsonEncode(<String, dynamic>{'id': 'init_done'}),
});
if (!mounted) return;
app_state.initData = app_state.WearAppInitialization(
isar: initData.isar,
syncStore: initData.syncStore,
tokenCount: await initData.isar.tokenModels.count(),
l10n: initData.l10n,
devInfo: initData.devInfo,
);
if (!mounted) return;
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (context) => WearHomeScreen(initData),
builder: (context) => WearHomeScreen(app_state.initData),
),
(route) => false,
);
} finally {
if (mounted) setState(() => isSyncing = false);
if (mounted) _syncCubit?.setSyncing(false);
}
}();
break;
@@ -112,7 +126,7 @@ class _WearLoginScreen extends State<WearLoginScreen> {
if (!isMessageSending) {
isMessageSending = true;
debugPrint("[Watch -> Phone]: ping");
app_state.logger.fine("[Watch -> Phone]: ping");
watch.sendMessage(<String, dynamic>{
'data': jsonEncode(<String, dynamic>{
'id': 'ping',
@@ -129,7 +143,7 @@ class _WearLoginScreen extends State<WearLoginScreen> {
});
}
(List<Widget>, double) buildBody(BuildContext context) {
(List<Widget>, double) buildBody(BuildContext context, bool isSyncing) {
if (!init) {
return (<Widget>[], 60);
}
@@ -196,7 +210,7 @@ class _WearLoginScreen extends State<WearLoginScreen> {
),
ElevatedButton(
onPressed: () async {
debugPrint("[Watch -> Phone]: ping");
app_state.logger.fine("[Watch -> Phone]: ping");
watch.sendMessage(<String, dynamic>{
'data': jsonEncode(<String, dynamic>{
'id': 'ping',
@@ -242,7 +256,7 @@ class _WearLoginScreen extends State<WearLoginScreen> {
),
ElevatedButton(
onPressed: () async {
debugPrint("[Watch -> Phone]: ping");
app_state.logger.fine("[Watch -> Phone]: ping");
watch.sendMessage(<String, dynamic>{
'data': jsonEncode(<String, dynamic>{
'id': 'ping',
@@ -290,39 +304,42 @@ class _WearLoginScreen extends State<WearLoginScreen> {
@override
Widget build(BuildContext context) {
var (body, offset) = buildBody(context);
return Scaffold(
backgroundColor: wearStyle.colors.background,
body: Center(
child: Column(
children: [
WatchShape(
builder: (context, shape, child) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Column(
return BlocBuilder<WearSyncCubit, WearSyncState>(
builder: (context, syncState) {
var (body, offset) = buildBody(context, syncState.isSyncing);
return Scaffold(
backgroundColor: wearStyle.colors.background,
body: Center(
child: Column(
children: [
WatchShape(
builder: (context, shape, child) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
padding: EdgeInsets.only(top: offset),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: body,
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
padding: EdgeInsets.only(top: offset),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: body,
),
),
],
),
child!,
],
),
child!,
],
);
},
child: SizedBox(),
);
},
child: SizedBox(),
),
],
),
],
),
),
),
);
},
);
}

View File

@@ -41,7 +41,6 @@ dependencies:
dio: ^5.8.0+1
isar_community: 3.3.0
isar_community_flutter_libs: 3.3.0
build_runner: any
path_provider: ^2.1.0
carousel_slider: ^5.0.0
dart_jsonwebtoken: ^3.2.0
@@ -58,8 +57,11 @@ dependencies:
flutter_screenutil: ^5.9.3
flutter_arc_text: ^0.6.0
flutter_svg: ^1.1.6
logging: ^1.3.0
flutter_bloc: ^9.0.0
dev_dependencies:
build_runner: any
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0