1
0
forked from firka/firka

fix wearos pairing and syncing

This commit is contained in:
2026-03-02 14:50:14 +01:00
parent 9f36569d2a
commit 5d5c3c4c6f
13 changed files with 503 additions and 149 deletions

View File

@@ -51,6 +51,9 @@ class AppInitialization {
late KretaClient client;
List<TokenModel> tokens;
bool hasWatchListener = false;
/// Set by the wear pairing modal; called when watch sends init_done or sync_done to dismiss the sheet.
void Function()? dismissWearPairingSheet;
Uint8List? profilePicture;
SettingsStore settings;
ThemeCubit? themeCubit;

View File

@@ -111,6 +111,16 @@ class _InitializationScreenState extends State<InitializationScreen> {
}
});
}
break;
case "init_done":
case "sync_done":
final ctx = navigatorKey.currentContext;
if (ctx != null && ctx.mounted) {
ScaffoldMessenger.of(ctx).hideCurrentSnackBar();
}
initData.dismissWearPairingSheet?.call();
initData.dismissWearPairingSheet = null;
break;
}
};
@@ -120,6 +130,12 @@ class _InitializationScreenState extends State<InitializationScreen> {
if (msg['id'] == 'request_sync' &&
initDone &&
isWearOsSupportEnabled()) {
final ctx = navigatorKey.currentContext;
if (ctx != null && ctx.mounted) {
ScaffoldMessenger.of(ctx).showSnackBar(
SnackBar(content: Text(initData.l10n.wear_syncing)),
);
}
await WatchSyncHelper.runWearSyncInForeground(
initData.client,
);

View File

@@ -589,9 +589,12 @@ class WatchSyncHelper {
}
/// Send a fire-and-forget message to Watch via WatchSessionManager (iOS) or watch_connectivity (Android).
/// On Android the payload is sent as a JSON string for reliable transport.
static Future<void> sendMessageToWatch(Map<String, dynamic> message) async {
if (Platform.isAndroid) {
await _watchConnectivityAndroid.sendMessage(message);
await _watchConnectivityAndroid.sendMessage(<String, dynamic>{
'data': jsonEncode(message),
});
return;
}
if (!Platform.isIOS) return;
@@ -669,9 +672,14 @@ class WatchSyncHelper {
/// Stream of messages from the watch (Android: watch_connectivity). Use for request_sync etc.
static Stream<Map<String, dynamic>> get watchMessageStream {
if (!Platform.isAndroid) return const Stream.empty();
return _watchConnectivityAndroid.messageStream.map(
(m) => Map<String, dynamic>.from(m),
);
return _watchConnectivityAndroid.messageStream.map((m) {
final map = Map<String, dynamic>.from(m);
final data = map['data'];
if (data is String) {
return jsonDecode(data) as Map<String, dynamic>;
}
return map;
});
}
static Future<void> sendTokenToWatch() async {

View File

@@ -1,3 +1,4 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/services.dart';
@@ -55,7 +56,9 @@ void wearSyncBackgroundEntrypoint() {
if (payload == null) return null;
await writeWearSyncCache(cachePath, payload);
final wc = WatchConnectivity();
await wc.sendMessage(<String, dynamic>{'id': 'sync_data', ...payload});
await wc.sendMessage(<String, dynamic>{
'data': jsonEncode(<String, dynamic>{'id': 'sync_data', ...payload}),
});
return true;
} catch (e, st) {
debugPrint('[WearSyncBackground] Error: $e $st');

View File

@@ -0,0 +1,109 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:watch_connectivity/watch_connectivity.dart';
import 'package:firka/app/app_state.dart';
import 'package:firka/api/client/kreta_client.dart';
import 'package:firka/services/wear_sync_cache.dart';
/// Helper for Wear OS ↔ phone communication (Android only).
/// Handles sync service, watch_connectivity messages, and sending data to the watch.
class WearSyncHelper {
WearSyncHelper._();
static const _wearSyncChannel = MethodChannel('app.firka/wear_sync');
static WatchConnectivity? _watchConnectivity;
static WatchConnectivity get _watchConnectivityInstance {
_watchConnectivity ??= WatchConnectivity();
return _watchConnectivity!;
}
/// Sends a fire-and-forget message to the Wear OS watch (payload sent as JSON string).
static Future<void> sendMessageToWatch(Map<String, dynamic> message) async {
if (!Platform.isAndroid) return;
await _watchConnectivityInstance.sendMessage(<String, dynamic>{
'data': jsonEncode(message),
});
}
/// Starts the Wear sync foreground service (Android only).
static Future<void> startWearSyncService(
String cachePath,
String appDirPath,
) async {
if (!Platform.isAndroid) return;
await _wearSyncChannel.invokeMethod<void>(
'startWearSyncService',
<String, dynamic>{'cachePath': cachePath, 'appDirPath': appDirPath},
);
}
/// Builds fresh sync payload, writes cache, and starts the Wear sync service (Android only).
static Future<void> startWearSyncServiceWithFreshCache(
KretaClient client,
String appDirPath,
) async {
if (!Platform.isAndroid) return;
final payload = await buildWearSyncPayload(client);
if (payload == null) return;
final path = await getWearSyncCachePath();
await writeWearSyncCache(path, payload);
await startWearSyncService(path, appDirPath);
}
/// Sets the method call handler for getLocalizedString (Android). Call once when initData is ready.
static void setWearSyncMethodCallHandler() {
if (!Platform.isAndroid) return;
_wearSyncChannel.setMethodCallHandler((MethodCall call) async {
if (call.method == 'getLocalizedString') {
final key = call.arguments as String?;
return getLocalizedString(key);
}
return null;
});
}
/// Returns the localized string for [key] from l10n. Used by Kotlin for notification title/text.
static String? getLocalizedString(String? key) {
if (key == null || !initDone) return null;
switch (key) {
case 'wearSyncNotificationTitle':
return initData.l10n.wearSyncNotificationTitle;
case 'wearSyncNotificationText':
return initData.l10n.wearSyncNotificationText;
default:
return null;
}
}
/// Stops the Wear sync foreground service (Android only).
static Future<void> stopWearSyncService() async {
if (!Platform.isAndroid) return;
await _wearSyncChannel.invokeMethod<void>('stopWearSyncService');
}
/// Runs sync in foreground: fetches timetable + grades, writes cache, sends sync_data to watch.
static Future<void> runWearSyncInForeground(KretaClient client) async {
if (!Platform.isAndroid) return;
final payload = await buildWearSyncPayload(client);
if (payload == null) return;
final path = await getWearSyncCachePath();
await writeWearSyncCache(path, payload);
await sendMessageToWatch(<String, dynamic>{'id': 'sync_data', ...payload});
}
/// Stream of messages from the Wear OS watch. Empty when not on Android.
static Stream<Map<String, dynamic>> get watchMessageStream {
if (!Platform.isAndroid) return const Stream.empty();
return _watchConnectivityInstance.messageStream.map((m) {
final map = Map<String, dynamic>.from(m);
final data = map['data'];
if (data is String) {
return jsonDecode(data) as Map<String, dynamic>;
}
return map;
});
}
}

View File

@@ -1,10 +1,10 @@
import 'package:firka/ui/components/firka_card.dart';
import 'package:firka/app/app_state.dart';
import 'package:firka/services/watch_sync_helper.dart';
import 'package:firka/services/wear_sync_cache.dart';
import 'package:firka/app/app_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:firka/ui/components/firka_card.dart';
import 'package:firka/ui/theme/style.dart';
void showWearBottomSheet(
@@ -27,61 +27,145 @@ void showWearBottomSheet(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.47,
),
builder: (BuildContext context) {
return Stack(
children: [
Positioned.fill(
child: GestureDetector(
onTap: () => Navigator.pop(context),
behavior: HitTestBehavior.opaque,
child: Container(color: Colors.transparent),
),
builder: (BuildContext context) =>
_WearPairSheetContent(data: data, model: model, payload: payload),
);
}
class _WearPairSheetContent extends StatefulWidget {
const _WearPairSheetContent({
required this.data,
required this.model,
required this.payload,
});
final AppInitialization data;
final String model;
final Map<String, dynamic> payload;
@override
State<_WearPairSheetContent> createState() => _WearPairSheetContentState();
}
class _WearPairSheetContentState extends State<_WearPairSheetContent> {
bool _syncing = false;
@override
void initState() {
super.initState();
widget.data.dismissWearPairingSheet = () {
if (mounted) Navigator.of(context).pop();
};
}
@override
void dispose() {
widget.data.dismissWearPairingSheet = null;
super.dispose();
}
void _onPairTap() {
setState(() => _syncing = true);
final m = widget.data.client.model;
WatchSyncHelper.sendMessageToWatch({
'id': 'init_data',
'auth': {
'studentId': m.studentId,
'studentIdNorm': m.studentIdNorm,
'iss': m.iss,
'idToken': m.idToken,
'accessToken': m.accessToken,
'refreshToken': m.refreshToken,
'expiryDate': m.expiryDate!.millisecondsSinceEpoch,
},
'lastSyncAt': widget.payload['lastSyncAt'],
'timetable': widget.payload['timetable'],
'grades': widget.payload['grades'],
});
}
@override
Widget build(BuildContext context) {
final data = widget.data;
final model = widget.model;
return Stack(
children: [
Positioned.fill(
child: GestureDetector(
onTap: () => Navigator.pop(context),
behavior: HitTestBehavior.opaque,
child: Container(color: Colors.transparent),
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
decoration: BoxDecoration(
color: appStyle.colors.card,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
SvgPicture.asset("assets/images/wear_pair.svg"),
SizedBox(height: 32),
Center(
child: Text(
data.l10n.pairing,
style: appStyle.fonts.H_14px.apply(
color: appStyle.colors.secondary,
),
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
decoration: BoxDecoration(
color: appStyle.colors.card,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset("assets/images/wear_pair.svg"),
const SizedBox(height: 32),
Center(
child: Text(
data.l10n.pairing,
style: appStyle.fonts.H_14px.apply(
color: appStyle.colors.secondary,
),
),
Center(
),
Center(
child: Text(
model,
style: appStyle.fonts.H_H2.apply(
color: appStyle.colors.textPrimary,
),
),
),
Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6),
child: Text(
model,
style: appStyle.fonts.H_H2.apply(
data.l10n.pairing_description,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
textAlign: TextAlign.center,
),
),
Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6),
child: Text(
data.l10n.pairing_description,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
const SizedBox(height: 32),
if (_syncing)
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
children: [
const SizedBox(
width: 32,
height: 32,
child: CircularProgressIndicator(strokeWidth: 2),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
Text(
data.l10n.wear_syncing,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
),
],
),
),
SizedBox(height: 32),
)
else
SizedBox(
width: MediaQuery.of(context).size.width / 1.1,
child: GestureDetector(
onTap: _onPairTap,
child: FirkaCard(
left: [],
center: [
@@ -94,28 +178,10 @@ void showWearBottomSheet(
],
color: appStyle.colors.accent,
),
onTap: () {
final m = data.client.model;
WatchSyncHelper.sendMessageToWatch({
'id': 'init_data',
'auth': {
'studentId': m.studentId,
'studentIdNorm': m.studentIdNorm,
'iss': m.iss,
'idToken': m.idToken,
'accessToken': m.accessToken,
'refreshToken': m.refreshToken,
'expiryDate':
m.expiryDate!.millisecondsSinceEpoch,
},
'lastSyncAt': payload['lastSyncAt'],
'timetable': payload['timetable'],
'grades': payload['grades'],
});
},
),
),
SizedBox(height: 8),
if (!_syncing) ...[
const SizedBox(height: 8),
SizedBox(
width: MediaQuery.of(context).size.width / 1.1,
child: GestureDetector(
@@ -131,18 +197,16 @@ void showWearBottomSheet(
],
color: appStyle.colors.buttonSecondaryFill,
),
onTap: () {
Navigator.pop(context);
},
onTap: () => Navigator.pop(context),
),
),
],
),
],
),
),
),
],
);
},
);
),
],
);
}
}

View File

@@ -80,7 +80,7 @@ AppLocalizations getLang() {
Future<WearAppInitialization> initializeApp() async {
final isar = await initDB();
final syncStore = WearSyncStore();
final syncStore = WearSyncStore(isar);
await syncStore.load();
const channel = MethodChannel("firka.app/main");

View File

@@ -2,7 +2,15 @@ import 'package:isar_community/isar.dart';
part 'generic_cache_model.g.dart';
enum CacheId { getStudent, getNoticeBoard, getGrades, getOmissions, getTests }
enum CacheId {
getStudent,
getNoticeBoard,
getGrades,
getOmissions,
getTests,
wearSyncMetadata,
wearSyncTimetable,
}
@collection
class GenericCacheModel {

View File

@@ -1,14 +1,20 @@
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:path_provider/path_provider.dart';
import 'package:isar_community/isar.dart';
import 'package:kreta_api/kreta_api.dart';
const String _syncFileName = 'wear_sync_data.json';
import 'package:firka_wear/data/models/generic_cache_model.dart';
import 'package:firka_wear/data/models/token_model.dart';
/// Persists and loads synced data (timetable, grades, lastSyncAt) from the phone.
/// Uses [GenericCacheModel] for metadata, timetable (single JSON blob), and grades.
class WearSyncStore {
WearSyncStore(this.isar);
final Isar isar;
List<Lesson> _timetable = [];
List<Grade> _grades = [];
DateTime? _lastSyncAt;
@@ -23,29 +29,53 @@ class WearSyncStore {
return DateTime.now().difference(_lastSyncAt!) > const Duration(hours: 1);
}
Future<String> _getSyncFilePath() async {
final dir = await getApplicationDocumentsDirectory();
return '${dir.path}/$_syncFileName';
static int _genericCacheKey(int studentIdNorm, CacheId id) {
return (studentIdNorm + (id.index + 1) * pow(10, 11)) as int;
}
Future<int?> _getStudentIdNorm() async {
final token = await isar.tokenModels.where().findFirst();
return token?.studentIdNorm;
}
Future<void> load() async {
try {
final path = await _getSyncFilePath();
final file = File(path);
if (!await file.exists()) return;
final json =
jsonDecode(await file.readAsString()) as Map<String, dynamic>;
_lastSyncAt = json['lastSyncAt'] != null
? DateTime.parse(json['lastSyncAt'] as String)
: null;
final rawTimetable = json['timetable'] as List<dynamic>? ?? [];
_timetable = rawTimetable
.map((e) => Lesson.fromJson(Map<String, dynamic>.from(e as Map)))
.toList();
final rawGrades = json['grades'] as List<dynamic>? ?? [];
_grades = rawGrades
.map((e) => Grade.fromJson(Map<String, dynamic>.from(e as Map)))
.toList();
final studentIdNorm = await _getStudentIdNorm();
if (studentIdNorm == null) return;
final metadataKey = _genericCacheKey(
studentIdNorm,
CacheId.wearSyncMetadata,
);
final metadataCache = await isar.genericCacheModels.get(metadataKey);
if (metadataCache?.cacheData != null) {
final meta =
jsonDecode(metadataCache!.cacheData!) as Map<String, dynamic>;
_lastSyncAt = meta['lastSyncAt'] != null
? DateTime.parse(meta['lastSyncAt'] as String)
: null;
}
final timetableKey = _genericCacheKey(
studentIdNorm,
CacheId.wearSyncTimetable,
);
final timetableCache = await isar.genericCacheModels.get(timetableKey);
if (timetableCache?.cacheData != null) {
final raw = jsonDecode(timetableCache!.cacheData!) as List<dynamic>;
_timetable = raw
.map((e) => Lesson.fromJson(Map<String, dynamic>.from(e as Map)))
.toList();
}
final gradesKey = _genericCacheKey(studentIdNorm, CacheId.getGrades);
final gradesCache = await isar.genericCacheModels.get(gradesKey);
if (gradesCache?.cacheData != null) {
final raw = jsonDecode(gradesCache!.cacheData!) as List<dynamic>;
_grades = raw
.map((e) => Grade.fromJson(Map<String, dynamic>.from(e as Map)))
.toList();
}
} catch (_) {}
}
@@ -54,25 +84,54 @@ class WearSyncStore {
required List<Lesson> timetable,
required List<Grade> grades,
}) async {
final studentIdNorm = await _getStudentIdNorm();
if (studentIdNorm == null) return;
_lastSyncAt = lastSyncAt;
_timetable = timetable;
_grades = grades;
final path = await _getSyncFilePath();
final file = File(path);
await file.writeAsString(
jsonEncode({
'lastSyncAt': lastSyncAt?.toUtc().toIso8601String(),
'timetable': timetable.map((e) => e.toJson()).toList(),
'grades': grades.map((e) => e.toJson()).toList(),
}),
);
await isar.writeTxn(() async {
final metadataKey = _genericCacheKey(
studentIdNorm,
CacheId.wearSyncMetadata,
);
await isar.genericCacheModels.put(
GenericCacheModel()
..cacheKey = metadataKey
..cacheData = jsonEncode({
'lastSyncAt': lastSyncAt?.toUtc().toIso8601String(),
}),
);
final timetableKey = _genericCacheKey(
studentIdNorm,
CacheId.wearSyncTimetable,
);
await isar.genericCacheModels.put(
GenericCacheModel()
..cacheKey = timetableKey
..cacheData = jsonEncode(timetable.map((e) => e.toJson()).toList()),
);
final gradesKey = _genericCacheKey(studentIdNorm, CacheId.getGrades);
await isar.genericCacheModels.put(
GenericCacheModel()
..cacheKey = gradesKey
..cacheData = jsonEncode(grades.map((e) => e.toJson()).toList()),
);
});
}
/// Returns lessons that fall on [date] (by date string or start date).
/// Returns lessons that fall on [date] (compare by calendar day via lesson start).
List<Lesson> getLessonsForDate(DateTime date) {
final dateStr =
'${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
return _timetable.where((l) => l.date == dateStr).toList()
final y = date.year;
final m = date.month;
final d = date.day;
return _timetable
.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

@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:flutter/material.dart';
@@ -39,6 +40,7 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
final platform = MethodChannel('firka.app/main');
final watch = WatchConnectivity();
StreamSubscription? _messageSub;
bool _syncing = false;
bool disposed = false;
@@ -46,8 +48,14 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
void initState() {
super.initState();
now = timeNow();
today = data.syncStore.getLessonsForDate(now);
init = data.syncStore.timetable.isNotEmpty;
_messageSub = watch.messageStream.listen((e) {
final msg = Map<String, dynamic>.from(e);
final raw = Map<String, dynamic>.from(e);
final data = raw['data'];
final msg = data is String
? jsonDecode(data) as Map<String, dynamic>
: raw;
if (msg['id'] == 'sync_data') _onSyncData(msg);
});
timer = Timer.periodic(Duration(seconds: 1), (timer) async {
@@ -59,33 +67,44 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
}
void _onSyncData(Map<String, dynamic> msg) async {
final lastSyncAt = msg['lastSyncAt'] != null
? DateTime.parse(msg['lastSyncAt'] as String)
: null;
final rawTimetable = msg['timetable'] as List<dynamic>? ?? [];
final timetable = rawTimetable
.map((e) => Lesson.fromJson(Map<String, dynamic>.from(e as Map)))
.toList();
final rawGrades = msg['grades'] as List<dynamic>? ?? [];
final grades = rawGrades
.map((e) => Grade.fromJson(Map<String, dynamic>.from(e as Map)))
.toList();
await data.syncStore.save(
lastSyncAt: lastSyncAt,
timetable: timetable,
grades: grades,
);
if (disposed) return;
setState(() {
now = timeNow();
today = data.syncStore.getLessonsForDate(now);
});
setState(() => _syncing = true);
try {
final lastSyncAt = msg['lastSyncAt'] != null
? DateTime.parse(msg['lastSyncAt'] as String)
: null;
final rawTimetable = msg['timetable'] as List<dynamic>? ?? [];
final timetable = rawTimetable
.map((e) => Lesson.fromJson(Map<String, dynamic>.from(e as Map)))
.toList();
final rawGrades = msg['grades'] as List<dynamic>? ?? [];
final grades = rawGrades
.map((e) => Grade.fromJson(Map<String, dynamic>.from(e as Map)))
.toList();
await data.syncStore.save(
lastSyncAt: lastSyncAt,
timetable: timetable,
grades: grades,
);
watch.sendMessage(<String, dynamic>{
'data': jsonEncode(<String, dynamic>{'id': 'sync_done'}),
});
if (disposed) return;
setState(() {
now = timeNow();
today = data.syncStore.getLessonsForDate(now);
});
} finally {
if (!disposed) setState(() => _syncing = false);
}
}
Future<void> initStateAsync() async {
now = timeNow();
if (data.syncStore.needsSync) {
watch.sendMessage({'id': 'request_sync'});
watch.sendMessage(<String, dynamic>{
'data': jsonEncode(<String, dynamic>{'id': 'request_sync'}),
});
}
await data.syncStore.load();
if (disposed) return;
@@ -383,6 +402,31 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
],
),
),
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,6 +1,7 @@
// ignore_for_file: avoid_print
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:kreta_api/kreta_api.dart';
@@ -8,7 +9,6 @@ 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/core/extensions.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';
@@ -29,6 +29,7 @@ class _WearLoginScreen extends State<WearLoginScreen> {
bool isReachable = false;
bool isMessageSending = false;
bool isMessageSent = false;
bool isSyncing = false;
final watch = WatchConnectivity();
late Timer connectionTimer;
@@ -37,15 +38,21 @@ class _WearLoginScreen extends State<WearLoginScreen> {
super.initState();
watch.messageStream.listen((e) {
var msg = e.entries.toMap();
final raw = Map<String, dynamic>.from(e);
final data = raw['data'];
final msg = data is String
? jsonDecode(data) as Map<String, dynamic>
: raw;
var id = msg["id"];
debugPrint("[Phone -> Watch]: $id");
switch (id) {
case "init_data":
{
() async {
() async {
if (!mounted) return;
setState(() => isSyncing = true);
try {
final auth = msg["auth"] as Map<dynamic, dynamic>?;
if (auth == null) return;
final tokenModel = TokenModel.fromValues(
@@ -80,6 +87,9 @@ class _WearLoginScreen extends State<WearLoginScreen> {
timetable: timetable,
grades: grades,
);
watch.sendMessage(<String, dynamic>{
'data': jsonEncode(<String, dynamic>{'id': 'init_done'}),
});
if (!mounted) return;
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
@@ -87,8 +97,11 @@ class _WearLoginScreen extends State<WearLoginScreen> {
),
(route) => false,
);
}();
}
} finally {
if (mounted) setState(() => isSyncing = false);
}
}();
break;
}
});
@@ -100,7 +113,9 @@ class _WearLoginScreen extends State<WearLoginScreen> {
isMessageSending = true;
debugPrint("[Watch -> Phone]: ping");
watch.sendMessage({'id': 'ping'});
watch.sendMessage(<String, dynamic>{
'data': jsonEncode(<String, dynamic>{'id': 'ping'}),
});
}
setState(() {
@@ -116,6 +131,27 @@ class _WearLoginScreen extends State<WearLoginScreen> {
return (<Widget>[], 60);
}
if (isSyncing) {
return (
<Widget>[
const SizedBox(
width: 32,
height: 32,
child: CircularProgressIndicator(strokeWidth: 2),
),
const SizedBox(height: 12),
Text(
widget.data.l10n.wear_syncing,
textAlign: TextAlign.center,
style: wearStyle.fonts.B_16R.apply(
color: wearStyle.colors.textPrimary,
),
),
],
60,
);
}
if (!isPaired) {
return (
<Widget>[
@@ -158,9 +194,11 @@ class _WearLoginScreen extends State<WearLoginScreen> {
ElevatedButton(
onPressed: () async {
debugPrint("[Watch -> Phone]: ping");
watch.sendMessage({
'id': 'ping',
'model': initData.devInfo.model,
watch.sendMessage(<String, dynamic>{
'data': jsonEncode(<String, dynamic>{
'id': 'ping',
'model': initData.devInfo.model,
}),
});
},
// TODO: This is a placeholder, style this properly
@@ -202,7 +240,9 @@ class _WearLoginScreen extends State<WearLoginScreen> {
ElevatedButton(
onPressed: () async {
debugPrint("[Watch -> Phone]: ping");
watch.sendMessage({'id': 'ping'});
watch.sendMessage(<String, dynamic>{
'data': jsonEncode(<String, dynamic>{'id': 'ping'}),
});
},
// TODO: This is a placeholder, style this properly
style: ButtonStyle(