forked from firka/firka
firka_wear: local sync store, 1h rule, remove KretaClient; data from phone only
This commit is contained in:
@@ -1,519 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:firka_wear/helpers/api/model/homework.dart';
|
||||
import 'package:firka_wear/helpers/api/model/timetable.dart';
|
||||
import 'package:firka_wear/helpers/db/models/generic_cache_model.dart';
|
||||
import 'package:firka_wear/helpers/db/models/homework_cache_model.dart';
|
||||
import 'package:firka_wear/helpers/db/models/timetable_cache_model.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:isar_community/isar.dart';
|
||||
|
||||
import '../../../main.dart';
|
||||
import '../../db/models/token_model.dart';
|
||||
import '../../db/util.dart';
|
||||
import '../../debug_helper.dart';
|
||||
import '../consts.dart';
|
||||
import '../model/grade.dart';
|
||||
import '../model/notice_board.dart';
|
||||
import '../model/omission.dart';
|
||||
import '../model/student.dart';
|
||||
import '../model/test.dart';
|
||||
import '../token_grant.dart';
|
||||
|
||||
class ApiResponse<T> {
|
||||
T? response;
|
||||
int statusCode;
|
||||
String? err;
|
||||
bool cached;
|
||||
|
||||
ApiResponse(
|
||||
this.response,
|
||||
this.statusCode,
|
||||
this.err,
|
||||
this.cached,
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return "ApiResponse("
|
||||
"response: $response, "
|
||||
"statusCode: $statusCode, "
|
||||
"err: \"$err\", "
|
||||
"cached: $cached"
|
||||
")";
|
||||
}
|
||||
}
|
||||
|
||||
class KretaClient {
|
||||
bool _tokenMutex = false;
|
||||
TokenModel model;
|
||||
Isar isar;
|
||||
|
||||
KretaClient(this.model, this.isar);
|
||||
|
||||
Future<T> _mutexCallback<T>(Future<T> Function() callback) async {
|
||||
while (_tokenMutex) {
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
}
|
||||
_tokenMutex = true;
|
||||
try {
|
||||
return callback();
|
||||
} finally {
|
||||
_tokenMutex = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Response> _authReq(String method, String url, [Object? data]) async {
|
||||
var localToken = await _mutexCallback<String>(() async {
|
||||
var now = timeNow();
|
||||
|
||||
if (now.millisecondsSinceEpoch >=
|
||||
model.expiryDate!.millisecondsSinceEpoch) {
|
||||
var extended = await extendToken(model);
|
||||
var tokenModel = TokenModel.fromResp(extended);
|
||||
|
||||
await isar.writeTxn(() async {
|
||||
await isar.tokenModels.put(tokenModel);
|
||||
});
|
||||
|
||||
model = tokenModel;
|
||||
}
|
||||
|
||||
return model.accessToken!;
|
||||
});
|
||||
|
||||
final headers = <String, String>{
|
||||
// "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
"accept": "*/*",
|
||||
"user-agent": "eKretaStudent/264745 CFNetwork/1494.0.7 Darwin/23.4.0",
|
||||
"authorization": "Bearer $localToken",
|
||||
"apiKey": "21ff6c25-d1da-4a68-a811-c881a6057463"
|
||||
};
|
||||
|
||||
return await dio.get(url,
|
||||
options: Options(method: method, headers: headers), data: data);
|
||||
}
|
||||
|
||||
Future<(dynamic, int)> _authJson(String method, String url,
|
||||
[Object? data]) async {
|
||||
var resp = await _authReq(method, url, data);
|
||||
|
||||
return (resp.data, resp.statusCode!);
|
||||
}
|
||||
|
||||
Future<(dynamic, int, Object?, bool)> _cachingGet(
|
||||
CacheId id, String url, bool forceCache) async {
|
||||
// it would be *ideal* to use xor and left shift here, however
|
||||
// binary operations seem to round the number down to
|
||||
// 32 bits for some reason???
|
||||
var cacheKey = model.studentIdNorm! + ((id.index + 1) * pow(10, 11));
|
||||
var cache = await isar.genericCacheModels.get(cacheKey as int);
|
||||
|
||||
dynamic resp;
|
||||
int statusCode;
|
||||
try {
|
||||
if (forceCache && cache != null) {
|
||||
return (jsonDecode(cache.cacheData!), 200, null, true);
|
||||
}
|
||||
(resp, statusCode) = await _authJson("GET", url);
|
||||
|
||||
if (statusCode >= 400) {
|
||||
if (cache != null) {
|
||||
return (jsonDecode(cache.cacheData!), statusCode, null, true);
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
if (cache != null) {
|
||||
return (jsonDecode(cache.cacheData!), 0, ex, true);
|
||||
} else {
|
||||
return (null, 0, ex, false);
|
||||
}
|
||||
}
|
||||
|
||||
await isar.writeTxn(() async {
|
||||
var cache = GenericCacheModel();
|
||||
cache.cacheKey = cacheKey;
|
||||
cache.cacheData = jsonEncode(resp);
|
||||
|
||||
isar.genericCacheModels.put(cache);
|
||||
});
|
||||
|
||||
return (resp, statusCode, null, false);
|
||||
}
|
||||
|
||||
ApiResponse<Student>? studentCache;
|
||||
|
||||
Future<ApiResponse<Student>> getStudent({bool forceCache = true}) async {
|
||||
if (forceCache && studentCache != null) return studentCache!;
|
||||
var (resp, status, ex, cached) = await _cachingGet(CacheId.getStudent,
|
||||
KretaEndpoints.getStudentUrl(model.iss!), forceCache);
|
||||
|
||||
Student? student;
|
||||
String? err;
|
||||
try {
|
||||
student = Student.fromJson(resp);
|
||||
} catch (ex) {
|
||||
err = ex.toString();
|
||||
}
|
||||
|
||||
if (ex != null) {
|
||||
err = ex.toString();
|
||||
}
|
||||
|
||||
if (ex == null) studentCache = ApiResponse(student, 200, null, true);
|
||||
|
||||
return ApiResponse(student, status, err, cached);
|
||||
}
|
||||
|
||||
ApiResponse<List<NoticeBoardItem>>? noticeBoardCache;
|
||||
|
||||
Future<ApiResponse<List<NoticeBoardItem>>> getNoticeBoard(
|
||||
{bool forceCache = true}) async {
|
||||
if (forceCache && noticeBoardCache != null) return noticeBoardCache!;
|
||||
var (resp, status, ex, cached) = await _cachingGet(CacheId.getNoticeBoard,
|
||||
KretaEndpoints.getNoticeBoard(model.iss!), forceCache);
|
||||
|
||||
var items = List<NoticeBoardItem>.empty(growable: true);
|
||||
String? err;
|
||||
try {
|
||||
List<dynamic> rawItems = resp;
|
||||
for (var item in rawItems) {
|
||||
items.add(NoticeBoardItem.fromJson(item));
|
||||
}
|
||||
} catch (ex) {
|
||||
err = ex.toString();
|
||||
}
|
||||
|
||||
if (ex != null) {
|
||||
err = ex.toString();
|
||||
}
|
||||
|
||||
if (err == null) noticeBoardCache = ApiResponse(items, 200, null, true);
|
||||
|
||||
return ApiResponse(items, status, err, cached);
|
||||
}
|
||||
|
||||
ApiResponse<List<Grade>>? gradeCache;
|
||||
|
||||
Future<ApiResponse<List<Grade>>> getGrades({bool forceCache = true}) async {
|
||||
if (forceCache && gradeCache != null) {
|
||||
return gradeCache!;
|
||||
}
|
||||
var (resp, status, ex, cached) = await _cachingGet(
|
||||
CacheId.getGrades, KretaEndpoints.getGrades(model.iss!), forceCache);
|
||||
|
||||
var items = List<Grade>.empty(growable: true);
|
||||
String? err;
|
||||
try {
|
||||
List<dynamic> rawItems = resp;
|
||||
for (var item in rawItems) {
|
||||
items.add(Grade.fromJson(item));
|
||||
}
|
||||
} catch (ex) {
|
||||
err = ex.toString();
|
||||
}
|
||||
|
||||
if (ex != null) {
|
||||
err = ex.toString();
|
||||
}
|
||||
|
||||
items.sort((a, b) => b.recordDate.compareTo(a.recordDate));
|
||||
|
||||
if (ex == null) gradeCache = ApiResponse(items, 200, null, true);
|
||||
|
||||
return ApiResponse(items, status, err, cached);
|
||||
}
|
||||
|
||||
Future<(List<dynamic>, int, Object?, bool)>
|
||||
_timedCachingGet<T extends DatedCacheEntry>(
|
||||
IsarCollection<T> cacheModel,
|
||||
String endpoint,
|
||||
DateTime from,
|
||||
DateTime? to,
|
||||
bool forceCache,
|
||||
Future<void> Function(dynamic, int) storeCache) async {
|
||||
var cacheKey = genCacheKey(from, model.studentIdNorm!);
|
||||
var cache = await cacheModel.get(cacheKey);
|
||||
var formatter = DateFormat('yyyy-MM-dd');
|
||||
var fromStr = formatter.format(from);
|
||||
var toStr = to != null ? formatter.format(to) : null;
|
||||
var now = timeNow();
|
||||
|
||||
if (cache != null && (cache as dynamic).values == null) {
|
||||
(cache as dynamic).values = List<String>.empty(growable: true);
|
||||
}
|
||||
|
||||
List<dynamic> resp;
|
||||
int statusCode;
|
||||
try {
|
||||
if (forceCache && cache != null) {
|
||||
var items = List<dynamic>.empty(growable: true);
|
||||
for (var item in (cache as dynamic).values) {
|
||||
items.add(jsonDecode(item));
|
||||
}
|
||||
|
||||
return (items, 200, null, true);
|
||||
}
|
||||
if (toStr == null) {
|
||||
(resp, statusCode) = await _authJson(
|
||||
"GET",
|
||||
"$endpoint?"
|
||||
"datumTol=$fromStr");
|
||||
} else {
|
||||
(resp, statusCode) = await _authJson(
|
||||
"GET",
|
||||
"$endpoint?"
|
||||
"datumTol=$fromStr&datumIg=$toStr");
|
||||
}
|
||||
|
||||
if (statusCode >= 400) {
|
||||
if (cache != null) {
|
||||
var items = List<dynamic>.empty(growable: true);
|
||||
for (var item in (cache as dynamic).values) {
|
||||
items.add(jsonDecode(item));
|
||||
}
|
||||
return (items, statusCode, null, true);
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
if (cache != null) {
|
||||
var items = List<dynamic>.empty(growable: true);
|
||||
for (var item in (cache as dynamic).values) {
|
||||
items.add(jsonDecode(item));
|
||||
}
|
||||
return (items, 0, ex, true);
|
||||
} else {
|
||||
return (List<dynamic>.empty(growable: true), 0, ex, false);
|
||||
}
|
||||
}
|
||||
|
||||
// only cache stuff in a 1 month frame
|
||||
if (from.millisecondsSinceEpoch >=
|
||||
now.subtract(Duration(days: 30)).millisecondsSinceEpoch) {
|
||||
if (to == null ||
|
||||
to.millisecondsSinceEpoch <=
|
||||
now.add(Duration(days: 30)).millisecondsSinceEpoch) {
|
||||
await isar.writeTxn(() async {
|
||||
await storeCache(resp, cacheKey);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (resp, statusCode, null, false);
|
||||
}
|
||||
|
||||
/// Expects from and to to be 7 days apart
|
||||
Future<ApiResponse<List<Lesson>>> _getTimeTable(
|
||||
DateTime from, DateTime to, bool forceCache) async {
|
||||
var (resp, status, ex, cached) =
|
||||
await _timedCachingGet<TimetableCacheModel>(
|
||||
isar.timetableCacheModels,
|
||||
KretaEndpoints.getTimeTable(model.iss!),
|
||||
from,
|
||||
to,
|
||||
forceCache, (dynamic resp, int cacheKey) async {
|
||||
TimetableCacheModel cache = TimetableCacheModel();
|
||||
var rawClasses = List<String>.empty(growable: true);
|
||||
|
||||
for (var obj in resp) {
|
||||
rawClasses.add(jsonEncode(obj));
|
||||
}
|
||||
|
||||
cache.cacheKey = cacheKey;
|
||||
cache.values = rawClasses;
|
||||
|
||||
await isar.timetableCacheModels.put(cache as dynamic);
|
||||
});
|
||||
|
||||
var items = List<Lesson>.empty(growable: true);
|
||||
String? err;
|
||||
try {
|
||||
List<dynamic> rawItems = resp;
|
||||
for (var item in rawItems) {
|
||||
items.add(Lesson.fromJson(item));
|
||||
}
|
||||
} catch (ex) {
|
||||
err = ex.toString();
|
||||
}
|
||||
|
||||
if (ex != null) {
|
||||
err = ex.toString();
|
||||
}
|
||||
|
||||
return ApiResponse(items, status, err, cached);
|
||||
}
|
||||
|
||||
/// Expects from and to to be 7 days apart
|
||||
Future<ApiResponse<List<Homework>>> _getHomework(
|
||||
DateTime from, DateTime to, bool forceCache) async {
|
||||
var (resp, status, ex, cached) = await _timedCachingGet<HomeworkCacheModel>(
|
||||
isar.homeworkCacheModels,
|
||||
KretaEndpoints.getHomework(model.iss!),
|
||||
from,
|
||||
null,
|
||||
forceCache, (dynamic resp, int cacheKey) async {
|
||||
HomeworkCacheModel cache = HomeworkCacheModel();
|
||||
var rawClasses = List<String>.empty(growable: true);
|
||||
|
||||
for (var obj in resp) {
|
||||
rawClasses.add(jsonEncode(obj));
|
||||
}
|
||||
|
||||
cache.cacheKey = cacheKey;
|
||||
cache.values = rawClasses;
|
||||
|
||||
await isar.homeworkCacheModels.put(cache as dynamic);
|
||||
});
|
||||
|
||||
var items = List<Homework>.empty(growable: true);
|
||||
String? err;
|
||||
try {
|
||||
List<dynamic> rawItems = resp;
|
||||
for (var item in rawItems) {
|
||||
items.add(Homework.fromJson(item));
|
||||
}
|
||||
} catch (ex) {
|
||||
err = ex.toString();
|
||||
}
|
||||
|
||||
if (ex != null) {
|
||||
err = ex.toString();
|
||||
}
|
||||
|
||||
return ApiResponse(items, status, err, cached);
|
||||
}
|
||||
|
||||
/// Automatically aligns requests to start at Monday and end at Sunday
|
||||
Future<ApiResponse<List<Homework>>> getHomework(DateTime from, DateTime to,
|
||||
{bool forceCache = true}) async {
|
||||
var homework = List<Homework>.empty(growable: true);
|
||||
String? err;
|
||||
bool cached = true;
|
||||
|
||||
for (var i = from.millisecondsSinceEpoch;
|
||||
i < to.millisecondsSinceEpoch;
|
||||
i += 604800000) {
|
||||
var from = DateTime.fromMillisecondsSinceEpoch(i);
|
||||
var start = from.subtract(Duration(days: from.weekday - 1));
|
||||
var end = start.add(Duration(days: 6));
|
||||
|
||||
var resp = await _getHomework(start, end, forceCache);
|
||||
if (resp.err != null) {
|
||||
err = resp.err;
|
||||
if (!resp.cached) {
|
||||
return resp;
|
||||
} else {
|
||||
homework.addAll(resp.response!);
|
||||
}
|
||||
} else {
|
||||
homework.addAll(resp.response!);
|
||||
}
|
||||
if (!resp.cached) cached = false;
|
||||
}
|
||||
|
||||
homework.sort((a, b) => a.startDate.compareTo(b.startDate));
|
||||
homework = homework.where((h) => h.dueDate.isAfter(timeNow())).toList();
|
||||
|
||||
return ApiResponse(homework, 200, err, cached);
|
||||
}
|
||||
|
||||
/// Automatically aligns requests to start at Monday and end at Sunday
|
||||
Future<ApiResponse<List<Lesson>>> getTimeTable(DateTime from, DateTime to,
|
||||
{bool forceCache = true}) async {
|
||||
var lessons = List<Lesson>.empty(growable: true);
|
||||
String? err;
|
||||
bool cached = true;
|
||||
|
||||
for (var i = from.millisecondsSinceEpoch;
|
||||
i < to.millisecondsSinceEpoch;
|
||||
i += 604800000) {
|
||||
var from = DateTime.fromMillisecondsSinceEpoch(i);
|
||||
var start = from.subtract(Duration(days: from.weekday - 1));
|
||||
var end = start.add(Duration(days: 6));
|
||||
|
||||
var resp = await _getTimeTable(start, end, forceCache);
|
||||
if (resp.err != null) {
|
||||
err = resp.err;
|
||||
if (!resp.cached) {
|
||||
return resp;
|
||||
} else {
|
||||
lessons.addAll(resp.response!);
|
||||
}
|
||||
} else {
|
||||
lessons.addAll(resp.response!);
|
||||
}
|
||||
if (!resp.cached) cached = false;
|
||||
}
|
||||
|
||||
lessons.sort((a, b) => a.start.compareTo(b.start));
|
||||
lessons = lessons
|
||||
.where(
|
||||
(lesson) => lesson.start.isAfter(from) && lesson.end.isBefore(to))
|
||||
.toList();
|
||||
|
||||
return ApiResponse(lessons, 200, err, cached);
|
||||
}
|
||||
|
||||
Future<ApiResponse<List<Test>>> getTests({bool forceCache = true}) async {
|
||||
var (resp, status, ex, cached) = await _cachingGet(
|
||||
CacheId.getTests, KretaEndpoints.getTests(model.iss!), forceCache);
|
||||
|
||||
var items = List<Test>.empty(growable: true);
|
||||
String? err;
|
||||
try {
|
||||
List<dynamic> rawItems = resp;
|
||||
for (var item in rawItems) {
|
||||
items.add(Test.fromJson(item));
|
||||
}
|
||||
} catch (ex) {
|
||||
err = ex.toString();
|
||||
}
|
||||
|
||||
if (ex != null) {
|
||||
err = ex.toString();
|
||||
}
|
||||
|
||||
// items.sort((a, b) => a.date.compareTo(b.date));
|
||||
|
||||
return ApiResponse(items, status, err, cached);
|
||||
}
|
||||
|
||||
ApiResponse<List<Omission>>? omissionsCache;
|
||||
|
||||
Future<ApiResponse<List<Omission>>> getOmissions(
|
||||
{bool forceCache = true}) async {
|
||||
if (omissionsCache != null) return omissionsCache!;
|
||||
var (resp, status, ex, cached) = await _cachingGet(CacheId.getOmissions,
|
||||
KretaEndpoints.getOmissions(model.iss!), forceCache);
|
||||
|
||||
var items = List<Omission>.empty(growable: true);
|
||||
String? err;
|
||||
try {
|
||||
List<dynamic> rawItems = resp;
|
||||
for (var item in rawItems) {
|
||||
items.add(Omission.fromJson(item));
|
||||
}
|
||||
} catch (ex) {
|
||||
err = ex.toString();
|
||||
}
|
||||
|
||||
if (ex != null) {
|
||||
err = ex.toString();
|
||||
}
|
||||
|
||||
items.sort((a, b) => a.date.compareTo(b.date));
|
||||
|
||||
if (ex == null) omissionsCache = ApiResponse(items, 200, null, true);
|
||||
|
||||
return ApiResponse(items, status, err, cached);
|
||||
}
|
||||
|
||||
void evictMemCache() {
|
||||
studentCache = null;
|
||||
noticeBoardCache = null;
|
||||
gradeCache = null;
|
||||
omissionsCache = null;
|
||||
}
|
||||
}
|
||||
@@ -16,37 +16,7 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/// Watch no longer calls Kréta API; data comes from phone via init_data/sync_data.
|
||||
class Constants {
|
||||
static const clientId = "kreta-ellenorzo-student-mobile-ios";
|
||||
}
|
||||
|
||||
class KretaEndpoints {
|
||||
static String kretaBase = "e-kreta.hu";
|
||||
static String kreta(String iss) {
|
||||
if (iss == "firka-test") {
|
||||
return kretaBase;
|
||||
} else {
|
||||
return "https://$iss.$kretaBase";
|
||||
}
|
||||
}
|
||||
|
||||
static String kretaIdp = "https://idp.e-kreta.hu";
|
||||
static String kretaLoginUrl =
|
||||
"$kretaIdp/Account/Login?ReturnUrl=%2Fconnect%2Fauthorize%2Fcallback%3Fprompt%3Dlogin%26nonce%3DwylCrqT4oN6PPgQn2yQB0euKei9nJeZ6_ffJ-VpSKZU%26response_type%3Dcode%26code_challenge_method%3DS256%26scope%3Dopenid%2520email%2520offline_access%2520kreta-ellenorzo-webapi.public%2520kreta-eugyintezes-webapi.public%2520kreta-fileservice-webapi.public%2520kreta-mobile-global-webapi.public%2520kreta-dkt-webapi.public%2520kreta-ier-webapi.public%26code_challenge%3DHByZRRnPGb-Ko_wTI7ibIba1HQ6lor0ws4bcgReuYSQ%26redirect_uri%3Dhttps%253A%252F%252Fmobil.e-kreta.hu%252Fellenorzo-student%252Fprod%252Foauthredirect%26client_id%3Dkreta-ellenorzo-student-mobile-ios%26state%3Dkreta_student_mobile%26suppressed_prompt%3Dlogin";
|
||||
static String tokenGrantUrl = "$kretaIdp/connect/token";
|
||||
|
||||
static String getStudentUrl(String iss) =>
|
||||
"${kreta(iss)}/ellenorzo/v3/sajat/TanuloAdatlap";
|
||||
static String getNoticeBoard(String iss) =>
|
||||
"${kreta(iss)}/ellenorzo/v3/sajat/FaliujsagElemek";
|
||||
static String getGrades(String iss) =>
|
||||
"${kreta(iss)}/ellenorzo/v3/sajat/Ertekelesek";
|
||||
static String getTimeTable(String iss) =>
|
||||
"${kreta(iss)}/ellenorzo/v3/sajat/OrarendElemek";
|
||||
static String getOmissions(String iss) =>
|
||||
"${kreta(iss)}/ellenorzo/v3/sajat/Mulasztasok";
|
||||
static String getHomework(String iss) =>
|
||||
"${kreta(iss)}/ellenorzo/v3/sajat/HaziFeladatok";
|
||||
static String getTests(String iss) =>
|
||||
"${kreta(iss)}/ellenorzo/v3/sajat/BejelentettSzamonkeresek";
|
||||
}
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
/*
|
||||
Firka, alternative e-Kréta client.
|
||||
Copyright (C) 2025 QwIT Development
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:firka_wear/helpers/api/resp/token_grant.dart';
|
||||
import 'package:firka_wear/helpers/db/models/token_model.dart';
|
||||
|
||||
import '../../main.dart';
|
||||
import 'consts.dart';
|
||||
|
||||
Future<TokenGrantResponse> getAccessToken(String code) async {
|
||||
final headers = const <String, String>{
|
||||
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
"accept": "*/*",
|
||||
"user-agent": "eKretaStudent/264745 CFNetwork/1494.0.7 Darwin/23.4.0",
|
||||
};
|
||||
|
||||
final formData = <String, String>{
|
||||
"code": code,
|
||||
"code_verifier": "DSpuqj_HhDX4wzQIbtn8lr8NLE5wEi1iVLMtMK0jY6c",
|
||||
"redirect_uri":
|
||||
"https://mobil.e-kreta.hu/ellenorzo-student/prod/oauthredirect",
|
||||
"client_id": Constants.clientId,
|
||||
"grant_type": "authorization_code",
|
||||
};
|
||||
|
||||
try {
|
||||
final response = await dio.post(KretaEndpoints.tokenGrantUrl,
|
||||
options: Options(headers: headers), data: formData);
|
||||
|
||||
switch (response.statusCode) {
|
||||
case 200:
|
||||
return TokenGrantResponse.fromJson(response.data);
|
||||
case 401:
|
||||
throw Exception("Invalid grant");
|
||||
default:
|
||||
throw Exception(
|
||||
"Failed to get access token, response code: ${response.statusCode}");
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<TokenGrantResponse> extendToken(TokenModel model) async {
|
||||
final headers = const <String, String>{
|
||||
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
"accept": "*/*",
|
||||
"user-agent": "eKretaStudent/264745 CFNetwork/1494.0.7 Darwin/23.4.0",
|
||||
};
|
||||
|
||||
final formData = <String, String>{
|
||||
"institute_code": model.iss!,
|
||||
"refresh_token": model.refreshToken!,
|
||||
"grant_type": "refresh_token",
|
||||
"client_id": Constants.clientId,
|
||||
};
|
||||
|
||||
try {
|
||||
final response = await dio.post(KretaEndpoints.tokenGrantUrl,
|
||||
options: Options(headers: headers), data: formData);
|
||||
|
||||
switch (response.statusCode) {
|
||||
case 200:
|
||||
return TokenGrantResponse.fromJson(response.data);
|
||||
case 401:
|
||||
throw Exception("Invalid grant");
|
||||
default:
|
||||
throw Exception(
|
||||
"Failed to get access token, response code: ${response.statusCode}");
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,5 @@
|
||||
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
|
||||
import 'package:firka_wear/helpers/api/resp/token_grant.dart';
|
||||
import 'package:isar_community/isar.dart';
|
||||
|
||||
import '../../api/resp/token_grant.dart';
|
||||
import '../../debug_helper.dart';
|
||||
|
||||
part 'token_model.g.dart';
|
||||
|
||||
@collection
|
||||
@@ -19,8 +14,15 @@ class TokenModel {
|
||||
|
||||
TokenModel();
|
||||
|
||||
factory TokenModel.fromValues(Id studentIdNorm, studentId, String iss,
|
||||
String idToken, String accessToken, String refreshToken, int expiryDate) {
|
||||
factory TokenModel.fromValues(
|
||||
Id studentIdNorm,
|
||||
studentId,
|
||||
String iss,
|
||||
String idToken,
|
||||
String accessToken,
|
||||
String refreshToken,
|
||||
int expiryDate,
|
||||
) {
|
||||
var m = TokenModel();
|
||||
|
||||
m.studentIdNorm = studentIdNorm;
|
||||
@@ -33,22 +35,4 @@ class TokenModel {
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
factory TokenModel.fromResp(TokenGrantResponse resp) {
|
||||
var m = TokenModel();
|
||||
final jwt = JWT.decode(resp.idToken);
|
||||
|
||||
m.studentIdNorm = int.parse(
|
||||
jwt.payload["kreta:user_name"].toString().replaceAll("G0", ""));
|
||||
m.studentId = jwt.payload["kreta:user_name"];
|
||||
m.iss = jwt.payload["kreta:institute_code"];
|
||||
m.idToken = resp.idToken;
|
||||
m.accessToken = resp.accessToken;
|
||||
m.refreshToken = resp.refreshToken;
|
||||
m.expiryDate = timeNow()
|
||||
.add(Duration(seconds: resp.expiresIn))
|
||||
.subtract(Duration(minutes: 1)); // just to be safe
|
||||
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
79
firka_wear/lib/helpers/sync/wear_sync_store.dart
Normal file
79
firka_wear/lib/helpers/sync/wear_sync_store.dart
Normal file
@@ -0,0 +1,79 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import 'package:firka_wear/helpers/api/model/grade.dart';
|
||||
import 'package:firka_wear/helpers/api/model/timetable.dart';
|
||||
|
||||
const String _syncFileName = 'wear_sync_data.json';
|
||||
|
||||
/// Persists and loads synced data (timetable, grades, lastSyncAt) from the phone.
|
||||
class WearSyncStore {
|
||||
List<Lesson> _timetable = [];
|
||||
List<Grade> _grades = [];
|
||||
DateTime? _lastSyncAt;
|
||||
|
||||
List<Lesson> get timetable => List.unmodifiable(_timetable);
|
||||
List<Grade> get grades => List.unmodifiable(_grades);
|
||||
DateTime? get lastSyncAt => _lastSyncAt;
|
||||
|
||||
/// True if we have no data or data is older than 1 hour.
|
||||
bool get needsSync {
|
||||
if (_lastSyncAt == null) return true;
|
||||
return DateTime.now().difference(_lastSyncAt!) > const Duration(hours: 1);
|
||||
}
|
||||
|
||||
Future<String> _getSyncFilePath() async {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
return '${dir.path}/$_syncFileName';
|
||||
}
|
||||
|
||||
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();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<void> save({
|
||||
required DateTime? lastSyncAt,
|
||||
required List<Lesson> timetable,
|
||||
required List<Grade> grades,
|
||||
}) async {
|
||||
_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(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns lessons that fall on [date] (by date string or start date).
|
||||
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()
|
||||
..sort((a, b) => a.start.compareTo(b.start));
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import 'package:firka_wear/helpers/db/models/generic_cache_model.dart';
|
||||
import 'package:firka_wear/helpers/db/models/homework_cache_model.dart';
|
||||
import 'package:firka_wear/helpers/db/models/timetable_cache_model.dart';
|
||||
import 'package:firka_wear/helpers/db/models/token_model.dart';
|
||||
import 'package:firka_wear/helpers/sync/wear_sync_store.dart';
|
||||
import 'package:firka_wear/ui/model/style.dart';
|
||||
import 'package:firka_wear/ui/wear/screens/login/login_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -18,7 +19,6 @@ import 'package:path_provider/path_provider.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:wear_plus/wear_plus.dart';
|
||||
|
||||
import 'helpers/api/client/kreta_client.dart';
|
||||
import 'l10n/app_localizations.dart';
|
||||
import 'l10n/app_localizations_de.dart';
|
||||
import 'l10n/app_localizations_en.dart';
|
||||
@@ -47,16 +47,18 @@ class DeviceInfo {
|
||||
|
||||
class WearAppInitialization {
|
||||
final Isar isar;
|
||||
late KretaClient client;
|
||||
final WearSyncStore syncStore;
|
||||
final int tokenCount;
|
||||
final AppLocalizations l10n;
|
||||
final DeviceInfo devInfo;
|
||||
|
||||
WearAppInitialization(
|
||||
{required this.isar,
|
||||
required this.tokenCount,
|
||||
required this.l10n,
|
||||
required this.devInfo});
|
||||
WearAppInitialization({
|
||||
required this.isar,
|
||||
required this.syncStore,
|
||||
required this.tokenCount,
|
||||
required this.l10n,
|
||||
required this.devInfo,
|
||||
});
|
||||
}
|
||||
|
||||
Future<Isar> initDB() async {
|
||||
@@ -68,7 +70,7 @@ Future<Isar> initDB() async {
|
||||
TokenModelSchema,
|
||||
GenericCacheModelSchema,
|
||||
TimetableCacheModelSchema,
|
||||
HomeworkCacheModelSchema
|
||||
HomeworkCacheModelSchema,
|
||||
],
|
||||
inspector: true,
|
||||
directory: dir.path,
|
||||
@@ -90,28 +92,21 @@ AppLocalizations getLang() {
|
||||
|
||||
Future<WearAppInitialization> initializeApp() async {
|
||||
final isar = await initDB();
|
||||
final syncStore = WearSyncStore();
|
||||
await syncStore.load();
|
||||
|
||||
const channel = MethodChannel("firka.app/main");
|
||||
final rawInfo =
|
||||
((await channel.invokeMethod("get_info")) as String).split(";");
|
||||
final rawInfo = ((await channel.invokeMethod("get_info")) as String).split(
|
||||
";",
|
||||
);
|
||||
|
||||
var init = WearAppInitialization(
|
||||
return WearAppInitialization(
|
||||
isar: isar,
|
||||
syncStore: syncStore,
|
||||
tokenCount: await isar.tokenModels.count(),
|
||||
l10n: getLang(),
|
||||
devInfo: DeviceInfo(rawInfo[0], rawInfo[1], rawInfo[2]),
|
||||
);
|
||||
|
||||
resetOldTimeTableCache(isar);
|
||||
resetOldHomeworkCache(isar);
|
||||
|
||||
// TODO: Account selection
|
||||
if (init.tokenCount > 0) {
|
||||
init.client =
|
||||
KretaClient((await isar.tokenModels.where().findFirst())!, isar);
|
||||
}
|
||||
|
||||
return init;
|
||||
}
|
||||
|
||||
void main() async {
|
||||
@@ -207,7 +202,7 @@ class WearInitializationScreen extends StatelessWidget {
|
||||
'/login': (context) =>
|
||||
WearLoginScreen(data, key: ValueKey('wearLoginScreen')),
|
||||
'/home': (context) =>
|
||||
WearHomeScreen(data, key: ValueKey('wearHomeScreen'))
|
||||
WearHomeScreen(data, key: ValueKey('wearHomeScreen')),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -217,11 +212,7 @@ class WearInitializationScreen extends StatelessWidget {
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
color: wearStyle.colors.secondary,
|
||||
)
|
||||
],
|
||||
children: [Container(color: wearStyle.colors.secondary)],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:firka_wear/helpers/api/model/grade.dart';
|
||||
import 'package:firka_wear/helpers/api/model/timetable.dart';
|
||||
import 'package:firka_wear/helpers/extensions.dart';
|
||||
import 'package:firka_wear/ui/widget/class_icon.dart';
|
||||
@@ -9,6 +10,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_arc_text/flutter_arc_text.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:watch_connectivity/watch_connectivity.dart';
|
||||
import 'package:wear_plus/wear_plus.dart';
|
||||
|
||||
import '../../../../helpers/debug_helper.dart';
|
||||
@@ -38,6 +40,8 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
|
||||
bool init = false;
|
||||
WearMode mode = WearMode.active;
|
||||
final platform = MethodChannel('firka.app/main');
|
||||
final watch = WatchConnectivity();
|
||||
StreamSubscription? _messageSub;
|
||||
|
||||
bool disposed = false;
|
||||
|
||||
@@ -45,7 +49,10 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
now = timeNow();
|
||||
|
||||
_messageSub = watch.messageStream.listen((e) {
|
||||
final msg = Map<String, dynamic>.from(e);
|
||||
if (msg['id'] == 'sync_data') _onSyncData(msg);
|
||||
});
|
||||
timer = Timer.periodic(Duration(seconds: 1), (timer) async {
|
||||
setState(() {
|
||||
now = timeNow();
|
||||
@@ -54,22 +61,40 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
|
||||
initStateAsync();
|
||||
}
|
||||
|
||||
Future<void> initStateAsync() async {
|
||||
var kreta = data.client;
|
||||
|
||||
now = timeNow();
|
||||
var todayStart = now.getMidnight();
|
||||
var todayEnd = todayStart.add(Duration(hours: 23, minutes: 59));
|
||||
var classes = await kreta.getTimeTable(todayStart, todayEnd);
|
||||
|
||||
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(() {
|
||||
if (classes.response != null) today = classes.response!;
|
||||
if (classes.statusCode != 200) {
|
||||
apiError = "Unexpected status : ${classes.statusCode}";
|
||||
}
|
||||
if (classes.err != null) apiError = classes.err!;
|
||||
now = timeNow();
|
||||
today = data.syncStore.getLessonsForDate(now);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> initStateAsync() async {
|
||||
now = timeNow();
|
||||
if (data.syncStore.needsSync) {
|
||||
watch.sendMessage({'id': 'request_sync'});
|
||||
}
|
||||
await data.syncStore.load();
|
||||
if (disposed) return;
|
||||
setState(() {
|
||||
now = timeNow();
|
||||
today = data.syncStore.getLessonsForDate(now);
|
||||
init = true;
|
||||
});
|
||||
}
|
||||
@@ -82,34 +107,44 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
|
||||
return (body, 255.h);
|
||||
}
|
||||
|
||||
if (today.isEmpty && apiError != "") {
|
||||
body.add(Text(
|
||||
apiError,
|
||||
style:
|
||||
wearStyle.fonts.H_18px.apply(color: wearStyle.colors.textPrimary),
|
||||
textAlign: TextAlign.center,
|
||||
));
|
||||
|
||||
if (today.isEmpty &&
|
||||
data.syncStore.needsSync &&
|
||||
data.syncStore.timetable.isEmpty) {
|
||||
body.add(
|
||||
Text(
|
||||
AppLocalizations.of(context)!.wear_sync_with_phone,
|
||||
style: wearStyle.fonts.H_18px.apply(
|
||||
color: wearStyle.colors.textPrimary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
return (body, 255.h);
|
||||
}
|
||||
if (today.isEmpty) {
|
||||
body.add(Text(
|
||||
AppLocalizations.of(context)!.noClasses,
|
||||
style:
|
||||
wearStyle.fonts.H_18px.apply(color: wearStyle.colors.textPrimary),
|
||||
textAlign: TextAlign.center,
|
||||
));
|
||||
body.add(
|
||||
Text(
|
||||
AppLocalizations.of(context)!.noClasses,
|
||||
style: wearStyle.fonts.H_18px.apply(
|
||||
color: wearStyle.colors.textPrimary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
|
||||
platform.invokeMethod('activity_cancel');
|
||||
return (body, 255.h);
|
||||
}
|
||||
if (now.isAfter(today.last.end)) {
|
||||
body.add(Text(
|
||||
AppLocalizations.of(context)!.noMoreClasses,
|
||||
style:
|
||||
wearStyle.fonts.H_18px.apply(color: wearStyle.colors.textPrimary),
|
||||
textAlign: TextAlign.center,
|
||||
));
|
||||
body.add(
|
||||
Text(
|
||||
AppLocalizations.of(context)!.noMoreClasses,
|
||||
style: wearStyle.fonts.H_18px.apply(
|
||||
color: wearStyle.colors.textPrimary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
|
||||
platform.invokeMethod('activity_cancel');
|
||||
return (body, 300.h);
|
||||
@@ -117,12 +152,15 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
|
||||
if (now.isBefore(today.first.start)) {
|
||||
var untilFirst = today.first.start.difference(now);
|
||||
|
||||
body.add(Text(
|
||||
AppLocalizations.of(context)!.firstIn(untilFirst.formatDuration()),
|
||||
style:
|
||||
wearStyle.fonts.H_18px.apply(color: wearStyle.colors.textPrimary),
|
||||
textAlign: TextAlign.center,
|
||||
));
|
||||
body.add(
|
||||
Text(
|
||||
AppLocalizations.of(context)!.firstIn(untilFirst.formatDuration()),
|
||||
style: wearStyle.fonts.H_18px.apply(
|
||||
color: wearStyle.colors.textPrimary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
|
||||
platform.invokeMethod('activity_update');
|
||||
return (body, 255.h);
|
||||
@@ -155,14 +193,17 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
|
||||
|
||||
var minutes = currentBreakProgress.inMinutes + 1;
|
||||
|
||||
body.add(CustomPaint(
|
||||
body.add(
|
||||
CustomPaint(
|
||||
painter: CircularProgressPainter(
|
||||
progress: currentBreakProgress.inMilliseconds /
|
||||
currentBreak.inMilliseconds,
|
||||
// progress: 5 / 10,
|
||||
screenSize: MediaQuery.of(context).size,
|
||||
strokeWidth: 4,
|
||||
color: wearStyle.colors.accent),
|
||||
progress:
|
||||
currentBreakProgress.inMilliseconds /
|
||||
currentBreak.inMilliseconds,
|
||||
// progress: 5 / 10,
|
||||
screenSize: MediaQuery.of(context).size,
|
||||
strokeWidth: 4,
|
||||
color: wearStyle.colors.accent,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
@@ -174,9 +215,7 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
|
||||
color: wearStyle.colors.textPrimary,
|
||||
fontSize: 14,
|
||||
fontFamily: 'Montserrat',
|
||||
fontVariations: [
|
||||
FontVariation('wght', 600),
|
||||
],
|
||||
fontVariations: [FontVariation('wght', 600)],
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -187,14 +226,14 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
|
||||
color: wearStyle.colors.textPrimary,
|
||||
fontSize: 12,
|
||||
fontFamily: 'Montserrat',
|
||||
fontVariations: [
|
||||
FontVariation('wght', 400),
|
||||
],
|
||||
fontVariations: [FontVariation('wght', 400)],
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
)));
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
platform.invokeMethod('activity_update');
|
||||
return (body, 200.h);
|
||||
@@ -215,61 +254,61 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
|
||||
color: wearStyle.colors.textPrimary,
|
||||
fontSize: 12,
|
||||
fontFamily: 'Montserrat',
|
||||
fontVariations: [
|
||||
FontVariation('wght', 400),
|
||||
],
|
||||
fontVariations: [FontVariation('wght', 400)],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
body.add(CustomPaint(
|
||||
body.add(
|
||||
CustomPaint(
|
||||
painter: CircularProgressPainter(
|
||||
progress: elapsed.inMilliseconds / duration.inMilliseconds,
|
||||
screenSize: MediaQuery.of(context).size,
|
||||
strokeWidth: 4,
|
||||
color: wearStyle.colors.accent),
|
||||
child: Column(children: [
|
||||
SizedBox(height: nextLesson == null ? 20.h : 0),
|
||||
Center(
|
||||
child: ClassIconWidget(
|
||||
color: wearStyle.colors.accent,
|
||||
size: 16,
|
||||
uid: currentLesson.uid,
|
||||
className: currentLesson.name,
|
||||
category: currentLesson.subject?.name ?? '',
|
||||
).build(context),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Center(
|
||||
child: Text(
|
||||
"${currentLesson.name}, ${currentLesson.roomName}",
|
||||
style: TextStyle(
|
||||
color: wearStyle.colors.textPrimary,
|
||||
fontSize: 14,
|
||||
fontFamily: 'Montserrat',
|
||||
fontVariations: [
|
||||
FontVariation('wght', 600),
|
||||
],
|
||||
progress: elapsed.inMilliseconds / duration.inMilliseconds,
|
||||
screenSize: MediaQuery.of(context).size,
|
||||
strokeWidth: 4,
|
||||
color: wearStyle.colors.accent,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: nextLesson == null ? 20.h : 0),
|
||||
Center(
|
||||
child: ClassIconWidget(
|
||||
color: wearStyle.colors.accent,
|
||||
size: 16,
|
||||
uid: currentLesson.uid,
|
||||
className: currentLesson.name,
|
||||
category: currentLesson.subject?.name ?? '',
|
||||
).build(context),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Center(
|
||||
child: Text(
|
||||
"${currentLesson.name}, ${currentLesson.roomName}",
|
||||
style: TextStyle(
|
||||
color: wearStyle.colors.textPrimary,
|
||||
fontSize: 14,
|
||||
fontFamily: 'Montserrat',
|
||||
fontVariations: [FontVariation('wght', 600)],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.timeLeft(minutes),
|
||||
style: TextStyle(
|
||||
color: wearStyle.colors.textPrimary,
|
||||
fontSize: 12,
|
||||
fontFamily: 'Montserrat',
|
||||
fontVariations: [
|
||||
FontVariation('wght', 400),
|
||||
],
|
||||
Center(
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.timeLeft(minutes),
|
||||
style: TextStyle(
|
||||
color: wearStyle.colors.textPrimary,
|
||||
fontSize: 12,
|
||||
fontFamily: 'Montserrat',
|
||||
fontVariations: [FontVariation('wght', 400)],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
nextLessonWidget,
|
||||
])));
|
||||
const SizedBox(height: 8),
|
||||
nextLessonWidget,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
platform.invokeMethod('activity_update');
|
||||
return (body, 200.h);
|
||||
@@ -294,9 +333,7 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
|
||||
fontSize: 12,
|
||||
color: wearStyle.colors.secondary,
|
||||
fontFamily: 'Montserrat',
|
||||
fontVariations: [
|
||||
FontVariation('wght', 500),
|
||||
],
|
||||
fontVariations: [FontVariation('wght', 500)],
|
||||
),
|
||||
placement: Placement.inside,
|
||||
);
|
||||
@@ -308,9 +345,7 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
|
||||
: wearStyle.colors.backgroundAmoled,
|
||||
body: Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: titleBar,
|
||||
),
|
||||
Center(child: titleBar),
|
||||
Center(
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -318,9 +353,7 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
|
||||
builder: (context, shape, child) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
child!,
|
||||
],
|
||||
children: <Widget>[child!],
|
||||
);
|
||||
},
|
||||
child: AmbientMode(
|
||||
@@ -339,19 +372,20 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: EdgeInsets.only(top: padding),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [...body],
|
||||
)),
|
||||
padding: EdgeInsets.only(top: padding),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [...body],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -359,8 +393,9 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_messageSub?.cancel();
|
||||
timer?.cancel();
|
||||
disposed = true;
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:firka_wear/helpers/api/client/kreta_client.dart';
|
||||
import 'package:firka_wear/helpers/api/model/grade.dart';
|
||||
import 'package:firka_wear/helpers/api/model/timetable.dart';
|
||||
import 'package:firka_wear/helpers/extensions.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:watch_connectivity/watch_connectivity.dart';
|
||||
@@ -47,26 +48,46 @@ class _WearLoginScreen extends State<WearLoginScreen> {
|
||||
case "init_data":
|
||||
{
|
||||
() async {
|
||||
var data = msg["auth"];
|
||||
var tokenModel = TokenModel.fromValues(
|
||||
data["studentIdNorm"],
|
||||
data["studentId"],
|
||||
data["iss"],
|
||||
data["idToken"],
|
||||
data["accessToken"],
|
||||
data["refreshToken"],
|
||||
data["expiryDate"]);
|
||||
|
||||
initData.client = KretaClient(tokenModel, initData.isar);
|
||||
|
||||
final auth = msg["auth"] as Map<dynamic, dynamic>?;
|
||||
if (auth == null) return;
|
||||
final tokenModel = TokenModel.fromValues(
|
||||
auth["studentIdNorm"] as int,
|
||||
auth["studentId"] as String,
|
||||
auth["iss"] as String,
|
||||
auth["idToken"] as String,
|
||||
auth["accessToken"] as String,
|
||||
auth["refreshToken"] as String,
|
||||
auth["expiryDate"] as int,
|
||||
);
|
||||
await initData.isar.writeTxn(() async {
|
||||
await initData.isar.tokenModels.put(tokenModel);
|
||||
});
|
||||
|
||||
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 initData.syncStore.save(
|
||||
lastSyncAt: lastSyncAt,
|
||||
timetable: timetable,
|
||||
grades: grades,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => WearHomeScreen(initData)),
|
||||
(route) => false, // Remove all previous routes
|
||||
builder: (context) => WearHomeScreen(initData),
|
||||
),
|
||||
(route) => false,
|
||||
);
|
||||
}();
|
||||
}
|
||||
@@ -94,12 +115,7 @@ class _WearLoginScreen extends State<WearLoginScreen> {
|
||||
|
||||
(List<Widget>, double) buildBody(BuildContext context) {
|
||||
if (!init) {
|
||||
return (
|
||||
<Widget>[
|
||||
|
||||
],
|
||||
60
|
||||
);
|
||||
return (<Widget>[], 60);
|
||||
}
|
||||
|
||||
if (!isPaired) {
|
||||
@@ -108,11 +124,12 @@ class _WearLoginScreen extends State<WearLoginScreen> {
|
||||
Text(
|
||||
widget.data.l10n.wear_phone_unpaired,
|
||||
textAlign: TextAlign.center,
|
||||
style: wearStyle.fonts.B_14R
|
||||
.apply(color: wearStyle.colors.textPrimary),
|
||||
style: wearStyle.fonts.B_14R.apply(
|
||||
color: wearStyle.colors.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
60
|
||||
60,
|
||||
);
|
||||
}
|
||||
if (!isReachable) {
|
||||
@@ -121,11 +138,12 @@ class _WearLoginScreen extends State<WearLoginScreen> {
|
||||
Text(
|
||||
widget.data.l10n.wear_phone_disconnected,
|
||||
textAlign: TextAlign.center,
|
||||
style: wearStyle.fonts.B_16R
|
||||
.apply(color: wearStyle.colors.textPrimary),
|
||||
style: wearStyle.fonts.B_16R.apply(
|
||||
color: wearStyle.colors.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
60
|
||||
60,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -135,13 +153,17 @@ class _WearLoginScreen extends State<WearLoginScreen> {
|
||||
Text(
|
||||
widget.data.l10n.wear_pairing_request_sent,
|
||||
textAlign: TextAlign.center,
|
||||
style: wearStyle.fonts.B_16R
|
||||
.apply(color: wearStyle.colors.textPrimary),
|
||||
style: wearStyle.fonts.B_16R.apply(
|
||||
color: wearStyle.colors.textPrimary,
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
debugPrint("[Watch -> Phone]: ping");
|
||||
watch.sendMessage({'id': 'ping', 'model': initData.devInfo.model});
|
||||
watch.sendMessage({
|
||||
'id': 'ping',
|
||||
'model': initData.devInfo.model,
|
||||
});
|
||||
},
|
||||
// TODO: This is a placeholder, style this properly
|
||||
style: ButtonStyle(
|
||||
@@ -158,12 +180,14 @@ class _WearLoginScreen extends State<WearLoginScreen> {
|
||||
return wearStyle.colors.accent;
|
||||
}),
|
||||
),
|
||||
child: Text(widget.data.l10n.wear_try_again,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: wearStyle.colors.textPrimary)),
|
||||
child: Text(
|
||||
widget.data.l10n.wear_try_again,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: wearStyle.colors.textPrimary),
|
||||
),
|
||||
),
|
||||
],
|
||||
45
|
||||
45,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -173,8 +197,9 @@ class _WearLoginScreen extends State<WearLoginScreen> {
|
||||
Text(
|
||||
widget.data.l10n.wear_pairing_check_phone,
|
||||
textAlign: TextAlign.center,
|
||||
style: wearStyle.fonts.B_16R
|
||||
.apply(color: wearStyle.colors.textPrimary),
|
||||
style: wearStyle.fonts.B_16R.apply(
|
||||
color: wearStyle.colors.textPrimary,
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
@@ -196,22 +221,26 @@ class _WearLoginScreen extends State<WearLoginScreen> {
|
||||
return wearStyle.colors.accent;
|
||||
}),
|
||||
),
|
||||
child: Text(widget.data.l10n.wear_try_again,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: wearStyle.colors.textPrimary)),
|
||||
child: Text(
|
||||
widget.data.l10n.wear_try_again,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: wearStyle.colors.textPrimary),
|
||||
),
|
||||
),
|
||||
],
|
||||
55
|
||||
55,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>[
|
||||
Text("Unexpected state",
|
||||
style: TextStyle(color: wearStyle.colors.textPrimary, fontSize: 18),
|
||||
textAlign: TextAlign.center),
|
||||
Text(
|
||||
"Unexpected state",
|
||||
style: TextStyle(color: wearStyle.colors.textPrimary, fontSize: 18),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
60
|
||||
60,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -225,26 +254,28 @@ class _WearLoginScreen extends State<WearLoginScreen> {
|
||||
child: Column(
|
||||
children: [
|
||||
WatchShape(
|
||||
builder: (context, shape, child) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: EdgeInsets.only(top: offset),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: body,
|
||||
)),
|
||||
],
|
||||
),
|
||||
child!,
|
||||
],
|
||||
);
|
||||
},
|
||||
child: SizedBox())
|
||||
builder: (context, shape, child) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: EdgeInsets.only(top: offset),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: body,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child!,
|
||||
],
|
||||
);
|
||||
},
|
||||
child: SizedBox(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user