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/>.
|
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 {
|
class Constants {
|
||||||
static const clientId = "kreta-ellenorzo-student-mobile-ios";
|
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 'package:isar_community/isar.dart';
|
||||||
|
|
||||||
import '../../api/resp/token_grant.dart';
|
|
||||||
import '../../debug_helper.dart';
|
|
||||||
|
|
||||||
part 'token_model.g.dart';
|
part 'token_model.g.dart';
|
||||||
|
|
||||||
@collection
|
@collection
|
||||||
@@ -19,8 +14,15 @@ class TokenModel {
|
|||||||
|
|
||||||
TokenModel();
|
TokenModel();
|
||||||
|
|
||||||
factory TokenModel.fromValues(Id studentIdNorm, studentId, String iss,
|
factory TokenModel.fromValues(
|
||||||
String idToken, String accessToken, String refreshToken, int expiryDate) {
|
Id studentIdNorm,
|
||||||
|
studentId,
|
||||||
|
String iss,
|
||||||
|
String idToken,
|
||||||
|
String accessToken,
|
||||||
|
String refreshToken,
|
||||||
|
int expiryDate,
|
||||||
|
) {
|
||||||
var m = TokenModel();
|
var m = TokenModel();
|
||||||
|
|
||||||
m.studentIdNorm = studentIdNorm;
|
m.studentIdNorm = studentIdNorm;
|
||||||
@@ -33,22 +35,4 @@ class TokenModel {
|
|||||||
|
|
||||||
return m;
|
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/homework_cache_model.dart';
|
||||||
import 'package:firka_wear/helpers/db/models/timetable_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/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/model/style.dart';
|
||||||
import 'package:firka_wear/ui/wear/screens/login/login_screen.dart';
|
import 'package:firka_wear/ui/wear/screens/login/login_screen.dart';
|
||||||
import 'package:flutter/material.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:permission_handler/permission_handler.dart';
|
||||||
import 'package:wear_plus/wear_plus.dart';
|
import 'package:wear_plus/wear_plus.dart';
|
||||||
|
|
||||||
import 'helpers/api/client/kreta_client.dart';
|
|
||||||
import 'l10n/app_localizations.dart';
|
import 'l10n/app_localizations.dart';
|
||||||
import 'l10n/app_localizations_de.dart';
|
import 'l10n/app_localizations_de.dart';
|
||||||
import 'l10n/app_localizations_en.dart';
|
import 'l10n/app_localizations_en.dart';
|
||||||
@@ -47,16 +47,18 @@ class DeviceInfo {
|
|||||||
|
|
||||||
class WearAppInitialization {
|
class WearAppInitialization {
|
||||||
final Isar isar;
|
final Isar isar;
|
||||||
late KretaClient client;
|
final WearSyncStore syncStore;
|
||||||
final int tokenCount;
|
final int tokenCount;
|
||||||
final AppLocalizations l10n;
|
final AppLocalizations l10n;
|
||||||
final DeviceInfo devInfo;
|
final DeviceInfo devInfo;
|
||||||
|
|
||||||
WearAppInitialization(
|
WearAppInitialization({
|
||||||
{required this.isar,
|
required this.isar,
|
||||||
required this.tokenCount,
|
required this.syncStore,
|
||||||
required this.l10n,
|
required this.tokenCount,
|
||||||
required this.devInfo});
|
required this.l10n,
|
||||||
|
required this.devInfo,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Isar> initDB() async {
|
Future<Isar> initDB() async {
|
||||||
@@ -68,7 +70,7 @@ Future<Isar> initDB() async {
|
|||||||
TokenModelSchema,
|
TokenModelSchema,
|
||||||
GenericCacheModelSchema,
|
GenericCacheModelSchema,
|
||||||
TimetableCacheModelSchema,
|
TimetableCacheModelSchema,
|
||||||
HomeworkCacheModelSchema
|
HomeworkCacheModelSchema,
|
||||||
],
|
],
|
||||||
inspector: true,
|
inspector: true,
|
||||||
directory: dir.path,
|
directory: dir.path,
|
||||||
@@ -90,28 +92,21 @@ AppLocalizations getLang() {
|
|||||||
|
|
||||||
Future<WearAppInitialization> initializeApp() async {
|
Future<WearAppInitialization> initializeApp() async {
|
||||||
final isar = await initDB();
|
final isar = await initDB();
|
||||||
|
final syncStore = WearSyncStore();
|
||||||
|
await syncStore.load();
|
||||||
|
|
||||||
const channel = MethodChannel("firka.app/main");
|
const channel = MethodChannel("firka.app/main");
|
||||||
final rawInfo =
|
final rawInfo = ((await channel.invokeMethod("get_info")) as String).split(
|
||||||
((await channel.invokeMethod("get_info")) as String).split(";");
|
";",
|
||||||
|
);
|
||||||
|
|
||||||
var init = WearAppInitialization(
|
return WearAppInitialization(
|
||||||
isar: isar,
|
isar: isar,
|
||||||
|
syncStore: syncStore,
|
||||||
tokenCount: await isar.tokenModels.count(),
|
tokenCount: await isar.tokenModels.count(),
|
||||||
l10n: getLang(),
|
l10n: getLang(),
|
||||||
devInfo: DeviceInfo(rawInfo[0], rawInfo[1], rawInfo[2]),
|
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 {
|
void main() async {
|
||||||
@@ -207,7 +202,7 @@ class WearInitializationScreen extends StatelessWidget {
|
|||||||
'/login': (context) =>
|
'/login': (context) =>
|
||||||
WearLoginScreen(data, key: ValueKey('wearLoginScreen')),
|
WearLoginScreen(data, key: ValueKey('wearLoginScreen')),
|
||||||
'/home': (context) =>
|
'/home': (context) =>
|
||||||
WearHomeScreen(data, key: ValueKey('wearHomeScreen'))
|
WearHomeScreen(data, key: ValueKey('wearHomeScreen')),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -217,11 +212,7 @@ class WearInitializationScreen extends StatelessWidget {
|
|||||||
body: Center(
|
body: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [Container(color: wearStyle.colors.secondary)],
|
||||||
Container(
|
|
||||||
color: wearStyle.colors.secondary,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math';
|
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/api/model/timetable.dart';
|
||||||
import 'package:firka_wear/helpers/extensions.dart';
|
import 'package:firka_wear/helpers/extensions.dart';
|
||||||
import 'package:firka_wear/ui/widget/class_icon.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/services.dart';
|
||||||
import 'package:flutter_arc_text/flutter_arc_text.dart';
|
import 'package:flutter_arc_text/flutter_arc_text.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
|
import 'package:watch_connectivity/watch_connectivity.dart';
|
||||||
import 'package:wear_plus/wear_plus.dart';
|
import 'package:wear_plus/wear_plus.dart';
|
||||||
|
|
||||||
import '../../../../helpers/debug_helper.dart';
|
import '../../../../helpers/debug_helper.dart';
|
||||||
@@ -38,6 +40,8 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
|
|||||||
bool init = false;
|
bool init = false;
|
||||||
WearMode mode = WearMode.active;
|
WearMode mode = WearMode.active;
|
||||||
final platform = MethodChannel('firka.app/main');
|
final platform = MethodChannel('firka.app/main');
|
||||||
|
final watch = WatchConnectivity();
|
||||||
|
StreamSubscription? _messageSub;
|
||||||
|
|
||||||
bool disposed = false;
|
bool disposed = false;
|
||||||
|
|
||||||
@@ -45,7 +49,10 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
now = timeNow();
|
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 {
|
timer = Timer.periodic(Duration(seconds: 1), (timer) async {
|
||||||
setState(() {
|
setState(() {
|
||||||
now = timeNow();
|
now = timeNow();
|
||||||
@@ -54,22 +61,40 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
|
|||||||
initStateAsync();
|
initStateAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initStateAsync() async {
|
void _onSyncData(Map<String, dynamic> msg) async {
|
||||||
var kreta = data.client;
|
final lastSyncAt = msg['lastSyncAt'] != null
|
||||||
|
? DateTime.parse(msg['lastSyncAt'] as String)
|
||||||
now = timeNow();
|
: null;
|
||||||
var todayStart = now.getMidnight();
|
final rawTimetable = msg['timetable'] as List<dynamic>? ?? [];
|
||||||
var todayEnd = todayStart.add(Duration(hours: 23, minutes: 59));
|
final timetable = rawTimetable
|
||||||
var classes = await kreta.getTimeTable(todayStart, todayEnd);
|
.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;
|
if (disposed) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
if (classes.response != null) today = classes.response!;
|
now = timeNow();
|
||||||
if (classes.statusCode != 200) {
|
today = data.syncStore.getLessonsForDate(now);
|
||||||
apiError = "Unexpected status : ${classes.statusCode}";
|
});
|
||||||
}
|
}
|
||||||
if (classes.err != null) apiError = classes.err!;
|
|
||||||
|
|
||||||
|
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;
|
init = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -82,34 +107,44 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
|
|||||||
return (body, 255.h);
|
return (body, 255.h);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (today.isEmpty && apiError != "") {
|
if (today.isEmpty &&
|
||||||
body.add(Text(
|
data.syncStore.needsSync &&
|
||||||
apiError,
|
data.syncStore.timetable.isEmpty) {
|
||||||
style:
|
body.add(
|
||||||
wearStyle.fonts.H_18px.apply(color: wearStyle.colors.textPrimary),
|
Text(
|
||||||
textAlign: TextAlign.center,
|
AppLocalizations.of(context)!.wear_sync_with_phone,
|
||||||
));
|
style: wearStyle.fonts.H_18px.apply(
|
||||||
|
color: wearStyle.colors.textPrimary,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
);
|
||||||
return (body, 255.h);
|
return (body, 255.h);
|
||||||
}
|
}
|
||||||
if (today.isEmpty) {
|
if (today.isEmpty) {
|
||||||
body.add(Text(
|
body.add(
|
||||||
AppLocalizations.of(context)!.noClasses,
|
Text(
|
||||||
style:
|
AppLocalizations.of(context)!.noClasses,
|
||||||
wearStyle.fonts.H_18px.apply(color: wearStyle.colors.textPrimary),
|
style: wearStyle.fonts.H_18px.apply(
|
||||||
textAlign: TextAlign.center,
|
color: wearStyle.colors.textPrimary,
|
||||||
));
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
platform.invokeMethod('activity_cancel');
|
platform.invokeMethod('activity_cancel');
|
||||||
return (body, 255.h);
|
return (body, 255.h);
|
||||||
}
|
}
|
||||||
if (now.isAfter(today.last.end)) {
|
if (now.isAfter(today.last.end)) {
|
||||||
body.add(Text(
|
body.add(
|
||||||
AppLocalizations.of(context)!.noMoreClasses,
|
Text(
|
||||||
style:
|
AppLocalizations.of(context)!.noMoreClasses,
|
||||||
wearStyle.fonts.H_18px.apply(color: wearStyle.colors.textPrimary),
|
style: wearStyle.fonts.H_18px.apply(
|
||||||
textAlign: TextAlign.center,
|
color: wearStyle.colors.textPrimary,
|
||||||
));
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
platform.invokeMethod('activity_cancel');
|
platform.invokeMethod('activity_cancel');
|
||||||
return (body, 300.h);
|
return (body, 300.h);
|
||||||
@@ -117,12 +152,15 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
|
|||||||
if (now.isBefore(today.first.start)) {
|
if (now.isBefore(today.first.start)) {
|
||||||
var untilFirst = today.first.start.difference(now);
|
var untilFirst = today.first.start.difference(now);
|
||||||
|
|
||||||
body.add(Text(
|
body.add(
|
||||||
AppLocalizations.of(context)!.firstIn(untilFirst.formatDuration()),
|
Text(
|
||||||
style:
|
AppLocalizations.of(context)!.firstIn(untilFirst.formatDuration()),
|
||||||
wearStyle.fonts.H_18px.apply(color: wearStyle.colors.textPrimary),
|
style: wearStyle.fonts.H_18px.apply(
|
||||||
textAlign: TextAlign.center,
|
color: wearStyle.colors.textPrimary,
|
||||||
));
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
platform.invokeMethod('activity_update');
|
platform.invokeMethod('activity_update');
|
||||||
return (body, 255.h);
|
return (body, 255.h);
|
||||||
@@ -155,14 +193,17 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
|
|||||||
|
|
||||||
var minutes = currentBreakProgress.inMinutes + 1;
|
var minutes = currentBreakProgress.inMinutes + 1;
|
||||||
|
|
||||||
body.add(CustomPaint(
|
body.add(
|
||||||
|
CustomPaint(
|
||||||
painter: CircularProgressPainter(
|
painter: CircularProgressPainter(
|
||||||
progress: currentBreakProgress.inMilliseconds /
|
progress:
|
||||||
currentBreak.inMilliseconds,
|
currentBreakProgress.inMilliseconds /
|
||||||
// progress: 5 / 10,
|
currentBreak.inMilliseconds,
|
||||||
screenSize: MediaQuery.of(context).size,
|
// progress: 5 / 10,
|
||||||
strokeWidth: 4,
|
screenSize: MediaQuery.of(context).size,
|
||||||
color: wearStyle.colors.accent),
|
strokeWidth: 4,
|
||||||
|
color: wearStyle.colors.accent,
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@@ -174,9 +215,7 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
|
|||||||
color: wearStyle.colors.textPrimary,
|
color: wearStyle.colors.textPrimary,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontFamily: 'Montserrat',
|
fontFamily: 'Montserrat',
|
||||||
fontVariations: [
|
fontVariations: [FontVariation('wght', 600)],
|
||||||
FontVariation('wght', 600),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -187,14 +226,14 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
|
|||||||
color: wearStyle.colors.textPrimary,
|
color: wearStyle.colors.textPrimary,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontFamily: 'Montserrat',
|
fontFamily: 'Montserrat',
|
||||||
fontVariations: [
|
fontVariations: [FontVariation('wght', 400)],
|
||||||
FontVariation('wght', 400),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
)));
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
platform.invokeMethod('activity_update');
|
platform.invokeMethod('activity_update');
|
||||||
return (body, 200.h);
|
return (body, 200.h);
|
||||||
@@ -215,61 +254,61 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
|
|||||||
color: wearStyle.colors.textPrimary,
|
color: wearStyle.colors.textPrimary,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontFamily: 'Montserrat',
|
fontFamily: 'Montserrat',
|
||||||
fontVariations: [
|
fontVariations: [FontVariation('wght', 400)],
|
||||||
FontVariation('wght', 400),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.add(CustomPaint(
|
body.add(
|
||||||
|
CustomPaint(
|
||||||
painter: CircularProgressPainter(
|
painter: CircularProgressPainter(
|
||||||
progress: elapsed.inMilliseconds / duration.inMilliseconds,
|
progress: elapsed.inMilliseconds / duration.inMilliseconds,
|
||||||
screenSize: MediaQuery.of(context).size,
|
screenSize: MediaQuery.of(context).size,
|
||||||
strokeWidth: 4,
|
strokeWidth: 4,
|
||||||
color: wearStyle.colors.accent),
|
color: wearStyle.colors.accent,
|
||||||
child: Column(children: [
|
),
|
||||||
SizedBox(height: nextLesson == null ? 20.h : 0),
|
child: Column(
|
||||||
Center(
|
children: [
|
||||||
child: ClassIconWidget(
|
SizedBox(height: nextLesson == null ? 20.h : 0),
|
||||||
color: wearStyle.colors.accent,
|
Center(
|
||||||
size: 16,
|
child: ClassIconWidget(
|
||||||
uid: currentLesson.uid,
|
color: wearStyle.colors.accent,
|
||||||
className: currentLesson.name,
|
size: 16,
|
||||||
category: currentLesson.subject?.name ?? '',
|
uid: currentLesson.uid,
|
||||||
).build(context),
|
className: currentLesson.name,
|
||||||
),
|
category: currentLesson.subject?.name ?? '',
|
||||||
const SizedBox(height: 4),
|
).build(context),
|
||||||
Center(
|
),
|
||||||
child: Text(
|
const SizedBox(height: 4),
|
||||||
"${currentLesson.name}, ${currentLesson.roomName}",
|
Center(
|
||||||
style: TextStyle(
|
child: Text(
|
||||||
color: wearStyle.colors.textPrimary,
|
"${currentLesson.name}, ${currentLesson.roomName}",
|
||||||
fontSize: 14,
|
style: TextStyle(
|
||||||
fontFamily: 'Montserrat',
|
color: wearStyle.colors.textPrimary,
|
||||||
fontVariations: [
|
fontSize: 14,
|
||||||
FontVariation('wght', 600),
|
fontFamily: 'Montserrat',
|
||||||
],
|
fontVariations: [FontVariation('wght', 600)],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Center(
|
||||||
Center(
|
child: Text(
|
||||||
child: Text(
|
AppLocalizations.of(context)!.timeLeft(minutes),
|
||||||
AppLocalizations.of(context)!.timeLeft(minutes),
|
style: TextStyle(
|
||||||
style: TextStyle(
|
color: wearStyle.colors.textPrimary,
|
||||||
color: wearStyle.colors.textPrimary,
|
fontSize: 12,
|
||||||
fontSize: 12,
|
fontFamily: 'Montserrat',
|
||||||
fontFamily: 'Montserrat',
|
fontVariations: [FontVariation('wght', 400)],
|
||||||
fontVariations: [
|
),
|
||||||
FontVariation('wght', 400),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 8),
|
||||||
const SizedBox(height: 8),
|
nextLessonWidget,
|
||||||
nextLessonWidget,
|
],
|
||||||
])));
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
platform.invokeMethod('activity_update');
|
platform.invokeMethod('activity_update');
|
||||||
return (body, 200.h);
|
return (body, 200.h);
|
||||||
@@ -294,9 +333,7 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
|
|||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: wearStyle.colors.secondary,
|
color: wearStyle.colors.secondary,
|
||||||
fontFamily: 'Montserrat',
|
fontFamily: 'Montserrat',
|
||||||
fontVariations: [
|
fontVariations: [FontVariation('wght', 500)],
|
||||||
FontVariation('wght', 500),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
placement: Placement.inside,
|
placement: Placement.inside,
|
||||||
);
|
);
|
||||||
@@ -308,9 +345,7 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
|
|||||||
: wearStyle.colors.backgroundAmoled,
|
: wearStyle.colors.backgroundAmoled,
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
Center(
|
Center(child: titleBar),
|
||||||
child: titleBar,
|
|
||||||
),
|
|
||||||
Center(
|
Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -318,9 +353,7 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
|
|||||||
builder: (context, shape, child) {
|
builder: (context, shape, child) {
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[child!],
|
||||||
child!,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: AmbientMode(
|
child: AmbientMode(
|
||||||
@@ -339,19 +372,20 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Container(
|
Container(
|
||||||
padding: EdgeInsets.only(top: padding),
|
padding: EdgeInsets.only(top: padding),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [...body],
|
children: [...body],
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -359,8 +393,9 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
_messageSub?.cancel();
|
||||||
timer?.cancel();
|
timer?.cancel();
|
||||||
disposed = true;
|
disposed = true;
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
import 'dart:async';
|
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:firka_wear/helpers/extensions.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:watch_connectivity/watch_connectivity.dart';
|
import 'package:watch_connectivity/watch_connectivity.dart';
|
||||||
@@ -47,26 +48,46 @@ class _WearLoginScreen extends State<WearLoginScreen> {
|
|||||||
case "init_data":
|
case "init_data":
|
||||||
{
|
{
|
||||||
() async {
|
() async {
|
||||||
var data = msg["auth"];
|
final auth = msg["auth"] as Map<dynamic, dynamic>?;
|
||||||
var tokenModel = TokenModel.fromValues(
|
if (auth == null) return;
|
||||||
data["studentIdNorm"],
|
final tokenModel = TokenModel.fromValues(
|
||||||
data["studentId"],
|
auth["studentIdNorm"] as int,
|
||||||
data["iss"],
|
auth["studentId"] as String,
|
||||||
data["idToken"],
|
auth["iss"] as String,
|
||||||
data["accessToken"],
|
auth["idToken"] as String,
|
||||||
data["refreshToken"],
|
auth["accessToken"] as String,
|
||||||
data["expiryDate"]);
|
auth["refreshToken"] as String,
|
||||||
|
auth["expiryDate"] as int,
|
||||||
initData.client = KretaClient(tokenModel, initData.isar);
|
);
|
||||||
|
|
||||||
await initData.isar.writeTxn(() async {
|
await initData.isar.writeTxn(() async {
|
||||||
await initData.isar.tokenModels.put(tokenModel);
|
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(
|
Navigator.of(context).pushAndRemoveUntil(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => WearHomeScreen(initData)),
|
builder: (context) => WearHomeScreen(initData),
|
||||||
(route) => false, // Remove all previous routes
|
),
|
||||||
|
(route) => false,
|
||||||
);
|
);
|
||||||
}();
|
}();
|
||||||
}
|
}
|
||||||
@@ -94,12 +115,7 @@ class _WearLoginScreen extends State<WearLoginScreen> {
|
|||||||
|
|
||||||
(List<Widget>, double) buildBody(BuildContext context) {
|
(List<Widget>, double) buildBody(BuildContext context) {
|
||||||
if (!init) {
|
if (!init) {
|
||||||
return (
|
return (<Widget>[], 60);
|
||||||
<Widget>[
|
|
||||||
|
|
||||||
],
|
|
||||||
60
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isPaired) {
|
if (!isPaired) {
|
||||||
@@ -108,11 +124,12 @@ class _WearLoginScreen extends State<WearLoginScreen> {
|
|||||||
Text(
|
Text(
|
||||||
widget.data.l10n.wear_phone_unpaired,
|
widget.data.l10n.wear_phone_unpaired,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: wearStyle.fonts.B_14R
|
style: wearStyle.fonts.B_14R.apply(
|
||||||
.apply(color: wearStyle.colors.textPrimary),
|
color: wearStyle.colors.textPrimary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
60
|
60,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!isReachable) {
|
if (!isReachable) {
|
||||||
@@ -121,11 +138,12 @@ class _WearLoginScreen extends State<WearLoginScreen> {
|
|||||||
Text(
|
Text(
|
||||||
widget.data.l10n.wear_phone_disconnected,
|
widget.data.l10n.wear_phone_disconnected,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: wearStyle.fonts.B_16R
|
style: wearStyle.fonts.B_16R.apply(
|
||||||
.apply(color: wearStyle.colors.textPrimary),
|
color: wearStyle.colors.textPrimary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
60
|
60,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,13 +153,17 @@ class _WearLoginScreen extends State<WearLoginScreen> {
|
|||||||
Text(
|
Text(
|
||||||
widget.data.l10n.wear_pairing_request_sent,
|
widget.data.l10n.wear_pairing_request_sent,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: wearStyle.fonts.B_16R
|
style: wearStyle.fonts.B_16R.apply(
|
||||||
.apply(color: wearStyle.colors.textPrimary),
|
color: wearStyle.colors.textPrimary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
debugPrint("[Watch -> Phone]: ping");
|
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
|
// TODO: This is a placeholder, style this properly
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
@@ -158,12 +180,14 @@ class _WearLoginScreen extends State<WearLoginScreen> {
|
|||||||
return wearStyle.colors.accent;
|
return wearStyle.colors.accent;
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
child: Text(widget.data.l10n.wear_try_again,
|
child: Text(
|
||||||
textAlign: TextAlign.center,
|
widget.data.l10n.wear_try_again,
|
||||||
style: TextStyle(color: wearStyle.colors.textPrimary)),
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(color: wearStyle.colors.textPrimary),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
45
|
45,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,8 +197,9 @@ class _WearLoginScreen extends State<WearLoginScreen> {
|
|||||||
Text(
|
Text(
|
||||||
widget.data.l10n.wear_pairing_check_phone,
|
widget.data.l10n.wear_pairing_check_phone,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: wearStyle.fonts.B_16R
|
style: wearStyle.fonts.B_16R.apply(
|
||||||
.apply(color: wearStyle.colors.textPrimary),
|
color: wearStyle.colors.textPrimary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
@@ -196,22 +221,26 @@ class _WearLoginScreen extends State<WearLoginScreen> {
|
|||||||
return wearStyle.colors.accent;
|
return wearStyle.colors.accent;
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
child: Text(widget.data.l10n.wear_try_again,
|
child: Text(
|
||||||
textAlign: TextAlign.center,
|
widget.data.l10n.wear_try_again,
|
||||||
style: TextStyle(color: wearStyle.colors.textPrimary)),
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(color: wearStyle.colors.textPrimary),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
55
|
55,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Widget>[
|
<Widget>[
|
||||||
Text("Unexpected state",
|
Text(
|
||||||
style: TextStyle(color: wearStyle.colors.textPrimary, fontSize: 18),
|
"Unexpected state",
|
||||||
textAlign: TextAlign.center),
|
style: TextStyle(color: wearStyle.colors.textPrimary, fontSize: 18),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
60
|
60,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,26 +254,28 @@ class _WearLoginScreen extends State<WearLoginScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
WatchShape(
|
WatchShape(
|
||||||
builder: (context, shape, child) {
|
builder: (context, shape, child) {
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Column(
|
Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Container(
|
Container(
|
||||||
padding: EdgeInsets.only(top: offset),
|
padding: EdgeInsets.only(top: offset),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: body,
|
children: body,
|
||||||
)),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
child!,
|
),
|
||||||
],
|
child!,
|
||||||
);
|
],
|
||||||
},
|
);
|
||||||
child: SizedBox())
|
},
|
||||||
|
child: SizedBox(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user