1
0
forked from firka/firka

fix warnings, reformat files

This commit is contained in:
2026-02-27 22:37:18 +01:00
committed by 4831c0
parent b0c8f1f4b3
commit 28fb054571
82 changed files with 7109 additions and 5097 deletions

View File

@@ -8,8 +8,7 @@ int resolveActiveAccountIndex(dynamic settings) {
if (accountIndex is int && accountIndex >= 0) {
return accountIndex;
}
} catch (_) {
}
} catch (_) {}
return 0;
}

View File

@@ -42,12 +42,7 @@ class ApiResponse<T> {
String? err;
bool cached;
ApiResponse(
this.response,
this.statusCode,
this.err,
this.cached,
);
ApiResponse(this.response, this.statusCode, this.err, this.cached);
@override
String toString() {
@@ -85,7 +80,8 @@ class KretaClient {
KretaClient(this.model, this.isar);
Future<TokenModel> _refreshModelWithCrossDeviceLease(
TokenModel sourceToken) async {
TokenModel sourceToken,
) async {
final studentIdNorm = sourceToken.studentIdNorm;
String? leaseOperationId;
@@ -111,9 +107,7 @@ class KretaClient {
final extended = await extendToken(sourceToken);
return TokenModel.fromResp(extended);
} finally {
if (Platform.isIOS &&
studentIdNorm != null &&
leaseOperationId != null) {
if (Platform.isIOS && studentIdNorm != null && leaseOperationId != null) {
await WatchSyncHelper.releaseIPhoneRefreshLease(
studentIdNorm: studentIdNorm,
operationId: leaseOperationId,
@@ -133,7 +127,8 @@ class KretaClient {
final watchInstalled = await WatchSyncHelper.isWatchAppInstalled();
if (!watchInstalled) {
debugPrint(
'[KretaClient] Skipping Apple token sync because no paired Watch app is installed');
'[KretaClient] Skipping Apple token sync because no paired Watch app is installed',
);
return;
}
@@ -186,7 +181,8 @@ class KretaClient {
if (localExpiry != null &&
localExpiry.isAfter(now.add(const Duration(seconds: 60)))) {
logger.info(
"[Recovery] Existing token is still valid, skipping recovery steps");
"[Recovery] Existing token is still valid, skipping recovery steps",
);
clearReauthFlag();
return true;
}
@@ -209,8 +205,9 @@ class KretaClient {
}
if (!Platform.isIOS || !initDone) {
logger
.warning("[Recovery] Not iOS or not initialized, cannot try iCloud");
logger.warning(
"[Recovery] Not iOS or not initialized, cannot try iCloud",
);
return false;
}
@@ -227,12 +224,14 @@ class KretaClient {
break;
}
logger.info(
"[Recovery] Waiting ${delay}s before attempt ${attempt + 1}...");
"[Recovery] Waiting ${delay}s before attempt ${attempt + 1}...",
);
await Future.delayed(Duration(seconds: delay));
}
logger.info(
"[Recovery] iCloud attempt ${attempt + 1}/${retryDelays.length}...");
"[Recovery] iCloud attempt ${attempt + 1}/${retryDelays.length}...",
);
final recovered = await WatchSyncHelper.checkAndRecoverFromiCloud(
isar: isar,
@@ -244,20 +243,24 @@ class KretaClient {
if (recovered) {
iCloudHasToken = true;
await _reloadActiveTokenModel(
preferredStudentIdNorm: model.studentIdNorm);
preferredStudentIdNorm: model.studentIdNorm,
);
final recoveredExpiry = model.expiryDate;
if (recoveredExpiry != null &&
recoveredExpiry
.isAfter(timeNow().add(const Duration(seconds: 60)))) {
recoveredExpiry.isAfter(
timeNow().add(const Duration(seconds: 60)),
)) {
logger.info(
"[Recovery] Step 2 SUCCESS on attempt ${attempt + 1}: usable iCloud token applied without immediate refresh");
"[Recovery] Step 2 SUCCESS on attempt ${attempt + 1}: usable iCloud token applied without immediate refresh",
);
clearReauthFlag();
return true;
}
logger.info(
"[Recovery] Found iCloud token close to expiry, trying refresh...");
"[Recovery] Found iCloud token close to expiry, trying refresh...",
);
try {
var tokenModel = await _refreshModelWithCrossDeviceLease(model);
@@ -272,12 +275,14 @@ class KretaClient {
return true;
} catch (e) {
logger.warning(
"[Recovery] iCloud token refresh failed on attempt ${attempt + 1}: $e");
"[Recovery] iCloud token refresh failed on attempt ${attempt + 1}: $e",
);
iCloudHasToken = true;
}
} else {
logger.info(
"[Recovery] No fresh token in iCloud on attempt ${attempt + 1}");
"[Recovery] No fresh token in iCloud on attempt ${attempt + 1}",
);
if (attempt == 0) {
iCloudHasToken = false;
}
@@ -295,7 +300,8 @@ class KretaClient {
if (model.expiryDate == null ||
model.expiryDate!.isBefore(fiveMinutesFromNow)) {
logger.info(
"[Proactive] Token expired or expiring soon, starting recovery...");
"[Proactive] Token expired or expiring soon, starting recovery...",
);
final recovered = await recoverToken();
if (recovered) {
@@ -315,7 +321,8 @@ class KretaClient {
}
logger.fine(
"[Proactive] Token still valid until ${model.expiryDate}, no refresh needed");
"[Proactive] Token still valid until ${model.expiryDate}, no refresh needed",
);
return true;
}
@@ -326,7 +333,8 @@ class KretaClient {
while (_tokenMutex) {
if (DateTime.now().difference(startTime) > maxWaitTime) {
logger.warning(
"[Mutex] Timeout waiting for token mutex, forcing release");
"[Mutex] Timeout waiting for token mutex, forcing release",
);
_tokenMutex = false;
break;
}
@@ -347,7 +355,8 @@ class KretaClient {
if (now.millisecondsSinceEpoch >=
model.expiryDate!.millisecondsSinceEpoch) {
logger.info(
"Token expired at ${model.expiryDate}, starting recovery for user: ${model.studentId}");
"Token expired at ${model.expiryDate}, starting recovery for user: ${model.studentId}",
);
final recovered = await recoverToken();
if (!recovered) {
@@ -364,15 +373,21 @@ class KretaClient {
"accept": "*/*",
"user-agent": Constants.userAgent,
"authorization": "Bearer $localToken",
"apiKey": "21ff6c25-d1da-4a68-a811-c881a6057463"
"apiKey": "21ff6c25-d1da-4a68-a811-c881a6057463",
};
return await dio.get(url,
options: Options(method: method, headers: headers), data: data);
return await dio.get(
url,
options: Options(method: method, headers: headers),
data: data,
);
}
Future<(dynamic, int)> _authJson(String method, String url,
[Object? data]) async {
Future<(dynamic, int)> _authJson(
String method,
String url, [
Object? data,
]) async {
Response<dynamic> resp;
try {
@@ -388,13 +403,17 @@ class KretaClient {
(responseData is List && responseData.isEmpty) ||
(responseData is Map && responseData.isEmpty)) {
logger.warning(
"API returned ${resp.statusCode} with empty data for: $url - possible stale session");
"API returned ${resp.statusCode} with empty data for: $url - possible stale session",
);
}
}
} catch (ex) {
if (ex is Error) {
logger.shout(
"Request to url: $url failed", ex.toString(), ex.stackTrace);
"Request to url: $url failed",
ex.toString(),
ex.stackTrace,
);
} else {
logger.shout("Request to url: $url failed", ex.toString());
}
@@ -406,7 +425,11 @@ class KretaClient {
}
Future<(dynamic, int, Object?, bool)> _cachingGet(
CacheId id, String url, bool forceCache, int counter) async {
CacheId id,
String url,
bool forceCache,
int counter,
) 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???
@@ -418,7 +441,8 @@ class KretaClient {
try {
if (forceCache && cache != null) {
logger.finest(
"_cachingGet(forceCache: $forceCache}): decoding cached response for: $url");
"_cachingGet(forceCache: $forceCache}): decoding cached response for: $url",
);
return (jsonDecode(cache.cacheData!), 200, null, true);
}
@@ -428,14 +452,18 @@ class KretaClient {
if (statusCode >= 400) {
if (cache != null) {
logger.finest(
"_cachingGet(forceCache: $forceCache}): decoding uncached response for: $url");
"_cachingGet(forceCache: $forceCache}): decoding uncached response for: $url",
);
return (jsonDecode(cache.cacheData!), statusCode, null, true);
}
}
} catch (ex) {
if (ex is Error) {
logger.finest(
"Request failed for $url", ex.toString(), ex.stackTrace);
"Request failed for $url",
ex.toString(),
ex.stackTrace,
);
} else {
logger.finest("Request failed for $url", ex.toString());
}
@@ -494,8 +522,12 @@ class KretaClient {
} else if (studentCache != null) {
return studentCache!;
}
var (resp, status, ex, cached) = await _cachingGet(CacheId.getStudent,
KretaEndpoints.getStudentUrl(model.iss!), forceCache, 0);
var (resp, status, ex, cached) = await _cachingGet(
CacheId.getStudent,
KretaEndpoints.getStudentUrl(model.iss!),
forceCache,
0,
);
Student? student;
String? err;
@@ -516,15 +548,20 @@ class KretaClient {
ApiResponse<List<ClassGroup>>? classGroupCache;
Future<ApiResponse<List<ClassGroup>>> getClassGroups(
{bool forceCache = true}) async {
Future<ApiResponse<List<ClassGroup>>> getClassGroups({
bool forceCache = true,
}) async {
if (!forceCache) {
classGroupCache = null;
} else {
if (classGroupCache != null) return classGroupCache!;
}
var (resp, status, ex, cached) = await _cachingGet(CacheId.getClassGroup,
KretaEndpoints.getClassGroups(model.iss!), forceCache, 0);
var (resp, status, ex, cached) = await _cachingGet(
CacheId.getClassGroup,
KretaEndpoints.getClassGroups(model.iss!),
forceCache,
0,
);
final classGroups = List<ClassGroup>.empty(growable: true);
String? err;
@@ -548,15 +585,20 @@ class KretaClient {
ApiResponse<List<NoticeBoardItem>>? noticeBoardCache;
Future<ApiResponse<List<NoticeBoardItem>>> getNoticeBoard(
{bool forceCache = true}) async {
Future<ApiResponse<List<NoticeBoardItem>>> getNoticeBoard({
bool forceCache = true,
}) async {
if (!forceCache) {
noticeBoardCache = null;
} else if (noticeBoardCache != null) {
return noticeBoardCache!;
}
var (resp, status, ex, cached) = await _cachingGet(CacheId.getNoticeBoard,
KretaEndpoints.getNoticeBoard(model.iss!), forceCache, 0);
var (resp, status, ex, cached) = await _cachingGet(
CacheId.getNoticeBoard,
KretaEndpoints.getNoticeBoard(model.iss!),
forceCache,
0,
);
var items = List<NoticeBoardItem>.empty(growable: true);
String? err;
@@ -580,11 +622,16 @@ class KretaClient {
ApiResponse<List<InfoBoardItem>>? infoBoardCache;
Future<ApiResponse<List<InfoBoardItem>>> getInfoBoard(
{bool forceCache = true}) async {
Future<ApiResponse<List<InfoBoardItem>>> getInfoBoard({
bool forceCache = true,
}) async {
if (forceCache && infoBoardCache != null) return infoBoardCache!;
var (resp, status, ex, cached) = await _cachingGet(CacheId.getInfoBoard,
KretaEndpoints.getInfoBoard(model.iss!), forceCache, 0);
var (resp, status, ex, cached) = await _cachingGet(
CacheId.getInfoBoard,
KretaEndpoints.getInfoBoard(model.iss!),
forceCache,
0,
);
var items = List<InfoBoardItem>.empty(growable: true);
String? err;
@@ -615,7 +662,11 @@ class KretaClient {
return gradeCache!;
}
var (resp, status, ex, cached) = await _cachingGet(
CacheId.getGrades, KretaEndpoints.getGrades(model.iss!), forceCache, 0);
CacheId.getGrades,
KretaEndpoints.getGrades(model.iss!),
forceCache,
0,
);
var items = List<Grade>.empty(growable: true);
String? err;
@@ -642,14 +693,19 @@ class KretaClient {
ApiResponse<List<SubjectAverage>>? subjectAverageCache;
Future<ApiResponse<List<SubjectAverage>>> getSubjectAverage(
ClassGroup classGroup,
{bool forceCache = true}) async {
ClassGroup classGroup, {
bool forceCache = true,
}) async {
String? err;
if (classGroup.studyTask == null) {
err = "classGroup.studyTask is null";
logger.warning(err);
return ApiResponse(
List<SubjectAverage>.empty(growable: true), 0, err, false);
List<SubjectAverage>.empty(growable: true),
0,
err,
false,
);
}
if (!forceCache) {
subjectAverageCache = null;
@@ -657,8 +713,12 @@ class KretaClient {
return subjectAverageCache!;
}
var studyTaskUid = classGroup.studyTask!.uid.toString().split(",").first;
var (resp, status, ex, cached) = await _cachingGet(CacheId.getSubjectAvg,
KretaEndpoints.getSubjectAvg(model.iss!, studyTaskUid), forceCache, 0);
var (resp, status, ex, cached) = await _cachingGet(
CacheId.getSubjectAvg,
KretaEndpoints.getSubjectAvg(model.iss!, studyTaskUid),
forceCache,
0,
);
var items = List<SubjectAverage>.empty(growable: true);
try {
@@ -679,14 +739,15 @@ class KretaClient {
}
Future<(List<dynamic>, int, Object?, bool)>
_timedCachingGet<T extends DatedCacheEntry>(
IsarCollection<T> cacheModel,
String endpoint,
DateTime from,
DateTime? to,
bool forceCache,
int counter,
Future<void> Function(dynamic, int) storeCache) async {
_timedCachingGet<T extends DatedCacheEntry>(
IsarCollection<T> cacheModel,
String endpoint,
DateTime from,
DateTime? to,
bool forceCache,
int counter,
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');
@@ -712,14 +773,16 @@ class KretaClient {
try {
if (toStr == null) {
(resp, statusCode) = await _authJson(
"GET",
"$endpoint?"
"datumTol=$fromStr");
"GET",
"$endpoint?"
"datumTol=$fromStr",
);
} else {
(resp, statusCode) = await _authJson(
"GET",
"$endpoint?"
"datumTol=$fromStr&datumIg=$toStr");
"GET",
"$endpoint?"
"datumTol=$fromStr&datumIg=$toStr",
);
}
if (statusCode >= 400) {
@@ -739,16 +802,25 @@ class KretaClient {
}
await Future.delayed(
Duration(milliseconds: backoffMin + (counter * backoffStep)));
Duration(milliseconds: backoffMin + (counter * backoffStep)),
);
return _timedCachingGet(cacheModel, endpoint, from, to, forceCache,
counter + 1, storeCache);
return _timedCachingGet(
cacheModel,
endpoint,
from,
to,
forceCache,
counter + 1,
storeCache,
);
}
} catch (ex) {
if (_isTokenExpired(ex)) {
await _setReauthFlag();
logger.warning(
"Token expired in timed request, setting needsReauth flag");
"Token expired in timed request, setting needsReauth flag",
);
}
if (cache != null) {
@@ -779,27 +851,36 @@ class KretaClient {
/// 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,
0, (dynamic resp, int cacheKey) async {
TimetableCacheModel cache = TimetableCacheModel();
var rawClasses = List<String>.empty(growable: true);
DateTime from,
DateTime to,
bool forceCache,
) async {
var (
resp,
status,
ex,
cached,
) = await _timedCachingGet<TimetableCacheModel>(
isar.timetableCacheModels,
KretaEndpoints.getTimeTable(model.iss!),
from,
to,
forceCache,
0,
(dynamic resp, int cacheKey) async {
TimetableCacheModel cache = TimetableCacheModel();
var rawClasses = List<String>.empty(growable: true);
for (var obj in resp) {
rawClasses.add(jsonEncode(obj));
}
for (var obj in resp) {
rawClasses.add(jsonEncode(obj));
}
cache.cacheKey = cacheKey;
cache.values = rawClasses;
cache.cacheKey = cacheKey;
cache.values = rawClasses;
await isar.timetableCacheModels.put(cache as dynamic);
});
await isar.timetableCacheModels.put(cache as dynamic);
},
);
var items = List<Lesson>.empty(growable: true);
String? err;
@@ -819,16 +900,18 @@ class KretaClient {
return ApiResponse(items, status, err, cached);
}
Future<ApiResponse<List<Homework>>> getHomework(
{bool forceCache = true}) async {
Future<ApiResponse<List<Homework>>> getHomework({
bool forceCache = true,
}) async {
final now = timeNow().subtract(Duration(days: 365));
var formatter = DateFormat('yyyy-MM-dd');
var start = formatter.format(now);
var (resp, status, ex, cached) = await _cachingGet(
CacheId.getHomework,
"${KretaEndpoints.getHomework(model.iss!)}?datumTol=$start",
forceCache,
0);
CacheId.getHomework,
"${KretaEndpoints.getHomework(model.iss!)}?datumTol=$start",
forceCache,
0,
);
var items = List<Homework>.empty(growable: true);
String? err;
@@ -851,15 +934,20 @@ class KretaClient {
}
/// Automatically aligns requests to start at Monday and end at Sunday
Future<ApiResponse<List<Lesson>>> getTimeTable(DateTime from, DateTime to,
{bool forceCache = true}) async {
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) {
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));
@@ -881,14 +969,16 @@ class KretaClient {
lessons.sort((a, b) => a.start.compareTo(b.start));
lessons = lessons
.where(
(lesson) => lesson.start.isAfter(from) && lesson.end.isBefore(to))
(lesson) => lesson.start.isAfter(from) && lesson.end.isBefore(to),
)
.toList();
return ApiResponse(lessons, 200, err, cached);
}
Future<ApiResponse<List<AllLessons>>> getLessons(
{bool forceCache = true}) async {
Future<ApiResponse<List<AllLessons>>> getLessons({
bool forceCache = true,
}) async {
var (resp, status, ex, cached) = await _cachingGet(
CacheId.getLessons,
KretaEndpoints.getLessons(model.iss!),
@@ -925,7 +1015,11 @@ class KretaClient {
Future<ApiResponse<List<Test>>> getTests({bool forceCache = true}) async {
var (resp, status, ex, cached) = await _cachingGet(
CacheId.getTests, KretaEndpoints.getTests(model.iss!), forceCache, 0);
CacheId.getTests,
KretaEndpoints.getTests(model.iss!),
forceCache,
0,
);
var items = List<Test>.empty(growable: true);
String? err;
@@ -949,15 +1043,20 @@ class KretaClient {
ApiResponse<List<Omission>>? omissionsCache;
Future<ApiResponse<List<Omission>>> getOmissions(
{bool forceCache = true}) async {
Future<ApiResponse<List<Omission>>> getOmissions({
bool forceCache = true,
}) async {
if (!forceCache) {
omissionsCache = null;
} else {
if (omissionsCache != null) return omissionsCache!;
}
var (resp, status, ex, cached) = await _cachingGet(CacheId.getOmissions,
KretaEndpoints.getOmissions(model.iss!), forceCache, 0);
var (resp, status, ex, cached) = await _cachingGet(
CacheId.getOmissions,
KretaEndpoints.getOmissions(model.iss!),
forceCache,
0,
);
var items = List<Omission>.empty(growable: true);
String? err;

View File

@@ -21,8 +21,9 @@ bool getTestsStreamFL = false;
bool getOmissionsStreamFL = false;
extension KretaStream on KretaClient {
Stream<ApiResponse<Student>> getStudentStream(
{bool cacheOnly = true}) async* {
Stream<ApiResponse<Student>> getStudentStream({
bool cacheOnly = true,
}) async* {
while (getStudentFL) {
await Future.delayed(Duration(milliseconds: 10));
}
@@ -35,8 +36,9 @@ extension KretaStream on KretaClient {
getStudentFL = false;
}
Stream<ApiResponse<List<ClassGroup>>> getClassGroupsStream(
{bool cacheOnly = true}) async* {
Stream<ApiResponse<List<ClassGroup>>> getClassGroupsStream({
bool cacheOnly = true,
}) async* {
while (getClassGroupsFL) {
await Future.delayed(Duration(milliseconds: 10));
}
@@ -49,8 +51,9 @@ extension KretaStream on KretaClient {
getClassGroupsFL = false;
}
Stream<ApiResponse<List<NoticeBoardItem>>> getNoticeBoardStream(
{bool cacheOnly = true}) async* {
Stream<ApiResponse<List<NoticeBoardItem>>> getNoticeBoardStream({
bool cacheOnly = true,
}) async* {
while (getNoticeBoardStreamFL) {
await Future.delayed(Duration(milliseconds: 10));
}
@@ -63,8 +66,9 @@ extension KretaStream on KretaClient {
getNoticeBoardStreamFL = false;
}
Stream<ApiResponse<List<InfoBoardItem>>> getInfoBoardStream(
{bool cacheOnly = true}) async* {
Stream<ApiResponse<List<InfoBoardItem>>> getInfoBoardStream({
bool cacheOnly = true,
}) async* {
while (getInfoBoardStreamFL) {
await Future.delayed(Duration(milliseconds: 10));
}
@@ -77,8 +81,9 @@ extension KretaStream on KretaClient {
getInfoBoardStreamFL = false;
}
Stream<ApiResponse<List<Grade>>> getGradesStream(
{bool cacheOnly = true}) async* {
Stream<ApiResponse<List<Grade>>> getGradesStream({
bool cacheOnly = true,
}) async* {
while (getGradesStreamFL) {
await Future.delayed(Duration(milliseconds: 10));
}
@@ -92,8 +97,9 @@ extension KretaStream on KretaClient {
}
Stream<ApiResponse<List<SubjectAverage>>> getSubjectAverageStream(
ClassGroup classGroup,
{bool cacheOnly = true}) async* {
ClassGroup classGroup, {
bool cacheOnly = true,
}) async* {
while (getSubjectAverageStreamFL) {
await Future.delayed(Duration(milliseconds: 10));
}
@@ -106,8 +112,9 @@ extension KretaStream on KretaClient {
getSubjectAverageStreamFL = false;
}
Stream<ApiResponse<List<Homework>>> getHomeworkStream(
{bool cacheOnly = true}) async* {
Stream<ApiResponse<List<Homework>>> getHomeworkStream({
bool cacheOnly = true,
}) async* {
while (getHomeworkStreamFL) {
await Future.delayed(Duration(milliseconds: 10));
}
@@ -121,8 +128,10 @@ extension KretaStream on KretaClient {
}
Stream<ApiResponse<List<Lesson>>> getTimeTableStream(
DateTime from, DateTime to,
{bool cacheOnly = true}) async* {
DateTime from,
DateTime to, {
bool cacheOnly = true,
}) async* {
while (getTimeTableStreamFL) {
await Future.delayed(Duration(milliseconds: 10));
}
@@ -135,8 +144,9 @@ extension KretaStream on KretaClient {
getTimeTableStreamFL = false;
}
Stream<ApiResponse<List<Test>>> getTestsStream(
{bool cacheOnly = true}) async* {
Stream<ApiResponse<List<Test>>> getTestsStream({
bool cacheOnly = true,
}) async* {
while (getTestsStreamFL) {
await Future.delayed(Duration(milliseconds: 10));
}
@@ -149,8 +159,9 @@ extension KretaStream on KretaClient {
getTestsStreamFL = false;
}
Stream<ApiResponse<List<Omission>>> getOmissionsStream(
{bool cacheOnly = true}) async* {
Stream<ApiResponse<List<Omission>>> getOmissionsStream({
bool cacheOnly = true,
}) async* {
while (getOmissionsStreamFL) {
await Future.delayed(Duration(milliseconds: 10));
}

View File

@@ -55,7 +55,8 @@ class LiveActivityBackendClient {
'roomName': lesson.roomName,
'isSubstitution': lesson.substituteTeacher != null,
'substituteTeacher': lesson.substituteTeacher,
'isCancelled': lesson.state.name?.toLowerCase().contains('elmarad') ?? false,
'isCancelled':
lesson.state.name?.toLowerCase().contains('elmarad') ?? false,
'lastModified': validLastModified.toIso8601String(),
};
}).toList();
@@ -86,7 +87,9 @@ class LiveActivityBackendClient {
requestData['liveActivityEnabled'] = liveActivityEnabled;
}
_logger.info('Registering device with backend. Sending ${lessonsData.length} lessons.');
_logger.info(
'Registering device with backend. Sending ${lessonsData.length} lessons.',
);
if (_logger.isLoggable(Level.FINE)) {
for (var lesson in lessonsData) {
_logger.fine(' Lesson data: $lesson');
@@ -99,7 +102,9 @@ class LiveActivityBackendClient {
);
if (response.statusCode == 200 || response.statusCode == 201) {
_logger.info('Device registered successfully with ${timetable.length} lessons');
_logger.info(
'Device registered successfully with ${timetable.length} lessons',
);
return true;
}
@@ -136,12 +141,15 @@ class LiveActivityBackendClient {
'roomName': lesson.roomName,
'isSubstitution': lesson.substituteTeacher != null,
'substituteTeacher': lesson.substituteTeacher,
'isCancelled': lesson.state.name?.toLowerCase().contains('elmarad') ?? false,
'isCancelled':
lesson.state.name?.toLowerCase().contains('elmarad') ?? false,
'lastModified': validLastModified.toIso8601String(),
};
}).toList();
_logger.info('Updating timetable with backend. Sending ${lessonsData.length} lessons.');
_logger.info(
'Updating timetable with backend. Sending ${lessonsData.length} lessons.',
);
if (_logger.isLoggable(Level.FINE)) {
for (var lesson in lessonsData) {
_logger.fine(' Lesson data: $lesson');
@@ -177,15 +185,11 @@ class LiveActivityBackendClient {
}
/// Unregister device (called when user logs out)
Future<bool> unregisterDevice({
required String deviceToken,
}) async {
Future<bool> unregisterDevice({required String deviceToken}) async {
try {
final response = await _dio.delete(
'/live-activity/unregister',
data: {
'deviceToken': deviceToken,
},
data: {'deviceToken': deviceToken},
);
if (response.statusCode == 200) {
@@ -228,15 +232,11 @@ class LiveActivityBackendClient {
}
/// Get current timetable from backend
Future<List<Lesson>?> getTimetable({
required String deviceToken,
}) async {
Future<List<Lesson>?> getTimetable({required String deviceToken}) async {
try {
final response = await _dio.get(
'/live-activity/timetable',
queryParameters: {
'deviceToken': deviceToken,
},
queryParameters: {'deviceToken': deviceToken},
);
if (response.statusCode == 200 && response.data is Map) {
@@ -261,10 +261,7 @@ class LiveActivityBackendClient {
try {
final response = await _dio.post(
'/live-activity/push-token',
data: {
'deviceToken': deviceToken,
'pushToken': pushToken,
},
data: {'deviceToken': deviceToken, 'pushToken': pushToken},
);
if (response.statusCode == 200) {
@@ -288,10 +285,7 @@ class LiveActivityBackendClient {
try {
final response = await _dio.post(
'/live-activity/apns-token',
data: {
'deviceToken': deviceToken,
'apnsPushToken': apnsPushToken,
},
data: {'deviceToken': deviceToken, 'apnsPushToken': apnsPushToken},
);
if (response.statusCode == 200) {
@@ -299,7 +293,9 @@ class LiveActivityBackendClient {
return true;
}
_logger.warning('Failed to update APNs push token: ${response.statusCode}');
_logger.warning(
'Failed to update APNs push token: ${response.statusCode}',
);
return false;
} catch (e) {
_logger.severe('Error updating APNs push token: $e');
@@ -308,15 +304,11 @@ class LiveActivityBackendClient {
}
/// Send a test notification (for debugging)
Future<bool> sendTestNotification({
required String deviceToken,
}) async {
Future<bool> sendTestNotification({required String deviceToken}) async {
try {
final response = await _dio.post(
'/live-activity/test-notification',
data: {
'deviceToken': deviceToken,
},
data: {'deviceToken': deviceToken},
);
if (response.statusCode == 200) {
@@ -324,7 +316,9 @@ class LiveActivityBackendClient {
return true;
}
_logger.warning('Failed to send test notification: ${response.statusCode}');
_logger.warning(
'Failed to send test notification: ${response.statusCode}',
);
return false;
} catch (e) {
_logger.severe('Error sending test notification: $e');
@@ -340,10 +334,7 @@ class LiveActivityBackendClient {
try {
final response = await _dio.put(
'/live-activity/language',
data: {
'deviceToken': deviceToken,
'language': language,
},
data: {'deviceToken': deviceToken, 'language': language},
);
if (response.statusCode == 200) {
@@ -367,10 +358,7 @@ class LiveActivityBackendClient {
try {
final response = await _dio.put(
'/live-activity/bell-delay',
data: {
'deviceToken': deviceToken,
'bellDelay': bellDelay,
},
data: {'deviceToken': deviceToken, 'bellDelay': bellDelay},
);
if (response.statusCode == 200) {
@@ -397,17 +385,25 @@ class LiveActivityBackendClient {
'/live-activity/morning-notification',
data: {
'deviceToken': deviceToken,
if (morningNotificationTime != null) 'morningNotificationTime': morningNotificationTime,
if (morningNotificationEnabled != null) 'morningNotificationEnabled': morningNotificationEnabled,
...?(morningNotificationTime != null
? {'morningNotificationTime': morningNotificationTime}
: null),
...?(morningNotificationEnabled != null
? {'morningNotificationEnabled': morningNotificationEnabled}
: null),
},
);
if (response.statusCode == 200) {
_logger.info('Morning notification settings updated successfully: enabled=$morningNotificationEnabled, time=$morningNotificationTime');
_logger.info(
'Morning notification settings updated successfully: enabled=$morningNotificationEnabled, time=$morningNotificationTime',
);
return true;
}
_logger.warning('Failed to update morning notification settings: ${response.statusCode}');
_logger.warning(
'Failed to update morning notification settings: ${response.statusCode}',
);
return false;
} catch (e) {
_logger.severe('Error updating morning notification settings: $e');
@@ -430,7 +426,9 @@ class LiveActivityBackendClient {
);
if (response.statusCode == 200) {
_logger.info('Live Activity ${liveActivityEnabled ? "enabled" : "disabled"} successfully');
_logger.info(
'Live Activity ${liveActivityEnabled ? "enabled" : "disabled"} successfully',
);
return true;
}
@@ -442,4 +440,3 @@ class LiveActivityBackendClient {
}
}
}

View File

@@ -117,7 +117,7 @@ class KretaEndpoints {
static String getTests(String iss) =>
"${kreta(iss)}/ellenorzo/v3/sajat/BejelentettSzamonkeresek";
static String getLessons(String iss) =>
"${kreta(iss)}/dktapi/intezmenyek/munkaterek/tanulok";
}
}

View File

@@ -62,67 +62,66 @@ class AllLessons {
});
factory AllLessons.fromJson(Map<String, dynamic> json) => AllLessons(
schoolId: json['intezmenyId']?.toString() ?? '',
yearId: json['tanevId']?.toString() ?? '',
classId: json['osztalyId'],
className: json['osztalyNev']?.toString(),
classWorkspace: json['osztalyMunkaTer'] == true,
groupId: json['csoportId'],
groupName: json['csoportNev']?.toString(),
groupWorkspace: json['csoportMunkaTer'] == true,
groupWorkspaceName: json['osztalyCsoportNev']?.toString() ?? '',
subjectId: json['tantargyId'],
subjectName: json['tantargyNev']?.toString() ?? '',
teacherId: json['alkalmazottId'],
teacherGuid: json['alkalmazottGuid']?.toString() ?? '',
teacherName: json['alkalmazottNev']?.toString() ?? '',
teacherAnnoId: json['alkalmazottUzenoFalId'],
annoId: json['uzenoFalId'],
languageId: json['nyelvId']?.toString(),
subjectCategoryId: json['tantargyKategoriaId'],
subjectCategoryName: json['tantargyKategoriaNev']?.toString() ?? '',
typeId: json['tipusId'],
typeName: json['tipusNev']?.toString() ?? '',
gradeTypeId: json['evfolyamTipusId'],
gradeTypeName: json['evfolyamTipusNev']?.toString() ?? '',
taskPlaceId: json['feladatEllatasiHelyId'],
taskPlaceName: json['feladatEllatasiHelyNev']?.toString() ?? '',
teacherAvatarTypeId: json['alkalmazottAvatarTipusId'],
teacherAvatarTypePath:
json['alkalmazottAvatarEleres']?.toString() ?? '',
taskGroupId: json['oraiFeladatGroupId'],
);
schoolId: json['intezmenyId']?.toString() ?? '',
yearId: json['tanevId']?.toString() ?? '',
classId: json['osztalyId'],
className: json['osztalyNev']?.toString(),
classWorkspace: json['osztalyMunkaTer'] == true,
groupId: json['csoportId'],
groupName: json['csoportNev']?.toString(),
groupWorkspace: json['csoportMunkaTer'] == true,
groupWorkspaceName: json['osztalyCsoportNev']?.toString() ?? '',
subjectId: json['tantargyId'],
subjectName: json['tantargyNev']?.toString() ?? '',
teacherId: json['alkalmazottId'],
teacherGuid: json['alkalmazottGuid']?.toString() ?? '',
teacherName: json['alkalmazottNev']?.toString() ?? '',
teacherAnnoId: json['alkalmazottUzenoFalId'],
annoId: json['uzenoFalId'],
languageId: json['nyelvId']?.toString(),
subjectCategoryId: json['tantargyKategoriaId'],
subjectCategoryName: json['tantargyKategoriaNev']?.toString() ?? '',
typeId: json['tipusId'],
typeName: json['tipusNev']?.toString() ?? '',
gradeTypeId: json['evfolyamTipusId'],
gradeTypeName: json['evfolyamTipusNev']?.toString() ?? '',
taskPlaceId: json['feladatEllatasiHelyId'],
taskPlaceName: json['feladatEllatasiHelyNev']?.toString() ?? '',
teacherAvatarTypeId: json['alkalmazottAvatarTipusId'],
teacherAvatarTypePath: json['alkalmazottAvatarEleres']?.toString() ?? '',
taskGroupId: json['oraiFeladatGroupId'],
);
Map<String, dynamic> toJson() => {
'intezmenyId': schoolId,
'tanevId': yearId,
'osztalyId': classId,
'osztalyNev': className,
'osztalyMunkaTer': classWorkspace,
'csoportId': groupId,
'csoportNev': groupName,
'csoportMunkaTer': groupWorkspace,
'osztalyCsoportNev': groupWorkspaceName,
'tantargyId': subjectId,
'tantargyNev': subjectName,
'alkalmazottId': teacherId,
'alkalmazottGuid': teacherGuid,
'alkalmazottNev': teacherName,
'alkalmazottUzenoFalId': teacherAnnoId,
'uzenoFalId': annoId,
'nyelvId': languageId,
'tantargyKategoriaId': subjectCategoryId,
'tantargyKategoriaNev': subjectCategoryName,
'tipusId': typeId,
'tipusNev': typeName,
'evfolyamTipusId': gradeTypeId,
'evfolyamTipusNev': gradeTypeName,
'feladatEllatasiHelyId': taskPlaceId,
'feladatEllatasiHelyNev': taskPlaceName,
'alkalmazottAvatarTipusId': teacherAvatarTypeId,
'alkalmazottAvatarEleres': teacherAvatarTypePath,
'oraiFeladatGroupId': taskGroupId,
};
'intezmenyId': schoolId,
'tanevId': yearId,
'osztalyId': classId,
'osztalyNev': className,
'osztalyMunkaTer': classWorkspace,
'csoportId': groupId,
'csoportNev': groupName,
'csoportMunkaTer': groupWorkspace,
'osztalyCsoportNev': groupWorkspaceName,
'tantargyId': subjectId,
'tantargyNev': subjectName,
'alkalmazottId': teacherId,
'alkalmazottGuid': teacherGuid,
'alkalmazottNev': teacherName,
'alkalmazottUzenoFalId': teacherAnnoId,
'uzenoFalId': annoId,
'nyelvId': languageId,
'tantargyKategoriaId': subjectCategoryId,
'tantargyKategoriaNev': subjectCategoryName,
'tipusId': typeId,
'tipusNev': typeName,
'evfolyamTipusId': gradeTypeId,
'evfolyamTipusNev': gradeTypeName,
'feladatEllatasiHelyId': taskPlaceId,
'feladatEllatasiHelyNev': taskPlaceName,
'alkalmazottAvatarTipusId': teacherAvatarTypeId,
'alkalmazottAvatarEleres': teacherAvatarTypePath,
'oraiFeladatGroupId': taskGroupId,
};
}
List<AllLessons> lessonsFromJson(String str) =>

View File

@@ -11,34 +11,36 @@ class ClassGroup {
final bool isActive;
final String type;
ClassGroup(
{required this.uid,
required this.name,
required this.headTeacher,
required this.substituteHeadTeacher,
required this.studyGroup,
required this.studyGroupSortIndex,
required this.studyTask,
required this.isActive,
required this.type});
ClassGroup({
required this.uid,
required this.name,
required this.headTeacher,
required this.substituteHeadTeacher,
required this.studyGroup,
required this.studyGroupSortIndex,
required this.studyTask,
required this.isActive,
required this.type,
});
factory ClassGroup.fromJson(Map<String, dynamic> json) {
return ClassGroup(
uid: json['Uid'],
name: json['Nev'],
headTeacher: json['OsztalyFonok'] != null
? UidObj.fromJson(json['OsztalyFonok'])
: null,
substituteHeadTeacher: json['OsztalyFonokHelyettes'] != null
? UidObj.fromJson(json['OsztalyFonokHelyettes'])
: null,
studyGroup: NameUidDesc.fromJson(json['OktatasNevelesiKategoria']),
studyGroupSortIndex: json['OktatasNevelesiKategoriaSortIndex'],
studyTask: json['OktatasNevelesiFeladat'] != null
? NameUidDesc.fromJson(json['OktatasNevelesiFeladat'])
: null,
isActive: json['IsAktiv'],
type: json['Tipus']);
uid: json['Uid'],
name: json['Nev'],
headTeacher: json['OsztalyFonok'] != null
? UidObj.fromJson(json['OsztalyFonok'])
: null,
substituteHeadTeacher: json['OsztalyFonokHelyettes'] != null
? UidObj.fromJson(json['OsztalyFonokHelyettes'])
: null,
studyGroup: NameUidDesc.fromJson(json['OktatasNevelesiKategoria']),
studyGroupSortIndex: json['OktatasNevelesiKategoriaSortIndex'],
studyTask: json['OktatasNevelesiFeladat'] != null
? NameUidDesc.fromJson(json['OktatasNevelesiFeladat'])
: null,
isActive: json['IsAktiv'],
type: json['Tipus'],
);
}
@override

View File

@@ -3,12 +3,18 @@ class NameUidDesc {
final String? name;
final String? description;
NameUidDesc(
{required this.uid, required this.name, required this.description});
NameUidDesc({
required this.uid,
required this.name,
required this.description,
});
factory NameUidDesc.fromJson(Map<String, dynamic> json) {
return NameUidDesc(
uid: json['Uid'], name: json['Nev'], description: json['Leiras']);
uid: json['Uid'],
name: json['Nev'],
description: json['Leiras'],
);
}
Map<String, dynamic> toJson() {
@@ -29,23 +35,14 @@ class NameUid {
final String uid;
final String name;
NameUid({
required this.uid,
required this.name,
});
NameUid({required this.uid, required this.name});
factory NameUid.fromJson(Map<String, dynamic> json) {
return NameUid(
uid: json['Uid'],
name: json['Nev'],
);
return NameUid(uid: json['Uid'], name: json['Nev']);
}
Map<String, dynamic> toJson() {
return {
'Uid': uid,
'Nev': name,
};
return {'Uid': uid, 'Nev': name};
}
}
@@ -55,9 +52,7 @@ class UidObj {
UidObj({required this.uid});
factory UidObj.fromJson(Map<String, dynamic> json) {
return UidObj(
uid: json['Uid'],
);
return UidObj(uid: json['Uid']);
}
@override

View File

@@ -20,24 +20,25 @@ class Grade {
final UidObj? classGroup;
final int sortIndex;
Grade(
{required this.uid,
required this.recordDate,
required this.creationDate,
this.ackDate,
required this.subject,
this.topic,
required this.type,
this.mode,
required this.valueType,
required this.teacher,
this.kind,
this.numericValue,
required this.strValue,
this.weightPercentage,
this.shortStrValue,
this.classGroup,
required this.sortIndex});
Grade({
required this.uid,
required this.recordDate,
required this.creationDate,
this.ackDate,
required this.subject,
this.topic,
required this.type,
this.mode,
required this.valueType,
required this.teacher,
this.kind,
this.numericValue,
required this.strValue,
this.weightPercentage,
this.shortStrValue,
this.classGroup,
required this.sortIndex,
});
factory Grade.fromJson(Map<String, dynamic> json) {
return Grade(

View File

@@ -5,20 +5,22 @@ class Guardian {
final String? phoneNumber;
final String uid;
Guardian(
{required this.email,
required this.isLegalRepresentative,
required this.name,
required this.phoneNumber,
required this.uid});
Guardian({
required this.email,
required this.isLegalRepresentative,
required this.name,
required this.phoneNumber,
required this.uid,
});
factory Guardian.fromJson(Map<String, dynamic> json) {
return Guardian(
email: json['EmailCim'],
isLegalRepresentative: json['IsTorvenyesKepviselo'],
name: json['Nev'],
phoneNumber: json['Telefonszam'],
uid: json['Uid']);
email: json['EmailCim'],
isLegalRepresentative: json['IsTorvenyesKepviselo'],
name: json['Nev'],
phoneNumber: json['Telefonszam'],
uid: json['Uid'],
);
}
@override

View File

@@ -17,36 +17,38 @@ class Homework {
final UidObj classGroup;
final bool canAttach;
Homework(
{required this.uid,
required this.subject,
required this.subjectName,
required this.teacherName,
required this.description,
required this.startDate,
required this.dueDate,
required this.creationDate,
required this.isCreatedByTeacher,
required this.isDone,
required this.canBeSubmitted,
required this.classGroup,
required this.canAttach});
Homework({
required this.uid,
required this.subject,
required this.subjectName,
required this.teacherName,
required this.description,
required this.startDate,
required this.dueDate,
required this.creationDate,
required this.isCreatedByTeacher,
required this.isDone,
required this.canBeSubmitted,
required this.classGroup,
required this.canAttach,
});
factory Homework.fromJson(Map<String, dynamic> json) {
return Homework(
uid: json["Uid"],
subject: Subject.fromJson(json["Tantargy"]),
subjectName: json["TantargyNeve"],
teacherName: json["RogzitoTanarNeve"],
description: json["Szoveg"],
startDate: DateTime.parse(json["FeladasDatuma"]).toLocal(),
dueDate: DateTime.parse(json["HataridoDatuma"]).toLocal(),
creationDate: DateTime.parse(json["RogzitesIdopontja"]).toLocal(),
isCreatedByTeacher: json["IsTanarRogzitette"],
isDone: json["IsMegoldva"],
canBeSubmitted: json["IsBeadhato"],
classGroup: UidObj.fromJson(json["OsztalyCsoport"]),
canAttach: json["IsCsatolasEngedelyezes"]);
uid: json["Uid"],
subject: Subject.fromJson(json["Tantargy"]),
subjectName: json["TantargyNeve"],
teacherName: json["RogzitoTanarNeve"],
description: json["Szoveg"],
startDate: DateTime.parse(json["FeladasDatuma"]).toLocal(),
dueDate: DateTime.parse(json["HataridoDatuma"]).toLocal(),
creationDate: DateTime.parse(json["RogzitesIdopontja"]).toLocal(),
isCreatedByTeacher: json["IsTanarRogzitette"],
isDone: json["IsMegoldva"],
canBeSubmitted: json["IsBeadhato"],
classGroup: UidObj.fromJson(json["OsztalyCsoport"]),
canAttach: json["IsCsatolasEngedelyezes"],
);
}
@override

View File

@@ -4,11 +4,12 @@ class Institution {
final List<SystemModule> systemModuleList;
final String uid;
Institution(
{required this.customizationSettings,
required this.shortName,
required this.systemModuleList,
required this.uid});
Institution({
required this.customizationSettings,
required this.shortName,
required this.systemModuleList,
required this.uid,
});
factory Institution.fromJson(Map<String, dynamic> json) {
var systemModuleList = List<SystemModule>.empty(growable: true);
@@ -18,8 +19,9 @@ class Institution {
}
return Institution(
customizationSettings:
CustomizationSettings.fromJson(json['TestreszabasBeallitasok']),
customizationSettings: CustomizationSettings.fromJson(
json['TestreszabasBeallitasok'],
),
shortName: json['RovidNev'],
systemModuleList: systemModuleList,
uid: json['Uid'],
@@ -33,19 +35,21 @@ class CustomizationSettings {
final bool isLessonsThemeVisible;
final String nextServerDeployAsString;
CustomizationSettings(
{required this.delayForNotifications,
required this.isClassAverageVisible,
required this.isLessonsThemeVisible,
required this.nextServerDeployAsString});
CustomizationSettings({
required this.delayForNotifications,
required this.isClassAverageVisible,
required this.isLessonsThemeVisible,
required this.nextServerDeployAsString,
});
factory CustomizationSettings.fromJson(Map<String, dynamic> json) {
return CustomizationSettings(
delayForNotifications:
json['ErtekelesekMegjelenitesenekKesleltetesenekMerteke'],
isClassAverageVisible: json['IsOsztalyAtlagMegjeleniteseEllenorzoben'],
isLessonsThemeVisible: json['IsTanorakTemajaMegtekinthetoEllenorzoben'],
nextServerDeployAsString: json['KovetkezoTelepitesDatuma']);
delayForNotifications:
json['ErtekelesekMegjelenitesenekKesleltetesenekMerteke'],
isClassAverageVisible: json['IsOsztalyAtlagMegjeleniteseEllenorzoben'],
isLessonsThemeVisible: json['IsTanorakTemajaMegtekinthetoEllenorzoben'],
nextServerDeployAsString: json['KovetkezoTelepitesDatuma'],
);
}
@override
@@ -68,7 +72,10 @@ class SystemModule {
factory SystemModule.fromJson(Map<String, dynamic> json) {
return SystemModule(
isActive: json['IsAktiv'], type: json['Tipus'], url: json['Url']);
isActive: json['IsAktiv'],
type: json['Tipus'],
url: json['Url'],
);
}
@override

View File

@@ -9,24 +9,26 @@ class NoticeBoardItem {
final String contentHTML;
final String contentText;
NoticeBoardItem(
{required this.uid,
required this.author,
required this.validFrom,
required this.validTo,
required this.title,
required this.contentHTML,
required this.contentText});
NoticeBoardItem({
required this.uid,
required this.author,
required this.validFrom,
required this.validTo,
required this.title,
required this.contentHTML,
required this.contentText,
});
factory NoticeBoardItem.fromJson(Map<String, dynamic> json) {
return NoticeBoardItem(
uid: json['Uid'],
author: json['RogzitoNeve'],
validFrom: DateTime.parse(json['ErvenyessegKezdete']),
validTo: DateTime.parse(json['ErvenyessegVege']),
title: json['Cim'],
contentHTML: json['Tartalom'],
contentText: json['TartalomText']);
uid: json['Uid'],
author: json['RogzitoNeve'],
validFrom: DateTime.parse(json['ErvenyessegKezdete']),
validTo: DateTime.parse(json['ErvenyessegVege']),
title: json['Cim'],
contentHTML: json['Tartalom'],
contentText: json['TartalomText'],
);
}
@override
@@ -53,26 +55,28 @@ class InfoBoardItem {
final String contentText;
final NameUidDesc type;
InfoBoardItem(
{required this.uid,
required this.title,
required this.date,
required this.author,
required this.createdAt,
required this.contentHTML,
required this.contentText,
required this.type});
InfoBoardItem({
required this.uid,
required this.title,
required this.date,
required this.author,
required this.createdAt,
required this.contentHTML,
required this.contentText,
required this.type,
});
factory InfoBoardItem.fromJson(Map<String, dynamic> json) {
return InfoBoardItem(
uid: json['Uid'],
title: json['Cim'],
date: DateTime.parse(json['Datum']),
author: json['KeszitoTanarNeve'],
createdAt: DateTime.parse(json['KeszitesDatuma']),
contentText: json['Tartalom'],
contentHTML: json['TartalomFormazott'],
type: NameUidDesc.fromJson(json['Tipus']));
uid: json['Uid'],
title: json['Cim'],
date: DateTime.parse(json['Datum']),
author: json['KeszitoTanarNeve'],
createdAt: DateTime.parse(json['KeszitesDatuma']),
contentText: json['Tartalom'],
contentHTML: json['TartalomFormazott'],
type: NameUidDesc.fromJson(json['Tipus']),
);
}
@override

View File

@@ -75,11 +75,7 @@ class Class {
final DateTime end;
final int classNo;
Class({
required this.start,
required this.end,
required this.classNo,
});
Class({required this.start, required this.end, required this.classNo});
factory Class.fromJson(Map<String, dynamic> json) {
return Class(

View File

@@ -25,22 +25,23 @@ class Student {
final Institution institution;
Student(
{required this.addressDataList,
required this.bankAccount,
// required this.yearOfBirth,
// required this.monthOfBirth,
// required this.dayOfBirth,
required this.birthdate,
required this.emailAddress,
required this.name,
required this.phoneNumber,
required this.schoolYearUID,
required this.uid,
required this.guardianList,
required this.instituteCode,
required this.instituteName,
required this.institution});
Student({
required this.addressDataList,
required this.bankAccount,
// required this.yearOfBirth,
// required this.monthOfBirth,
// required this.dayOfBirth,
required this.birthdate,
required this.emailAddress,
required this.name,
required this.phoneNumber,
required this.schoolYearUID,
required this.uid,
required this.guardianList,
required this.instituteCode,
required this.instituteName,
required this.institution,
});
factory Student.fromJson(Map<String, dynamic> json) {
var guardianList = List<Guardian>.empty(growable: true);
@@ -50,19 +51,21 @@ class Student {
}
return Student(
addressDataList: listToTyped<String>(json['Cimek']),
bankAccount: BankAccount.fromJson(json['Bankszamla']),
birthdate: DateFormat('yyyy-M-d').parse(
"${json['SzuletesiEv']}-${json['SzuletesiHonap']}-${json['SzuletesiNap']}"),
emailAddress: json['EmailCim'],
name: json['Nev'],
phoneNumber: json['Telefonszam'],
schoolYearUID: json['TanevUid'],
uid: json['Uid'],
guardianList: guardianList,
instituteCode: json['IntezmenyAzonosito'],
instituteName: json['IntezmenyNev'],
institution: Institution.fromJson(json['Intezmeny']));
addressDataList: listToTyped<String>(json['Cimek']),
bankAccount: BankAccount.fromJson(json['Bankszamla']),
birthdate: DateFormat('yyyy-M-d').parse(
"${json['SzuletesiEv']}-${json['SzuletesiHonap']}-${json['SzuletesiNap']}",
),
emailAddress: json['EmailCim'],
name: json['Nev'],
phoneNumber: json['Telefonszam'],
schoolYearUID: json['TanevUid'],
uid: json['Uid'],
guardianList: guardianList,
instituteCode: json['IntezmenyAzonosito'],
instituteName: json['IntezmenyNev'],
institution: Institution.fromJson(json['Intezmeny']),
);
}
@override
@@ -89,18 +92,20 @@ class BankAccount {
final String? ownerName;
final int? ownerType;
BankAccount(
{required this.accountNumber,
required this.isReadOnly,
required this.ownerName,
required this.ownerType});
BankAccount({
required this.accountNumber,
required this.isReadOnly,
required this.ownerName,
required this.ownerType,
});
factory BankAccount.fromJson(Map<String, dynamic> json) {
return BankAccount(
accountNumber: json['BankszamlaSzam'],
isReadOnly: json['IsReadOnly'],
ownerName: json['BankszamlaTulajdonosNeve'],
ownerType: json['BankszamlaTulajdonosTipusId']);
accountNumber: json['BankszamlaSzam'],
isReadOnly: json['IsReadOnly'],
ownerName: json['BankszamlaTulajdonosNeve'],
ownerType: json['BankszamlaTulajdonosTipusId'],
);
}
@override

View File

@@ -83,8 +83,9 @@ class Lesson {
? NameUid.fromJson(json['OsztalyCsoport'])
: null,
teacher: json['TanarNeve'],
subject:
json['Tantargy'] != null ? Subject.fromJson(json['Tantargy']) : null,
subject: json['Tantargy'] != null
? Subject.fromJson(json['Tantargy'])
: null,
theme: json['Tema'],
roomName: json['TeremNeve'],
type: NameUidDesc.fromJson(json['Tipus']),
@@ -105,8 +106,8 @@ class Lesson {
digitalPlatformType: json['DigitalisPlatformTipus'],
digitalSupportDeviceTypeList:
json['DigitalisTamogatoEszkozTipusList'] != null
? List<String>.from(json['DigitalisTamogatoEszkozTipusList'])
: List<String>.empty(),
? List<String>.from(json['DigitalisTamogatoEszkozTipusList'])
: List<String>.empty(),
createdAt: DateTime.parse(json['Letrehozas']).toLocal(),
lastModifiedAt: DateTime.parse(json['UtolsoModositas']).toLocal(),
);

View File

@@ -6,22 +6,24 @@ class TokenGrantResponse {
final String refreshToken;
final String scope;
TokenGrantResponse(
{required this.idToken,
required this.accessToken,
required this.expiresIn,
required this.tokenType,
required this.refreshToken,
required this.scope});
TokenGrantResponse({
required this.idToken,
required this.accessToken,
required this.expiresIn,
required this.tokenType,
required this.refreshToken,
required this.scope,
});
factory TokenGrantResponse.fromJson(Map<String, dynamic> json) {
return TokenGrantResponse(
idToken: json['id_token'],
accessToken: json['access_token'],
expiresIn: json['expires_in'],
tokenType: json['token_type'],
refreshToken: json['refresh_token'],
scope: json['scope']);
idToken: json['id_token'],
accessToken: json['access_token'],
expiresIn: json['expires_in'],
tokenType: json['token_type'],
refreshToken: json['refresh_token'],
scope: json['scope'],
);
}
@override

View File

@@ -23,8 +23,11 @@ Future<TokenGrantResponse> getAccessToken(String code) async {
};
try {
final response = await dio.post(KretaEndpoints.tokenGrantUrl,
options: Options(headers: headers), data: formData);
final response = await dio.post(
KretaEndpoints.tokenGrantUrl,
options: Options(headers: headers),
data: formData,
);
switch (response.statusCode) {
case 200:
@@ -33,7 +36,8 @@ Future<TokenGrantResponse> getAccessToken(String code) async {
throw Exception("Invalid grant");
default:
throw Exception(
"Failed to get access token, response code: ${response.statusCode}");
"Failed to get access token, response code: ${response.statusCode}",
);
}
} catch (e) {
rethrow;
@@ -44,7 +48,8 @@ const _tokenRefreshRetryDelays = [1000, 3000, 5000];
Future<TokenGrantResponse> extendToken(TokenModel model) async {
logger.info(
"Extending token for user: ${model.studentId}, institute: ${model.iss}");
"Extending token for user: ${model.studentId}, institute: ${model.iss}",
);
final headers = <String, String>{
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
@@ -66,30 +71,38 @@ Future<TokenGrantResponse> extendToken(TokenModel model) async {
if (attempt > 0) {
final delay = _tokenRefreshRetryDelays[attempt - 1];
logger.info(
"Token refresh attempt ${attempt + 1}, waiting ${delay}ms...");
"Token refresh attempt ${attempt + 1}, waiting ${delay}ms...",
);
await Future.delayed(Duration(milliseconds: delay));
}
final response = await dio.post(KretaEndpoints.tokenGrantUrl,
options: Options(headers: headers), data: formData);
final response = await dio.post(
KretaEndpoints.tokenGrantUrl,
options: Options(headers: headers),
data: formData,
);
switch (response.statusCode) {
case 200:
logger
.info("Token extended successfully for user: ${model.studentId}");
logger.info(
"Token extended successfully for user: ${model.studentId}",
);
return TokenGrantResponse.fromJson(response.data);
case 400:
case 401:
logger.warning(
"Token refresh failed (${response.statusCode}) - refresh token invalid for user: ${model.studentId}");
"Token refresh failed (${response.statusCode}) - refresh token invalid for user: ${model.studentId}",
);
throw response.statusCode == 400
? TokenExpiredException()
: InvalidGrantException();
default:
logger.warning(
"Token refresh failed (${response.statusCode}) for user: ${model.studentId}, attempt ${attempt + 1}");
"Token refresh failed (${response.statusCode}) for user: ${model.studentId}, attempt ${attempt + 1}",
);
lastError = Exception(
"Failed to get access token, response code: ${response.statusCode}");
"Failed to get access token, response code: ${response.statusCode}",
);
// Continue to retry for network errors
continue;
}
@@ -99,7 +112,8 @@ Future<TokenGrantResponse> extendToken(TokenModel model) async {
rethrow;
} on DioException catch (e) {
logger.warning(
"Token refresh network error for user: ${model.studentId}, attempt ${attempt + 1}: $e");
"Token refresh network error for user: ${model.studentId}, attempt ${attempt + 1}: $e",
);
lastError = e;
continue;
} catch (e) {
@@ -109,7 +123,8 @@ Future<TokenGrantResponse> extendToken(TokenModel model) async {
}
}
logger
.severe("All token refresh attempts failed for user: ${model.studentId}");
logger.severe(
"All token refresh attempts failed for user: ${model.studentId}",
);
throw lastError ?? Exception("Token refresh failed after all retries");
}

View File

@@ -20,4 +20,4 @@ double calculateAverage(List<Grade> sortedGrades) {
final avg = weightedSum / totalWeight;
return double.parse(avg.toStringAsFixed(2));
}
}

View File

@@ -14,7 +14,9 @@ class IOSWidgetHelper {
if (!Platform.isIOS) return null;
try {
final result = await _channel.invokeMethod<String>('getAppGroupDirectory');
final result = await _channel.invokeMethod<String>(
'getAppGroupDirectory',
);
if (result != null) {
return Directory(result);
}
@@ -39,8 +41,12 @@ class IOSWidgetHelper {
if (!Platform.isIOS) return;
debugPrint('[IOSWidget] Starting updateWidgetData...');
debugPrint('[IOSWidget] todayLessons: ${todayLessons.length}, tomorrowLessons: ${tomorrowLessons.length}');
debugPrint('[IOSWidget] grades: ${grades.length}, subjectAverages: ${subjectAverages.length}');
debugPrint(
'[IOSWidget] todayLessons: ${todayLessons.length}, tomorrowLessons: ${tomorrowLessons.length}',
);
debugPrint(
'[IOSWidget] grades: ${grades.length}, subjectAverages: ${subjectAverages.length}',
);
final dir = await _getAppGroupDirectory();
if (dir == null) {
@@ -56,23 +62,31 @@ class IOSWidgetHelper {
'timetable': {
'today': todayLessons.map((l) => _lessonToJson(l)).toList(),
'tomorrow': tomorrowLessons.map((l) => _lessonToJson(l)).toList(),
'nextSchoolDay': nextSchoolDayLessons.map((l) => _lessonToJson(l)).toList(),
'nextSchoolDay': nextSchoolDayLessons
.map((l) => _lessonToJson(l))
.toList(),
'nextSchoolDayDate': nextSchoolDayDate?.toIso8601String(),
'currentBreak': currentBreak != null ? {
'name': currentBreak.name,
'nameKey': currentBreak.nameKey,
'endDate': currentBreak.endDate.toIso8601String(),
} : null,
'currentBreak': currentBreak != null
? {
'name': currentBreak.name,
'nameKey': currentBreak.nameKey,
'endDate': currentBreak.endDate.toIso8601String(),
}
: null,
},
'grades': grades.take(20).map((g) => _gradeToJson(g)).toList(),
'averages': {
'overall': overallAverage,
'subjects': subjectAverages.entries.map((e) => {
'uid': e.key,
'name': _getSubjectNameFromGrades(e.key, grades),
'average': e.value,
'gradeCount': _getGradeCount(e.key, grades),
}).toList(),
'subjects': subjectAverages.entries
.map(
(e) => {
'uid': e.key,
'name': _getSubjectNameFromGrades(e.key, grades),
'average': e.value,
'gradeCount': _getGradeCount(e.key, grades),
},
)
.toList(),
},
};
@@ -118,26 +132,29 @@ class IOSWidgetHelper {
'name': lesson.name,
'lessonNumber': lesson.lessonNumber,
'teacher': lesson.teacher,
'subject': subject != null ? {
'uid': subject.uid,
'name': subject.name,
'category': subject.category != null ? {
'uid': subject.category!.uid,
'name': subject.category!.name,
'description': subject.category!.description,
} : null,
'sortIndex': subject.sortIndex,
'teacherName': subject.teacherName,
} : {
'uid': '',
'name': lesson.name,
'category': null,
'sortIndex': 0,
'teacherName': null,
},
'subject': subject != null
? {
'uid': subject.uid,
'name': subject.name,
'category': {
'uid': subject.category.uid,
'name': subject.category.name,
'description': subject.category.description,
},
'sortIndex': subject.sortIndex,
'teacherName': subject.teacherName,
}
: {
'uid': '',
'name': lesson.name,
'category': null,
'sortIndex': 0,
'teacherName': null,
},
'theme': lesson.theme,
'roomName': lesson.roomName,
'isCancelled': lesson.state.name?.toLowerCase().contains('elmarad') ?? false,
'isCancelled':
lesson.state.name?.toLowerCase().contains('elmarad') ?? false,
'isSubstitution': lesson.substituteTeacher != null,
};
}
@@ -149,11 +166,11 @@ class IOSWidgetHelper {
'subject': {
'uid': grade.subject.uid,
'name': grade.subject.name,
'category': grade.subject.category != null ? {
'uid': grade.subject.category!.uid,
'name': grade.subject.category!.name,
'description': grade.subject.category!.description,
} : null,
'category': {
'uid': grade.subject.category.uid,
'name': grade.subject.category.name,
'description': grade.subject.category.description,
},
'sortIndex': grade.subject.sortIndex,
// Use the grade's teacher field, not subject.teacherName (which is usually null for grades)
'teacherName': grade.teacher,

View File

@@ -12,7 +12,7 @@ enum CacheId {
getClassGroup,
getSubjectAvg,
getLessons,
getHomework
getHomework,
}
@collection

View File

@@ -28,7 +28,7 @@ Future<void> resetOldHomeworkCache(Isar isar) async {
});
}
@collection
@collection
class HomeworkDoneModel {
Id? id;
@@ -36,13 +36,15 @@ class HomeworkDoneModel {
late DateTime doneAt;
HomeworkDoneModel();
}
Future<void> markAsDone(Isar isar, String homeWorkUid) async {
await isar.writeTxn(() async {
await isar.homeworkDoneModels.put(HomeworkDoneModel()
..homeworkId = homeWorkUid
..doneAt = DateTime.now());
await isar.homeworkDoneModels.put(
HomeworkDoneModel()
..homeworkId = homeWorkUid
..doneAt = DateTime.now(),
);
});
}
@@ -63,4 +65,4 @@ Future<bool> isHomeworkDone(Isar isar, String homeWorkUid) async {
.homeworkIdEqualTo(homeWorkUid)
.findFirst();
return existing != null;
}
}

View File

@@ -64,7 +64,8 @@ class TokenModel {
// you would expect all usernames to be numeric
// and for them be the student's student id, but NO
final hash = sha256.convert(utf8.encode(username));
final value = ((hash.bytes[0] << 24) |
final value =
((hash.bytes[0] << 24) |
(hash.bytes[1] << 16) |
(hash.bytes[2] << 8) |
(hash.bytes[3])) >>>

View File

@@ -57,7 +57,9 @@ class WidgetCacheHelper {
}
static Future<void> updateWidgetCache(
FirkaStyle style, KretaClient client) async {
FirkaStyle style,
KretaClient client,
) async {
final dataDir = await getApplicationDocumentsDirectory();
final now = timeNow();
@@ -69,9 +71,12 @@ class WidgetCacheHelper {
final widgetFile = File(p.join(dataDir.path, "widget_state.json"));
if (lessons.response != null) {
debugPrint('Android widget cache: ${lessons.response!.length} lessons (cached: ${lessons.cached})');
debugPrint(
'Android widget cache: ${lessons.response!.length} lessons (cached: ${lessons.cached})',
);
widgetFile.writeAsString(
jsonEncode(WidgetCacheHelper.toJson(style, lessons.response!)));
jsonEncode(WidgetCacheHelper.toJson(style, lessons.response!)),
);
} else {
debugPrint('Android widget cache: No lessons to cache');
}
@@ -105,13 +110,17 @@ class WidgetCacheHelper {
/// Comprehensive iOS widget refresh that collects all necessary data
/// Call this on: app open, user switch, data refresh
static Future<void> refreshIOSWidgets(KretaClient client, SettingsStore settings) async {
static Future<void> refreshIOSWidgets(
KretaClient client,
SettingsStore settings,
) async {
if (!Platform.isIOS) return;
try {
final langIndex = (settings.group("settings").subGroup("application")["language"]
as SettingsItemsRadio)
.activeIndex;
final langIndex =
(settings.group("settings").subGroup("application")["language"]
as SettingsItemsRadio)
.activeIndex;
String locale;
switch (langIndex) {
case 1:
@@ -127,9 +136,10 @@ class WidgetCacheHelper {
locale = 'hu';
}
final themeIndex = (settings.group("settings").subGroup("customization")["theme"]
as SettingsItemsRadio)
.activeIndex;
final themeIndex =
(settings.group("settings").subGroup("customization")["theme"]
as SettingsItemsRadio)
.activeIndex;
String theme;
switch (themeIndex) {
case 1:
@@ -160,7 +170,9 @@ class WidgetCacheHelper {
final todayLessons = todayResponse.response ?? [];
final tomorrowLessons = tomorrowResponse.response ?? [];
debugPrint('iOS widget refresh: ${todayLessons.length} today lessons, ${tomorrowLessons.length} tomorrow lessons');
debugPrint(
'iOS widget refresh: ${todayLessons.length} today lessons, ${tomorrowLessons.length} tomorrow lessons',
);
List<Lesson> nextSchoolDayLessons = [];
DateTime? nextSchoolDayDate;
@@ -176,7 +188,9 @@ class WidgetCacheHelper {
if (dayLessons.isNotEmpty) {
nextSchoolDayLessons = dayLessons;
nextSchoolDayDate = dayMidnight;
debugPrint('iOS widget: Next school day found ${i} days ahead with ${dayLessons.length} lessons');
debugPrint(
'iOS widget: Next school day found $i days ahead with ${dayLessons.length} lessons',
);
break;
}
}
@@ -185,7 +199,9 @@ class WidgetCacheHelper {
final gradesResponse = await client.getGrades(forceCache: false);
final grades = gradesResponse.response ?? [];
debugPrint('iOS widget refresh: ${grades.length} grades fetched (cached: ${gradesResponse.cached})');
debugPrint(
'iOS widget refresh: ${grades.length} grades fetched (cached: ${gradesResponse.cached})',
);
final Map<String, double> subjectAverages = {};
final Set<String> subjectUids = {};
@@ -198,7 +214,9 @@ class WidgetCacheHelper {
int validSubjectCount = 0;
for (var uid in subjectUids) {
final subjectGrades = grades.where((g) => g.subject.uid == uid).toList();
final subjectGrades = grades
.where((g) => g.subject.uid == uid)
.toList();
final avg = _calculateWeightedAverage(subjectGrades);
if (!avg.isNaN && avg > 0) {
subjectAverages[uid] = avg;

View File

@@ -12,29 +12,43 @@ extension TimetableExtension on Iterable<Lesson> {
if (lesson.lessonNumber == null) continue;
if (lessons.firstWhereOrNull(
(lesson2) => lesson.lessonNumber == lesson2.lessonNumber) ==
(lesson2) => lesson.lessonNumber == lesson2.lessonNumber,
) ==
null) {
final ref = reference.start;
final newStart = DateTime(ref.year, ref.month, ref.day,
lesson.start.hour, lesson.start.minute, lesson.start.second);
final newEnd = DateTime(ref.year, ref.month, ref.day, lesson.end.hour,
lesson.end.minute, lesson.end.second);
final newStart = DateTime(
ref.year,
ref.month,
ref.day,
lesson.start.hour,
lesson.start.minute,
lesson.start.second,
);
final newEnd = DateTime(
ref.year,
ref.month,
ref.day,
lesson.end.hour,
lesson.end.minute,
lesson.end.second,
);
final lessonCopy = Lesson(
uid: lesson.uid,
date: lesson.date,
start: newStart,
end: newEnd,
name: lesson.name,
type: lesson.type,
state: lesson.state,
canStudentEditHomework: lesson.canStudentEditHomework,
isHomeworkComplete: lesson.isHomeworkComplete,
attachments: lesson.attachments,
lessonNumber: lesson.lessonNumber,
isDigitalLesson: lesson.isDigitalLesson,
digitalSupportDeviceTypeList: lesson.digitalSupportDeviceTypeList,
createdAt: lesson.createdAt,
lastModifiedAt: lesson.lastModifiedAt);
uid: lesson.uid,
date: lesson.date,
start: newStart,
end: newEnd,
name: lesson.name,
type: lesson.type,
state: lesson.state,
canStudentEditHomework: lesson.canStudentEditHomework,
isHomeworkComplete: lesson.isHomeworkComplete,
attachments: lesson.attachments,
lessonNumber: lesson.lessonNumber,
isDigitalLesson: lesson.isDigitalLesson,
digitalSupportDeviceTypeList: lesson.digitalSupportDeviceTypeList,
createdAt: lesson.createdAt,
lastModifiedAt: lesson.lastModifiedAt,
);
lessons.add(lessonCopy);
}
}
@@ -85,7 +99,7 @@ enum FormatMode {
yyyymmddwedd,
yyyymmmm,
yyyymmdd,
yyyymmddhhmmss
yyyymmddhhmmss,
}
enum Cycle { morning, day, afternoon, night }
@@ -105,7 +119,10 @@ extension DateExtension on DateTime {
switch (mode) {
case FormatMode.grades:
if (isBefore(yesterdayLim)) {
final month = DateFormat('MMMM', l10n.localeName).format(this).firstUpper();
final month = DateFormat(
'MMMM',
l10n.localeName,
).format(this).firstUpper();
final day = DateFormat('d', l10n.localeName).format(this);
return "$month $day";
}
@@ -125,17 +142,23 @@ extension DateExtension on DateTime {
case FormatMode.hmm:
return DateFormat('H:mm', l10n.localeName).format(this);
case FormatMode.welcome:
final dayName = DateFormat('EEEE', l10n.localeName).format(this).firstUpper();
final monthAbbr = DateFormat('MMM', l10n.localeName).format(this).firstUpper();
final dayName = DateFormat(
'EEEE',
l10n.localeName,
).format(this).firstUpper();
final monthAbbr = DateFormat(
'MMM',
l10n.localeName,
).format(this).firstUpper();
final day = DateFormat('d', l10n.localeName).format(this);
return "$dayName, $monthAbbr $day";
case FormatMode.d:
return DateFormat('d', l10n.localeName).format(this);
case FormatMode.da:
return DateFormat('EEEE', l10n.localeName)
.format(this)
.substring(0, 2)
.firstUpper();
return DateFormat(
'EEEE',
l10n.localeName,
).format(this).substring(0, 2).firstUpper();
case FormatMode.dd:
return DateFormat('dd', l10n.localeName).format(this);
case FormatMode.yyyymmddwedd:
@@ -167,12 +190,15 @@ extension DateExtension on DateTime {
}
DateTime getMidnight() {
return subtract(Duration(
return subtract(
Duration(
hours: hour,
minutes: minute,
seconds: second,
milliseconds: millisecond,
microseconds: microsecond));
microseconds: microsecond,
),
);
}
Cycle getDayCycle() {
@@ -199,22 +225,28 @@ extension DateGrouper<T> on Iterable<T> {
Map<DateTime, List<T>> newList = {};
var today = timeNow();
today = today.subtract(Duration(
today = today.subtract(
Duration(
hours: today.hour,
minutes: today.minute,
seconds: today.second,
milliseconds: today.millisecond));
milliseconds: today.millisecond,
),
);
var tomorrow = today.add(Duration(days: 1));
var yesterday = today.subtract(Duration(days: 1));
for (var elem in this) {
var date = getDate(elem);
var day = date.subtract(Duration(
var day = date.subtract(
Duration(
hours: date.hour,
minutes: date.minute,
seconds: date.second,
milliseconds: date.millisecond));
milliseconds: date.millisecond,
),
);
if (date.isAfter(tomorrow.add(Duration(days: 1)))) {
if (newList[day] == null) {
@@ -260,17 +292,20 @@ extension LessonExtension on List<Lesson> {
Lesson? getCurrentLesson(DateTime now) {
return firstWhereOrNull(
(lesson) => now.isAfter(lesson.start) && now.isBefore(lesson.end));
(lesson) => now.isAfter(lesson.start) && now.isBefore(lesson.end),
);
}
Lesson? getPrevLesson(DateTime now) {
return firstWhereOrNull(
(lesson) => lesson.end.isBefore(now.add(Duration(milliseconds: 1))));
(lesson) => lesson.end.isBefore(now.add(Duration(milliseconds: 1))),
);
}
Lesson? getNextLesson(DateTime now) {
return firstWhereOrNull(
(lesson) => lesson.start.isAfter(now.add(Duration(milliseconds: 1))));
(lesson) => lesson.start.isAfter(now.add(Duration(milliseconds: 1))),
);
}
}

View File

@@ -34,8 +34,9 @@ class FirkaBundle extends CachingAssetBundle {
@override
Future<ByteData> load(String key) async {
if (!_compressedBundle) {
logger
.finest("Loading asset from root bundle: assets/flutter_assets/$key");
logger.finest(
"Loading asset from root bundle: assets/flutter_assets/$key",
);
return rootBundle.load(key);
} else {
index ??= await loadIndex();
@@ -43,7 +44,8 @@ class FirkaBundle extends CachingAssetBundle {
final gzip = GZipCodec();
logger.finest(
"Loading asset from firka bundle: assets/flutter_assets/$key");
"Loading asset from firka bundle: assets/flutter_assets/$key",
);
switch (index!["assets/flutter_assets/$key"]!) {
case "b": // brotli
return decode(brotli, await rootBundle.load(key));

View File

@@ -34,7 +34,7 @@ enum ClassIcon {
linux,
database,
applications,
project
project,
}
Map<ClassIcon, RegExp> _descriptors = {
@@ -49,8 +49,9 @@ Map<ClassIcon, RegExp> _descriptors = {
ClassIcon.pe: RegExp(r'^tes(i|tneveles)|sport|edzeselmelet'),
ClassIcon.chemistry: RegExp(r'kemia'),
ClassIcon.biology: RegExp(r'biologia'),
ClassIcon.env:
RegExp(r'kornyezet|termeszet ?(tudomany|ismeret)|hon( es nep)?ismeret'),
ClassIcon.env: RegExp(
r'kornyezet|termeszet ?(tudomany|ismeret)|hon( es nep)?ismeret',
),
ClassIcon.religion: RegExp(r'(hit|erkolcs)tan|vallas|etika|bibliaismeret'),
ClassIcon.economics: RegExp(r'penzugy|gazdasag'),
ClassIcon.it: RegExp(r'informatika|szoftver|iroda|digitalis'),
@@ -66,12 +67,13 @@ Map<ClassIcon, RegExp> _descriptors = {
ClassIcon.ofo: RegExp(r'osztaly(fonoki|kozosseg)|kozossegi|neveles'),
ClassIcon.diligence: RegExp(r'szorgalom'),
ClassIcon.attitude: RegExp(r'magatartas'),
ClassIcon.language:
RegExp(r'angol|nemet|francia|olasz|orosz|spanyol|latin|kinai|nyelv'),
ClassIcon.language: RegExp(
r'angol|nemet|francia|olasz|orosz|spanyol|latin|kinai|nyelv',
),
ClassIcon.linux: RegExp(r'linux'),
ClassIcon.database: RegExp(r'adatbazis.*'),
ClassIcon.applications: RegExp(r'asztali alkalmazasok'),
ClassIcon.project: RegExp(r'projekt')
ClassIcon.project: RegExp(r'projekt'),
};
Map<ClassIcon, Uint8List> _iconMap = {
@@ -117,17 +119,19 @@ ClassIcon? getIconType(String uid, String className, String category) {
if (icon == null) {
for (var desc in _descriptors.entries) {
if (desc.value.hasMatch(className
.replaceAll("ö", "o")
.replaceAll("ü", "u")
.replaceAll("ó", "o")
.replaceAll("ő", "o")
.replaceAll("ú", "u")
.replaceAll("é", "e")
.replaceAll("á", "a")
.replaceAll("ű", "u")
.replaceAll("í", "i")
.toLowerCase())) {
if (desc.value.hasMatch(
className
.replaceAll("ö", "o")
.replaceAll("ü", "u")
.replaceAll("ó", "o")
.replaceAll("ő", "o")
.replaceAll("ú", "u")
.replaceAll("é", "e")
.replaceAll("á", "a")
.replaceAll("ű", "u")
.replaceAll("í", "i")
.toLowerCase(),
)) {
icon = desc.key;
break;

View File

@@ -10,7 +10,9 @@ class ImagePreloader {
static final Map<String, Future<ui.Image>> _loadingFutures = {};
static Future<ui.Image> preloadAssetImage(
AssetBundle bundle, String assetPath) async {
AssetBundle bundle,
String assetPath,
) async {
if (_cache.containsKey(assetPath)) {
return _cache[assetPath]!;
}
@@ -32,9 +34,12 @@ class ImagePreloader {
}
static Future<List<ui.Image>> preloadMultipleAssets(
AssetBundle bundle, List<String> assetPaths) async {
final futures =
assetPaths.map((path) => preloadAssetImage(bundle, path)).toList();
AssetBundle bundle,
List<String> assetPaths,
) async {
final futures = assetPaths
.map((path) => preloadAssetImage(bundle, path))
.toList();
return await Future.wait(futures);
}
@@ -91,7 +96,9 @@ class ImagePreloader {
}
static Future<ui.Image> _loadAssetImage(
AssetBundle bundle, String assetPath) async {
AssetBundle bundle,
String assetPath,
) async {
logger.finest("Caching: $assetPath");
final ByteData data = await bundle.load(assetPath);
final Uint8List bytes = data.buffer.asUint8List();
@@ -119,7 +126,9 @@ class PreloadedImageProvider extends ImageProvider<PreloadedImageProvider> {
@override
ImageStreamCompleter loadImage(
PreloadedImageProvider key, ImageDecoderCallback decode) {
PreloadedImageProvider key,
ImageDecoderCallback decode,
) {
return OneFrameImageStreamCompleter(_loadAsync(key));
}
@@ -133,8 +142,10 @@ class PreloadedImageProvider extends ImageProvider<PreloadedImageProvider> {
}
try {
final image =
await ImagePreloader.preloadAssetImage(assetBundle, key.assetPath);
final image = await ImagePreloader.preloadAssetImage(
assetBundle,
key.assetPath,
);
return ImageInfo(image: image.clone());
} catch (e) {
final ByteData data = await assetBundle.load(key.assetPath);

View File

@@ -5,7 +5,9 @@ import 'package:firka/helpers/api/model/timetable.dart';
import 'package:logging/logging.dart';
class LiveActivityManager {
static const MethodChannel _channel = MethodChannel('firka.app/live_activity');
static const MethodChannel _channel = MethodChannel(
'firka.app/live_activity',
);
static final Logger _logger = Logger('LiveActivityManager');
static String? _activityId;
@@ -31,7 +33,9 @@ class LiveActivityManager {
if (activeActivities.isNotEmpty) {
_activityId = activeActivities.first;
_isActivityActive = true;
_logger.info('Synced activity state: Found existing activity $_activityId');
_logger.info(
'Synced activity state: Found existing activity $_activityId',
);
} else {
_activityId = null;
_isActivityActive = false;
@@ -48,7 +52,9 @@ class LiveActivityManager {
final args = call.arguments as Map;
final activityId = args['activityId'] as String;
final pushToken = args['pushToken'] as String;
_logger.info('Received LiveActivity push token: ${pushToken.substring(0, 10)}...');
_logger.info(
'Received LiveActivity push token: ${pushToken.substring(0, 10)}...',
);
_onPushTokenReceived?.call(activityId, pushToken);
break;
default:
@@ -56,7 +62,9 @@ class LiveActivityManager {
}
}
static void setOnPushTokenReceived(Function(String activityId, String pushToken) callback) {
static void setOnPushTokenReceived(
Function(String activityId, String pushToken) callback,
) {
_onPushTokenReceived = callback;
}
@@ -92,7 +100,9 @@ class LiveActivityManager {
try {
await _syncActivityState();
if (_isActivityActive) {
_logger.info('Activity already exists, ending it to create new one with fresh token');
_logger.info(
'Activity already exists, ending it to create new one with fresh token',
);
await endAllActivities();
await Future.delayed(const Duration(milliseconds: 500));
}
@@ -217,18 +227,23 @@ class LiveActivityManager {
final payload = {
'isBreak': isBeforeSchool ? false : isBreak,
'lessonName': isBeforeSchool ? currentLesson.name : (isBreak ? 'Szünet' : currentLesson.name),
'lessonName': isBeforeSchool
? currentLesson.name
: (isBreak ? 'Szünet' : currentLesson.name),
'lessonTheme': (isBeforeSchool || isBreak) ? null : currentLesson.theme,
'roomName': (isBeforeSchool || isBreak) ? null : currentLesson.roomName,
'teacherName': (isBeforeSchool || isBreak) ? null : currentLesson.teacher,
'startTime': startTimeForActivity.toUtc().toIso8601String(),
'endTime': endTimeForActivity.toUtc().toIso8601String(),
'lessonNumber': (isBeforeSchool || isBreak) ? null : currentLesson.lessonNumber,
'lessonNumber': (isBeforeSchool || isBreak)
? null
: currentLesson.lessonNumber,
'nextLessonName': isBeforeSchool ? null : nextLesson?.name,
'nextRoomName': isBeforeSchool ? null : nextLesson?.roomName,
'nextStartTime': nextStartTimeForActivity?.toUtc().toIso8601String(),
'isSubstitution': currentLesson.substituteTeacher != null,
'isCancelled': currentLesson.state.name?.toLowerCase().contains('elmarad') ?? false,
'isCancelled':
currentLesson.state.name?.toLowerCase().contains('elmarad') ?? false,
'substituteTeacher': currentLesson.substituteTeacher,
'currentTime': now.toUtc().toIso8601String(),
'mode': mode,

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,9 @@ import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
Future<void> pickProfilePicture(
AppInitialization data, ImagePicker picker) async {
AppInitialization data,
ImagePicker picker,
) async {
var imageFile = await picker.pickImage(source: ImageSource.gallery);
if (imageFile == null) return;

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,7 @@ Future<void> loadDirtyWords() async {
for (final node in document.findAllElements('Word')) {
final type = node.getAttribute('type');
final text = node.text.trim();
final text = node.innerText.trim();
if (type == 'f') {
_nouns.add(text);
@@ -21,8 +21,11 @@ Future<void> loadDirtyWords() async {
}
}
String generateSwearSentence(
{int words = 3, bool capitalize = true, bool exclamation = true}) {
String generateSwearSentence({
int words = 3,
bool capitalize = true,
bool exclamation = true,
}) {
if (words < 1) {
throw ArgumentError('Words must be at least 1');
}

View File

@@ -25,25 +25,24 @@ import 'grade.dart';
import '../../helpers/api/model/test.dart';
Future<void> showLessonBottomSheet(
BuildContext context,
AppInitialization data,
Lesson lesson,
int? lessonNo,
Color accent,
Color secondary,
Color bgColor,
Test? test,
) async {
BuildContext context,
AppInitialization data,
Lesson lesson,
int? lessonNo,
Color accent,
Color secondary,
Color bgColor,
Test? test,
) async {
final statsForNerdsEnabled = data.settings
.group("settings")
.subGroup("developer")
.boolean("stats_for_nerds");
final showTests = data.settings
.group("settings")
.subGroup("timetable_toast")
.boolean("tests_and_homework");
.group("settings")
.subGroup("timetable_toast")
.boolean("tests_and_homework");
showModalBottomSheet(
context: context,
elevation: 100,
@@ -61,11 +60,12 @@ Future<void> showLessonBottomSheet(
"${data.l10n.stats_date}: ${lesson.start.isAfter(y2k) ? lesson.start.format(data.l10n, FormatMode.yyyymmddhhmmss) : "N/A"}\n"
"${data.l10n.stats_created_at}: ${lesson.createdAt.isAfter(y2k) ? lesson.createdAt.format(data.l10n, FormatMode.yyyymmddhhmmss) : "N/A"}\n"
"${data.l10n.stats_last_mod}: ${lesson.lastModifiedAt.isAfter(y2k) ? lesson.lastModifiedAt.format(data.l10n, FormatMode.yyyymmddhhmmss) : "N/A"}";
statsForNerds = Text(stats,
style:
appStyle.fonts.B_16R.apply(color: appStyle.colors.textPrimary));
statsForNerds = Text(
stats,
style: appStyle.fonts.B_16R.apply(color: appStyle.colors.textPrimary),
);
}
return Stack(
children: [
Positioned.fill(
@@ -104,7 +104,9 @@ Future<void> showLessonBottomSheet(
),
Text(
lessonNo.toString(),
style: appStyle.fonts.B_12R.apply(color: secondary),
style: appStyle.fonts.B_12R.apply(
color: secondary,
),
textAlign: TextAlign.center,
),
],
@@ -116,7 +118,8 @@ Future<void> showLessonBottomSheet(
shadowColor: Colors.transparent,
color: bgColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16)),
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: EdgeInsetsGeometry.all(4),
child: ClassIconWidget(
@@ -138,63 +141,75 @@ Future<void> showLessonBottomSheet(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(children: [
Text(
"${lesson.name} ${statsForNerdsEnabled ? "(${lesson.classGroup?.name ?? ''})" : ""}",
style: appStyle.fonts.H_18px
.apply(color: appStyle.colors.textPrimary),
),
Card(
shadowColor: Colors.transparent,
color: appStyle.colors.a15p,
child: Padding(
padding: EdgeInsets.all(5),
child: Text(lesson.roomName ?? 'N/A',
style: appStyle.fonts.B_12R.apply(
color: appStyle.colors.secondary)),
Row(
children: [
Text(
"${lesson.name} ${statsForNerdsEnabled ? "(${lesson.classGroup?.name ?? ''})" : ""}",
style: appStyle.fonts.H_18px.apply(
color: appStyle.colors.textPrimary,
),
),
),
]),
Card(
shadowColor: Colors.transparent,
color: appStyle.colors.a15p,
child: Padding(
padding: EdgeInsets.all(5),
child: Text(
lesson.roomName ?? 'N/A',
style: appStyle.fonts.B_12R.apply(
color: appStyle.colors.secondary,
),
),
),
),
],
),
Text(
lesson.teacher ?? 'N/A',
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textPrimary),
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
),
Text(
'${lesson.start.format(data.l10n, FormatMode.hmm)} - ${lesson.end.format(data.l10n, FormatMode.hmm)}',
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textSecondary),
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
),
],
),
),
SizedBox(height: 8),
FirkaCard(left: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
data.l10n.lesson_subject,
style: appStyle.fonts.H_14px
.apply(color: appStyle.colors.textPrimary),
),
SizedBox(height: 4),
FirkaCard(
left: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
data.l10n.lesson_subject,
style: appStyle.fonts.H_14px.apply(
color: appStyle.colors.textPrimary,
),
),
SizedBox(height: 4),
SizedBox(
width: MediaQuery.of(context).size.width * 0.7,
child: Text(
lesson.theme ?? 'N/A',
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textPrimary),
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
),
SizedBox(height: 4),
statsForNerds
],
)
]),
if (test != null && showTests)
SizedBox(height: 4),
statsForNerds,
],
),
],
),
if (test != null && showTests)
FirkaCard(
left: [
Container(
@@ -217,26 +232,32 @@ Future<void> showLessonBottomSheet(
alignment: Alignment.centerLeft,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
children: [
SizedBox(
width: MediaQuery.of(context).size.width * 0.6,
width:
MediaQuery.of(context).size.width * 0.6,
child: Text(
test.theme,
style: appStyle.fonts.B_16SB.apply(color: appStyle.colors.textPrimary),
maxLines: 2,
overflow: TextOverflow.ellipsis,
)
),
SizedBox(height: 4),
SizedBox(
width: MediaQuery.of(context).size.width * 0.6,
child: Text(
test.method.description ?? 'N/A',
style: appStyle.fonts.B_16R.apply(color: appStyle.colors.textSecondary),
style: appStyle.fonts.B_16SB.apply(
color: appStyle.colors.textPrimary,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
)
),
SizedBox(height: 4),
SizedBox(
width:
MediaQuery.of(context).size.width * 0.6,
child: Text(
test.method.description ?? 'N/A',
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
@@ -255,7 +276,8 @@ Future<void> showLessonBottomSheet(
),
),
),
]),
],
),
SizedBox(height: 8),
SizedBox(
width: MediaQuery.of(context).size.width / 1.1,
@@ -265,15 +287,20 @@ Future<void> showLessonBottomSheet(
center: [
Text(
data.l10n.view_subject_btn,
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textSecondary),
)
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
),
],
color: appStyle.colors.buttonSecondaryFill,
),
onTap: () {
Navigator.pop(context);
pageNavNotifier.value = PageNavData(HomePage.grades, lesson.subject!.uid, lesson.subject!.name);
pageNavNotifier.value = PageNavData(
HomePage.grades,
lesson.subject!.uid,
lesson.subject!.name,
);
},
),
),
@@ -289,19 +316,24 @@ Future<void> showLessonBottomSheet(
}
Future<void> showTestBottomSheet(
BuildContext context,
AppInitialization data,
Lesson lesson,
int? lessonNo,
Color accent,
Color secondary,
Color bgColor,
Test? test,
) async {
BuildContext context,
AppInitialization data,
Lesson lesson,
int? lessonNo,
Color accent,
Color secondary,
Color bgColor,
Test? test,
) async {
final date = lesson.start;
final formattedDate = DateFormat('MMMM d, EEEE', data.l10n.localeName).format(date);
final formattedTime = DateFormat('MMMM d, HH:mm', data.l10n.localeName).format(date);
final formattedDate = DateFormat(
'MMMM d, EEEE',
data.l10n.localeName,
).format(date);
final formattedTime = DateFormat(
'MMMM d, HH:mm',
data.l10n.localeName,
).format(date);
final statsForNerdsEnabled = data.settings
.group("settings")
@@ -309,9 +341,9 @@ Future<void> showTestBottomSheet(
.boolean("stats_for_nerds");
final showTests = data.settings
.group("settings")
.subGroup("timetable_toast")
.boolean("tests_and_homework");
.group("settings")
.subGroup("timetable_toast")
.boolean("tests_and_homework");
showModalBottomSheet(
context: context,
elevation: 100,
@@ -328,11 +360,12 @@ Future<void> showTestBottomSheet(
"${data.l10n.stats_date}: ${lesson.start.isAfter(y2k) ? lesson.start.format(data.l10n, FormatMode.yyyymmddhhmmss) : "N/A"}\n"
"${data.l10n.stats_created_at}: ${lesson.createdAt.isAfter(y2k) ? lesson.createdAt.format(data.l10n, FormatMode.yyyymmddhhmmss) : "N/A"}\n"
"${data.l10n.stats_last_mod}: ${lesson.lastModifiedAt.isAfter(y2k) ? lesson.lastModifiedAt.format(data.l10n, FormatMode.yyyymmddhhmmss) : "N/A"}";
statsForNerds = Text(stats,
style:
appStyle.fonts.B_16R.apply(color: appStyle.colors.textPrimary));
statsForNerds = Text(
stats,
style: appStyle.fonts.B_16R.apply(color: appStyle.colors.textPrimary),
);
}
return Stack(
children: [
Positioned.fill(
@@ -376,47 +409,54 @@ Future<void> showTestBottomSheet(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(children: [
Expanded(
child: Text(
"${test?.theme ?? 'N/A'} ${statsForNerdsEnabled ? "(${lesson.classGroup?.name ?? ''})" : ""}",
style: appStyle.fonts.H_18px
.apply(color: appStyle.colors.textPrimary),
maxLines: 4,
overflow: TextOverflow.ellipsis,
Row(
children: [
Expanded(
child: Text(
"${test?.theme ?? 'N/A'} ${statsForNerdsEnabled ? "(${lesson.classGroup?.name ?? ''})" : ""}",
style: appStyle.fonts.H_18px.apply(
color: appStyle.colors.textPrimary,
),
maxLines: 4,
overflow: TextOverflow.ellipsis,
),
),
),
]),
],
),
Text(
test?.method.description ?? 'N/A',
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textSecondary),
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
),
],
),
),
SizedBox(height: 8),
FirkaCard(left: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 4),
FirkaCard(
left: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 4),
SizedBox(
width: MediaQuery.of(context).size.width * 0.7,
child: Text(
"${data.l10n.data}: $formattedDate",
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textPrimary),
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
),
SizedBox(height: 4),
statsForNerds
],
)
]),
if (test != null && showTests)
SizedBox(height: 4),
statsForNerds,
],
),
],
),
if (test != null && showTests)
FirkaCard(
left: [
Row(
@@ -434,10 +474,13 @@ Future<void> showTestBottomSheet(
),
Padding(
padding: EdgeInsets.only(left: 8, top: 4),
child: Text(lessonNo.toString(),
style: appStyle.fonts.B_12R
.apply(color: secondary)),
)
child: Text(
lessonNo.toString(),
style: appStyle.fonts.B_12R.apply(
color: secondary,
),
),
),
],
),
),
@@ -447,7 +490,8 @@ Future<void> showTestBottomSheet(
shadowColor: Colors.transparent,
color: bgColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16)),
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: EdgeInsetsGeometry.all(4),
child: ClassIconWidget(
@@ -469,26 +513,31 @@ Future<void> showTestBottomSheet(
alignment: Alignment.centerLeft,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
children: [
SizedBox(
width: MediaQuery.of(context).size.width * 0.3,
width:
MediaQuery.of(context).size.width * 0.3,
child: Text(
lesson.name,
style: appStyle.fonts.B_16SB.apply(color: appStyle.colors.textPrimary),
style: appStyle.fonts.B_16SB.apply(
color: appStyle.colors.textPrimary,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
)
),
),
],
),
),
],
right: [
],
right: [
Text(
formattedTime,
style: appStyle.fonts.B_14R.apply(color: appStyle.colors.textSecondary),
)
],
formattedTime,
style: appStyle.fonts.B_14R.apply(
color: appStyle.colors.textSecondary,
),
),
],
),
SizedBox(height: 8),
SizedBox(
@@ -499,14 +548,24 @@ Future<void> showTestBottomSheet(
center: [
Text(
data.l10n.view_lesson_btn,
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textSecondary),
)
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
),
],
color: appStyle.colors.buttonSecondaryFill,
),
onTap: () {
showLessonBottomSheet(context, data, lesson, lessonNo, accent, secondary, bgColor, test);
showLessonBottomSheet(
context,
data,
lesson,
lessonNo,
accent,
secondary,
bgColor,
test,
);
},
),
),
@@ -522,7 +581,10 @@ Future<void> showTestBottomSheet(
}
Future<void> showGradeBottomSheet(
BuildContext context, AppInitialization data, Grade grade) async {
BuildContext context,
AppInitialization data,
Grade grade,
) async {
showModalBottomSheet(
context: context,
elevation: 100,
@@ -535,7 +597,10 @@ Future<void> showGradeBottomSheet(
),
builder: (BuildContext context) {
final gradeCreationDate = grade.creationDate;
final formattedDate = DateFormat('yyyy. MMMM d., EEEE', data.l10n.localeName).format(gradeCreationDate);
final formattedDate = DateFormat(
'yyyy. MMMM d., EEEE',
data.l10n.localeName,
).format(gradeCreationDate);
return Stack(
children: [
@@ -559,18 +624,19 @@ Future<void> showGradeBottomSheet(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [GradeWidget(grade)],
),
Row(children: [GradeWidget(grade)]),
SizedBox(height: 4),
Padding(
padding: const EdgeInsets.only(left: 6),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(grade.topic ?? grade.type.description!,
style: appStyle.fonts.H_18px
.apply(color: appStyle.colors.textPrimary)),
Text(
grade.topic ?? grade.type.description!,
style: appStyle.fonts.H_18px.apply(
color: appStyle.colors.textPrimary,
),
),
grade.mode?.description != null
? SizedBox(
width:
@@ -578,61 +644,73 @@ Future<void> showGradeBottomSheet(
child: Text(
grade.mode!.description!,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary),
color: appStyle.colors.textSecondary,
),
),
)
: SizedBox(),
SizedBox(
height: 20,
),
SizedBox(height: 20),
LessonWidget(
data,
[],
[],
[],
null,
Lesson(
uid: "-2",
date: "",
start: grade.creationDate,
end: grade.creationDate,
name: grade.subject.name,
type: NameUidDesc(
uid: "", name: "", description: ""),
state: NameUidDesc(
uid: "", name: "", description: ""),
canStudentEditHomework: false,
isHomeworkComplete: false,
attachments: [],
isDigitalLesson: false,
digitalSupportDeviceTypeList: [],
createdAt: timeNow(),
subject: grade.subject,
lastModifiedAt: timeNow()),
uid: "-2",
date: "",
start: grade.creationDate,
end: grade.creationDate,
name: grade.subject.name,
type: NameUidDesc(
uid: "",
name: "",
description: "",
),
state: NameUidDesc(
uid: "",
name: "",
description: "",
),
canStudentEditHomework: false,
isHomeworkComplete: false,
attachments: [],
isDigitalLesson: false,
digitalSupportDeviceTypeList: [],
createdAt: timeNow(),
subject: grade.subject,
lastModifiedAt: timeNow(),
),
null,
null,
placeholderMode: true,
),
FirkaCard(left: [
Column(
FirkaCard(
left: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${data.l10n.tt_added}$formattedDate",
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary),
color: appStyle.colors.textPrimary,
),
),
Text(
"${data.l10n.grade_teacherName}${grade.teacher}",
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary),
color: appStyle.colors.textPrimary,
),
),
Text(
"${data.l10n.grade_strValue}${grade.strValue}",
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary),
)
])
]),
color: appStyle.colors.textPrimary,
),
),
],
),
],
),
SizedBox(height: 8),
SizedBox(
width: MediaQuery.of(context).size.width / 1.1,
@@ -642,15 +720,20 @@ Future<void> showGradeBottomSheet(
center: [
Text(
data.l10n.view_subject_btn,
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textSecondary),
)
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
),
],
color: appStyle.colors.buttonSecondaryFill,
),
onTap: () {
Navigator.pop(context);
pageNavNotifier.value = PageNavData(HomePage.grades, grade.subject.uid, grade.subject.name);
pageNavNotifier.value = PageNavData(
HomePage.grades,
grade.subject.uid,
grade.subject.name,
);
},
),
),
@@ -669,7 +752,10 @@ Future<void> showGradeBottomSheet(
}
Future<void> showHomeworkBottomSheet(
BuildContext context, AppInitialization data, Homework homework) async {
BuildContext context,
AppInitialization data,
Homework homework,
) async {
showModalBottomSheet(
context: context,
elevation: 100,
@@ -681,8 +767,10 @@ Future<void> showHomeworkBottomSheet(
minHeight: MediaQuery.of(context).size.height * 0.34,
),
builder: (BuildContext context) {
final formattedDate = DateFormat('yyyy. MMMM d.', data.l10n.localeName).format(homework.dueDate);
final formattedDate = DateFormat(
'yyyy. MMMM d.',
data.l10n.localeName,
).format(homework.dueDate);
return Stack(
children: [
@@ -711,17 +799,21 @@ Future<void> showHomeworkBottomSheet(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(children: [
Text(
data.l10n.homework,
style: appStyle.fonts.H_18px
.apply(color: appStyle.colors.textPrimary),
),
]),
Row(
children: [
Text(
data.l10n.homework,
style: appStyle.fonts.H_18px.apply(
color: appStyle.colors.textPrimary,
),
),
],
),
Text(
formattedDate,
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textSecondary),
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
),
],
),
@@ -730,26 +822,25 @@ Future<void> showHomeworkBottomSheet(
LessonWidget(
data,
[],
[],
[],
null,
Lesson(
uid: "-1",
date: "",
start: homework.startDate,
end: homework.dueDate ,
name: homework.subjectName,
type: NameUidDesc(
uid: "", name: "", description: ""),
state: NameUidDesc(
uid: "", name: "", description: ""),
canStudentEditHomework: false,
isHomeworkComplete: false,
attachments: [],
isDigitalLesson: false,
digitalSupportDeviceTypeList: [],
createdAt: timeNow(),
subject: homework.subject,
lastModifiedAt: timeNow()),
uid: "-1",
date: "",
start: homework.startDate,
end: homework.dueDate,
name: homework.subjectName,
type: NameUidDesc(uid: "", name: "", description: ""),
state: NameUidDesc(uid: "", name: "", description: ""),
canStudentEditHomework: false,
isHomeworkComplete: false,
attachments: [],
isDigitalLesson: false,
digitalSupportDeviceTypeList: [],
createdAt: timeNow(),
subject: homework.subject,
lastModifiedAt: timeNow(),
),
null,
null,
placeholderMode: true,
@@ -760,11 +851,16 @@ Future<void> showHomeworkBottomSheet(
shadow: true,
child: Card(
color: appStyle.colors.card,
shadowColor: isLightMode.value ? null : Colors.transparent,
shadowColor: isLightMode.value
? null
: Colors.transparent,
child: Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 20.0),
padding: const EdgeInsets.symmetric(
horizontal: 20.0,
vertical: 20.0,
),
child: Html(
data: homework.description,
style: {
@@ -778,11 +874,11 @@ Future<void> showHomeworkBottomSheet(
textAlign: TextAlign.start,
),
},
),
),
),
),
),
)
),
),
FutureBuilder<bool>(
future: isHomeworkDone(data.isar, homework.uid),
@@ -805,8 +901,9 @@ Future<void> showHomeworkBottomSheet(
Text(
data.l10n.mark_as_done,
style: appStyle.fonts.B_16SB.apply(
color: appStyle.colors.textSecondary),
)
color: appStyle.colors.textSecondary,
),
),
],
color: appStyle.colors.accent,
),
@@ -826,8 +923,9 @@ Future<void> showHomeworkBottomSheet(
Text(
data.l10n.mark_as_not_done,
style: appStyle.fonts.B_16SB.apply(
color: appStyle.colors.textSecondary),
)
color: appStyle.colors.textSecondary,
),
),
],
color: appStyle.colors.accent,
),
@@ -849,15 +947,20 @@ Future<void> showHomeworkBottomSheet(
center: [
Text(
data.l10n.view_subject_btn,
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textSecondary),
)
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
),
],
color: appStyle.colors.buttonSecondaryFill,
),
onTap: () {
Navigator.pop(context);
pageNavNotifier.value = PageNavData(HomePage.grades, homework.subject.uid, homework.subjectName);
pageNavNotifier.value = PageNavData(
HomePage.grades,
homework.subject.uid,
homework.subjectName,
);
},
),
),
@@ -873,7 +976,10 @@ Future<void> showHomeworkBottomSheet(
}
Future<void> showSubjectBottomSheetSettings(
BuildContext context, AppInitialization data, Subject subject) async {
BuildContext context,
AppInitialization data,
Subject subject,
) async {
showModalBottomSheet(
context: context,
elevation: 100,
@@ -882,8 +988,6 @@ Future<void> showSubjectBottomSheetSettings(
backgroundColor: Colors.transparent,
barrierColor: appStyle.colors.a15p,
builder: (BuildContext context) {
return Stack(
children: [
Align(
@@ -904,13 +1008,16 @@ Future<void> showSubjectBottomSheetSettings(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(children: [
Text(
subject.name,
style: appStyle.fonts.H_18px
.apply(color: appStyle.colors.textPrimary),
),
]),
Row(
children: [
Text(
subject.name,
style: appStyle.fonts.H_18px.apply(
color: appStyle.colors.textPrimary,
),
),
],
),
],
),
),
@@ -924,19 +1031,23 @@ Future<void> showSubjectBottomSheetSettings(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(children: [
FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.editPen4Solid,
size: 24.0,
color: appStyle.colors.accent,
),
SizedBox(width: 8),
Text(
"Tantárgy átnevezése",
style: appStyle.fonts.B_16R.apply(color: appStyle.colors.textPrimary),
),
])
Row(
children: [
FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.editPen4Solid,
size: 24.0,
color: appStyle.colors.accent,
),
SizedBox(width: 8),
Text(
"Tantárgy átnevezése",
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
),
],
),
],
),
),
@@ -946,7 +1057,7 @@ Future<void> showSubjectBottomSheetSettings(
logger.finest("Tantárgy átnevezése");
// Navigator.pop(context);
},
)
),
],
),
),

View File

@@ -6,12 +6,13 @@ class FirkaButton extends StatelessWidget {
final TextStyle fontStyle;
final Icon? icon;
const FirkaButton(
{required this.text,
required this.bgColor,
required this.fontStyle,
this.icon,
super.key});
const FirkaButton({
required this.text,
required this.bgColor,
required this.fontStyle,
this.icon,
super.key,
});
@override
Widget build(BuildContext context) {
@@ -21,24 +22,25 @@ class FirkaButton extends StatelessWidget {
borderRadius: BorderRadius.all(Radius.circular(16)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
child: icon != null
? Row(
mainAxisSize: MainAxisSize.min,
children: [
icon!,
SizedBox(width: 8),
Text(text, style: fontStyle),
],
)
: Text(text, style: fontStyle)),
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
child: icon != null
? Row(
mainAxisSize: MainAxisSize.min,
children: [
icon!,
SizedBox(width: 8),
Text(text, style: fontStyle),
],
)
: Text(text, style: fontStyle),
),
);
}
}

View File

@@ -16,16 +16,17 @@ class FirkaCard extends StatelessWidget {
final Attach? attached;
final Color? color;
const FirkaCard(
{required this.left,
this.shadow = true,
this.center,
this.right,
this.extra,
this.attached,
this.color,
this.height,
super.key});
const FirkaCard({
required this.left,
this.shadow = true,
this.center,
this.right,
this.extra,
this.attached,
this.color,
this.height,
super.key,
});
@override
Widget build(BuildContext context) {
@@ -40,81 +41,95 @@ class FirkaCard extends StatelessWidget {
width: MediaQuery.of(context).size.width,
height: height,
child: FirkaShadow(
shadow: shadow,
child: Card(
color: color ?? appStyle.colors.card,
shadowColor:
isLightMode.value && shadow ? null : Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(attached == Attach.top
? attachedRounding
: defaultRounding),
topRight: Radius.circular(attached == Attach.top
? attachedRounding
: defaultRounding),
bottomLeft: Radius.circular(attached == Attach.bottom
? attachedRounding
: defaultRounding),
bottomRight: Radius.circular(attached == Attach.bottom
? attachedRounding
: defaultRounding)),
),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(children: left),
Row(children: center ?? []),
Row(children: right),
],
),
extra ?? SizedBox(),
],
shadow: shadow,
child: Card(
color: color ?? appStyle.colors.card,
shadowColor: isLightMode.value && shadow
? null
: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(
attached == Attach.top ? attachedRounding : defaultRounding,
),
topRight: Radius.circular(
attached == Attach.top ? attachedRounding : defaultRounding,
),
bottomLeft: Radius.circular(
attached == Attach.bottom
? attachedRounding
: defaultRounding,
),
bottomRight: Radius.circular(
attached == Attach.bottom
? attachedRounding
: defaultRounding,
),
),
)),
),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(children: left),
Row(children: center ?? []),
Row(children: right),
],
),
extra ?? SizedBox(),
],
),
),
),
),
);
} else {
return SizedBox(
width: MediaQuery.of(context).size.width,
height: height,
child: FirkaShadow(
shadow: shadow,
child: Card(
color: color ?? appStyle.colors.card,
shadowColor:
isLightMode.value && shadow ? null : Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(attached == Attach.top
? attachedRounding
: defaultRounding),
topRight: Radius.circular(attached == Attach.top
? attachedRounding
: defaultRounding),
bottomLeft: Radius.circular(attached == Attach.bottom
? attachedRounding
: defaultRounding),
bottomRight: Radius.circular(attached == Attach.bottom
? attachedRounding
: defaultRounding)),
),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(children: left),
Row(children: center ?? []),
Row(children: right),
],
shadow: shadow,
child: Card(
color: color ?? appStyle.colors.card,
shadowColor: isLightMode.value && shadow
? null
: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(
attached == Attach.top ? attachedRounding : defaultRounding,
),
topRight: Radius.circular(
attached == Attach.top ? attachedRounding : defaultRounding,
),
bottomLeft: Radius.circular(
attached == Attach.bottom
? attachedRounding
: defaultRounding,
),
bottomRight: Radius.circular(
attached == Attach.bottom
? attachedRounding
: defaultRounding,
),
),
)),
),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(children: left),
Row(children: center ?? []),
Row(children: right),
],
),
),
),
),
);
}
}

View File

@@ -14,16 +14,18 @@ class FirkaShadow extends StatelessWidget {
final borderRadius = BorderRadius.circular(8.0);
final shadowBox = BoxDecoration(
color: Colors.transparent,
shape: BoxShape.rectangle,
boxShadow: [
BoxShadow(
color: appStyle.colors.shadowColor,
spreadRadius: -4,
blurRadius: 0,
offset: Offset(0, 2))
],
borderRadius: BorderRadius.all(Radius.circular(16)));
color: Colors.transparent,
shape: BoxShape.rectangle,
boxShadow: [
BoxShadow(
color: appStyle.colors.shadowColor,
spreadRadius: -4,
blurRadius: 0,
offset: Offset(0, 2),
),
],
borderRadius: BorderRadius.all(Radius.circular(16)),
);
if (!shadow) {
return ClipRRect(borderRadius: borderRadius, child: child);

View File

@@ -18,8 +18,9 @@ class GradeWidget extends StatelessWidget {
if (grade.valueType.name == "Szazalekos") {
gradeStr = grade.strValue.replaceAll("%", "");
if (grade.numericValue != null) {
gradeColor =
getGradeColor(percentageToGrade(grade.numericValue!).toDouble());
gradeColor = getGradeColor(
percentageToGrade(grade.numericValue!).toDouble(),
);
}
if (grade.numericValue != null && grade.numericValue == 100) {
@@ -29,10 +30,14 @@ class GradeWidget extends StatelessWidget {
color: gradeColor.withAlpha(38),
child: Padding(
padding: EdgeInsets.only(left: 8, right: 8),
child: Row(children: [
Text("100", // TODO: Make this curved
style: appStyle.fonts.P_14.copyWith(color: gradeColor))
]),
child: Row(
children: [
Text(
"100", // TODO: Make this curved
style: appStyle.fonts.P_14.copyWith(color: gradeColor),
),
],
),
),
);
} else {
@@ -42,11 +47,18 @@ class GradeWidget extends StatelessWidget {
color: gradeColor.withAlpha(38),
child: Padding(
padding: EdgeInsets.only(left: 8, right: 8),
child: Row(children: [
Text(gradeStr,
style: appStyle.fonts.P_14.copyWith(color: gradeColor)),
Text("%", style: appStyle.fonts.P_12.copyWith(color: gradeColor))
]),
child: Row(
children: [
Text(
gradeStr,
style: appStyle.fonts.P_14.copyWith(color: gradeColor),
),
Text(
"%",
style: appStyle.fonts.P_12.copyWith(color: gradeColor),
),
],
),
),
);
}
@@ -54,26 +66,36 @@ class GradeWidget extends StatelessWidget {
if (grade.numericValue != null) {
gradeColor = getGradeColor(grade.numericValue!.toDouble());
}
if (gradeStr == "0"){
if (gradeStr == "0") {
return Card(
shadowColor: Colors.transparent,
color: gradeColor.withAlpha(38),
child: Padding(
padding: EdgeInsets.only(left: 8, right: 8, top:2, bottom:2),
child: Text(grade.strValue,
style: appStyle.fonts.H_H1
.copyWith(fontSize: 16, color: gradeColor))),
padding: EdgeInsets.only(left: 8, right: 8, top: 2, bottom: 2),
child: Text(
grade.strValue,
style: appStyle.fonts.H_H1.copyWith(
fontSize: 16,
color: gradeColor,
),
),
),
);
}else {
} else {
return Card(
shape: CircleBorder(eccentricity: eccentricity),
shadowColor: Colors.transparent,
color: gradeColor.withAlpha(38),
child: Padding(
padding: EdgeInsets.only(left: 8, right: 8),
child: Text(gradeStr,
style: appStyle.fonts.H_H1
.copyWith(fontSize: 24, color: gradeColor))),
padding: EdgeInsets.only(left: 8, right: 8),
child: Text(
gradeStr,
style: appStyle.fonts.H_H1.copyWith(
fontSize: 24,
color: gradeColor,
),
),
),
);
}
}

View File

@@ -36,11 +36,15 @@ class WatchSyncHelper {
try {
return await _watchChannel
.invokeMethod<T>(method, arguments)
.timeout(timeout, onTimeout: () {
debugPrint(
'[WatchSync] Timeout calling $method after ${timeout.inSeconds}s');
return null;
});
.timeout(
timeout,
onTimeout: () {
debugPrint(
'[WatchSync] Timeout calling $method after ${timeout.inSeconds}s',
);
return null;
},
);
} catch (e) {
debugPrint('[WatchSync] Error calling $method: $e');
return null;
@@ -96,8 +100,9 @@ class WatchSyncHelper {
if (parts.length < 2) return null;
try {
final payloadJson =
utf8.decode(base64Url.decode(base64Url.normalize(parts[1])));
final payloadJson = utf8.decode(
base64Url.decode(base64Url.normalize(parts[1])),
);
final payload = jsonDecode(payloadJson);
if (payload is! Map) return null;
final iatSeconds = _asInt(payload['iat']);
@@ -142,7 +147,8 @@ class WatchSyncHelper {
return true;
}
final incomingVersion = incomingTokenVersion ??
final incomingVersion =
incomingTokenVersion ??
_extractTokenVersionFromIdToken(incomingIdToken);
final currentVersion = _resolveTokenVersionForModel(currentToken);
final currentUpdatedAtMs = _resolveUpdatedAtForModel(currentToken);
@@ -233,11 +239,13 @@ class WatchSyncHelper {
_watchChannel.setMethodCallHandler(_handleMethodCall);
debugPrint('[WatchSync] Handler initialized');
unawaited(_invokeMethodWithTimeout(
'watchSyncReady',
null,
const Duration(seconds: 2),
));
unawaited(
_invokeMethodWithTimeout(
'watchSyncReady',
null,
const Duration(seconds: 2),
),
);
}
static Future<bool> isWatchAppInstalled({bool forceRefresh = false}) async {
@@ -262,11 +270,14 @@ class WatchSyncHelper {
return _watchAppInstalledCache;
}
static Future<bool> isWatchReachable({bool forceRefreshInstall = false}) async {
static Future<bool> isWatchReachable({
bool forceRefreshInstall = false,
}) async {
if (!Platform.isIOS) return false;
final watchInstalled =
await isWatchAppInstalled(forceRefresh: forceRefreshInstall);
final watchInstalled = await isWatchAppInstalled(
forceRefresh: forceRefreshInstall,
);
if (!watchInstalled) return false;
final result = await _invokeMethodWithTimeout<bool>(
@@ -309,16 +320,13 @@ class WatchSyncHelper {
if (!watchInstalled) return true;
final timeout = maxWait + const Duration(seconds: 5);
final result = await _invokeMethodWithTimeout<dynamic>(
'waitForPeerRefreshLease',
{
'owner': _leaseOwnerIPhone,
'studentIdNorm': studentIdNorm,
'maxWaitMs': maxWait.inMilliseconds,
'pollIntervalMs': _leasePollInterval.inMilliseconds,
},
timeout,
);
final result =
await _invokeMethodWithTimeout<dynamic>('waitForPeerRefreshLease', {
'owner': _leaseOwnerIPhone,
'studentIdNorm': studentIdNorm,
'maxWaitMs': maxWait.inMilliseconds,
'pollIntervalMs': _leasePollInterval.inMilliseconds,
}, timeout);
if (result is! Map) {
debugPrint('[WatchSync] Lease wait returned invalid response: $result');
@@ -330,7 +338,8 @@ class WatchSyncHelper {
final waitedMs = result['waitedMs'];
final leaseChanged = result['leaseChanged'] == true;
debugPrint(
'[WatchSync] Lease wait status=$status ready=$ready waitedMs=$waitedMs leaseChanged=$leaseChanged');
'[WatchSync] Lease wait status=$status ready=$ready waitedMs=$waitedMs leaseChanged=$leaseChanged',
);
return ready;
}
@@ -342,18 +351,17 @@ class WatchSyncHelper {
final watchInstalled = await isWatchAppInstalled();
if (!watchInstalled) return null;
final result = await _invokeMethodWithTimeout<dynamic>(
'acquireRefreshLease',
{
'owner': _leaseOwnerIPhone,
'studentIdNorm': studentIdNorm,
'ttlMs': ttl.inMilliseconds,
},
const Duration(seconds: 5),
);
final result =
await _invokeMethodWithTimeout<dynamic>('acquireRefreshLease', {
'owner': _leaseOwnerIPhone,
'studentIdNorm': studentIdNorm,
'ttlMs': ttl.inMilliseconds,
}, const Duration(seconds: 5));
if (result is! Map) {
debugPrint('[WatchSync] Lease acquire returned invalid response: $result');
debugPrint(
'[WatchSync] Lease acquire returned invalid response: $result',
);
return null;
}
if (result['skipped'] == true) {
@@ -372,28 +380,20 @@ class WatchSyncHelper {
required String operationId,
}) async {
if (!Platform.isIOS) return;
await _invokeMethodWithTimeout(
'releaseRefreshLease',
{
'owner': _leaseOwnerIPhone,
'studentIdNorm': studentIdNorm,
'operationId': operationId,
},
const Duration(seconds: 5),
);
await _invokeMethodWithTimeout('releaseRefreshLease', {
'owner': _leaseOwnerIPhone,
'studentIdNorm': studentIdNorm,
'operationId': operationId,
}, const Duration(seconds: 5));
}
static Future<void> clearRefreshLeaseForAccount(int studentIdNorm) async {
if (!Platform.isIOS) return;
final watchInstalled = await isWatchAppInstalled();
if (!watchInstalled) return;
await _invokeMethodWithTimeout(
'clearRefreshLeaseForAccount',
{
'studentIdNorm': studentIdNorm,
},
const Duration(seconds: 5),
);
await _invokeMethodWithTimeout('clearRefreshLeaseForAccount', {
'studentIdNorm': studentIdNorm,
}, const Duration(seconds: 5));
}
static Future<void> clearAllRefreshLeases() async {
@@ -430,7 +430,8 @@ class WatchSyncHelper {
}
debugPrint(
'[WatchSync] Fresh iOS install detected, clearing iCloud and local auth state');
'[WatchSync] Fresh iOS install detected, clearing iCloud and local auth state',
);
await clearICloudToken(notifyWatch: true);
await clearAllRefreshLeases();
@@ -463,7 +464,8 @@ class WatchSyncHelper {
return await _processTokenFromWatch(call.arguments);
case 'onTokenRecoveredFromiCloud':
debugPrint(
'[WatchSync] Token recovered from iCloud notification received');
'[WatchSync] Token recovered from iCloud notification received',
);
await _handleTokenRecoveredFromiCloud();
return null;
default:
@@ -476,7 +478,8 @@ class WatchSyncHelper {
static Future<void> _handleTokenRecoveredFromiCloud() async {
if (!initDone) {
debugPrint(
'[WatchSync] Cannot handle iCloud recovery: app not initialized');
'[WatchSync] Cannot handle iCloud recovery: app not initialized',
);
return;
}
@@ -489,7 +492,8 @@ class WatchSyncHelper {
if (recovered) {
debugPrint(
'[WatchSync] Token recovered from iCloud, reauth flag cleared');
'[WatchSync] Token recovered from iCloud, reauth flag cleared',
);
} else {
final token = pickActiveToken(
tokens: initData.tokens,
@@ -499,7 +503,8 @@ class WatchSyncHelper {
if (expiryDate != null && expiryDate.isAfter(DateTime.now())) {
KretaClient.clearReauthFlag();
debugPrint(
'[WatchSync] Cleared reauth flag after iCloud notification (token is valid)');
'[WatchSync] Cleared reauth flag after iCloud notification (token is valid)',
);
}
}
} catch (e) {
@@ -536,7 +541,8 @@ class WatchSyncHelper {
if (!_isAccessTokenUsable(token.expiryDate, skew: const Duration())) {
debugPrint(
'[WatchSync] Active iPhone token access is expired, forwarding token to Watch for recovery');
'[WatchSync] Active iPhone token access is expired, forwarding token to Watch for recovery',
);
}
final tokenData = _buildTokenSyncPayload(token, includeSentAt: true);
@@ -571,7 +577,8 @@ class WatchSyncHelper {
}
static Future<Map<String, dynamic>> _processTokenFromWatch(
dynamic arguments) async {
dynamic arguments,
) async {
if (!initDone) {
debugPrint('[WatchSync] Cannot process Watch token: app not initialized');
return {'success': false, 'error': 'not_initialized'};
@@ -604,7 +611,8 @@ class WatchSyncHelper {
final watchExpiryDate = DateTime.fromMillisecondsSinceEpoch(watchExpiry);
if (!_isAccessTokenUsable(watchExpiryDate, skew: const Duration())) {
debugPrint(
'[WatchSync] Rejecting expired token from Watch, expiry: $watchExpiryDate');
'[WatchSync] Rejecting expired token from Watch, expiry: $watchExpiryDate',
);
return {'success': false, 'error': 'expired_token'};
}
final watchTokenVersion = _resolveIncomingTokenVersion(tokenData);
@@ -612,7 +620,8 @@ class WatchSyncHelper {
final watchIdToken = tokenData['idToken'] as String?;
final watchRefreshToken = tokenData['refreshToken'] as String?;
final isForActiveAccount = expectedStudentIdNorm == null ||
final isForActiveAccount =
expectedStudentIdNorm == null ||
watchStudentIdNorm == expectedStudentIdNorm;
if (isForActiveAccount &&
currentToken != null &&
@@ -626,13 +635,15 @@ class WatchSyncHelper {
currentToken: currentToken,
)) {
debugPrint(
'[WatchSync] Ignoring stale token from Watch for active account. Incoming expiry: $watchExpiryDate, incomingVersion: $watchTokenVersion');
'[WatchSync] Ignoring stale token from Watch for active account. Incoming expiry: $watchExpiryDate, incomingVersion: $watchTokenVersion',
);
return {'success': false, 'error': 'stale_token'};
}
}
debugPrint(
'[WatchSync] Accepting token from Watch, expiry: $watchExpiryDate (expired: ${watchExpiryDate.isBefore(DateTime.now())})');
'[WatchSync] Accepting token from Watch, expiry: $watchExpiryDate (expired: ${watchExpiryDate.isBefore(DateTime.now())})',
);
final newToken = TokenModel.fromValues(
watchStudentIdNorm,
@@ -656,7 +667,8 @@ class WatchSyncHelper {
KretaClient.clearReauthFlag();
} else {
debugPrint(
'[WatchSync] Stored token for inactive account ($watchStudentIdNorm), active is $expectedStudentIdNorm');
'[WatchSync] Stored token for inactive account ($watchStudentIdNorm), active is $expectedStudentIdNorm',
);
}
debugPrint('[WatchSync] Token from Watch saved successfully');
@@ -685,15 +697,18 @@ class WatchSyncHelper {
return;
}
final accessExpired =
!_isAccessTokenUsable(token.expiryDate, skew: const Duration());
final accessExpired = !_isAccessTokenUsable(
token.expiryDate,
skew: const Duration(),
);
if (accessExpired && !allowExpiredAccessToken) {
debugPrint('[WatchSync] Token expired, not sending to Watch');
return;
}
if (accessExpired && allowExpiredAccessToken) {
debugPrint(
'[WatchSync] Sending expired-access token to Watch for account-switch recovery');
'[WatchSync] Sending expired-access token to Watch for account-switch recovery',
);
}
final tokenData = _buildTokenSyncPayload(token, includeSentAt: true);
@@ -705,7 +720,8 @@ class WatchSyncHelper {
static String? _getLanguageForWatch() {
if (!initDone) {
debugPrint(
'[WatchSync] App not initialized yet, language unavailable for Watch');
'[WatchSync] App not initialized yet, language unavailable for Watch',
);
return null;
}
@@ -727,7 +743,8 @@ class WatchSyncHelper {
await _invokeMethodWithTimeout('sendLanguageToWatch', languageCode);
debugPrint(
'[WatchSync] Language sent to Watch: $languageCode (or timeout)');
'[WatchSync] Language sent to Watch: $languageCode (or timeout)',
);
}
/// Check iCloud for a fresher token and update local storage if found.
@@ -757,7 +774,10 @@ class WatchSyncHelper {
try {
debugPrint('[WatchSync] Checking iCloud for fresher token...');
final result = await _invokeMethodWithTimeout(
'checkiCloudToken', null, const Duration(seconds: 5));
'checkiCloudToken',
null,
const Duration(seconds: 5),
);
if (result == null) {
debugPrint('[WatchSync] No response from native (timeout or error)');
@@ -779,7 +799,8 @@ class WatchSyncHelper {
if (expectedStudentIdNorm != null &&
iCloudStudentIdNorm != expectedStudentIdNorm) {
debugPrint(
'[WatchSync] iCloud token belongs to different account ($iCloudStudentIdNorm), active is $expectedStudentIdNorm - ignoring');
'[WatchSync] iCloud token belongs to different account ($iCloudStudentIdNorm), active is $expectedStudentIdNorm - ignoring',
);
return false;
}
@@ -789,13 +810,17 @@ class WatchSyncHelper {
return false;
}
final iCloudExpiryDate =
DateTime.fromMillisecondsSinceEpoch(iCloudExpiry);
final iCloudAccessExpired =
!_isAccessTokenUsable(iCloudExpiryDate, skew: const Duration());
final iCloudExpiryDate = DateTime.fromMillisecondsSinceEpoch(
iCloudExpiry,
);
final iCloudAccessExpired = !_isAccessTokenUsable(
iCloudExpiryDate,
skew: const Duration(),
);
if (iCloudAccessExpired && !allowExpiredAccessToken) {
debugPrint(
'[WatchSync] iCloud token access is expired (expiry: $iCloudExpiryDate), skipping direct apply');
'[WatchSync] iCloud token access is expired (expiry: $iCloudExpiryDate), skipping direct apply',
);
return false;
}
final iCloudTokenVersion = _resolveIncomingTokenVersion(tokenData);
@@ -821,7 +846,8 @@ class WatchSyncHelper {
if (shouldAccept) {
debugPrint(
'[WatchSync] iCloud has fresher token! iCloud: $iCloudExpiryDate, Local: $localExpiry, iCloudVersion: $iCloudTokenVersion');
'[WatchSync] iCloud has fresher token! iCloud: $iCloudExpiryDate, Local: $localExpiry, iCloudVersion: $iCloudTokenVersion',
);
final newToken = TokenModel.fromValues(
(tokenData['studentIdNorm'] as int?) ?? 0,
@@ -852,7 +878,8 @@ class WatchSyncHelper {
effectiveClient.model = newToken;
}
final shouldClearReauth = !iCloudAccessExpired &&
final shouldClearReauth =
!iCloudAccessExpired &&
(expectedStudentIdNorm == null ||
newToken.studentIdNorm == expectedStudentIdNorm);
if (shouldClearReauth) {
@@ -860,11 +887,13 @@ class WatchSyncHelper {
}
debugPrint(
'[WatchSync] Token recovered from iCloud! New expiry: $iCloudExpiryDate');
'[WatchSync] Token recovered from iCloud! New expiry: $iCloudExpiryDate',
);
return true;
} else {
debugPrint(
'[WatchSync] Local token is same or fresher. Local: $localExpiry, iCloud: $iCloudExpiryDate');
'[WatchSync] Local token is same or fresher. Local: $localExpiry, iCloud: $iCloudExpiryDate',
);
return false;
}
} catch (e) {
@@ -890,7 +919,8 @@ class WatchSyncHelper {
final watchInstalled = await isWatchAppInstalled();
if (!watchInstalled) {
debugPrint(
'[WatchSync] Skipping iCloud token save because no paired Watch app is installed');
'[WatchSync] Skipping iCloud token save because no paired Watch app is installed',
);
return;
}
@@ -898,7 +928,10 @@ class WatchSyncHelper {
tokenData['forceAccountSwitch'] = forceAccountSwitch;
await _invokeMethodWithTimeout(
'saveTokeToniCloud', tokenData, const Duration(seconds: 5));
'saveTokeToniCloud',
tokenData,
const Duration(seconds: 5),
);
debugPrint('[WatchSync] Token saved to iCloud (or timeout)');
}
@@ -921,7 +954,10 @@ class WatchSyncHelper {
try {
debugPrint('[WatchSync] Requesting token from Watch...');
final result = await _invokeMethodWithTimeout(
'requestTokenFromWatch', null, const Duration(seconds: 10));
'requestTokenFromWatch',
null,
const Duration(seconds: 10),
);
final expectedStudentIdNorm = _resolveExpectedStudentIdNorm(
tokens: effectiveTokens,
client: effectiveClient,
@@ -956,7 +992,8 @@ class WatchSyncHelper {
currentToken.expiryDate != null &&
!KretaClient.needsReauth) {
debugPrint(
'[WatchSync] Sending iPhone token to Watch (Watch has no token)');
'[WatchSync] Sending iPhone token to Watch (Watch has no token)',
);
await _sendTokenToWatchInternal(
currentToken,
allowExpiredAccessToken: true,
@@ -980,7 +1017,8 @@ class WatchSyncHelper {
if (expectedStudentIdNorm != null &&
watchStudentIdNorm != expectedStudentIdNorm) {
debugPrint(
'[WatchSync] Watch token belongs to different account ($watchStudentIdNorm), active is $expectedStudentIdNorm - keeping active account');
'[WatchSync] Watch token belongs to different account ($watchStudentIdNorm), active is $expectedStudentIdNorm - keeping active account',
);
if (currentToken != null &&
currentToken.accessToken != null &&
currentToken.refreshToken != null &&
@@ -997,13 +1035,16 @@ class WatchSyncHelper {
final watchExpiryDate = DateTime.fromMillisecondsSinceEpoch(watchExpiry);
if (!_isAccessTokenUsable(watchExpiryDate, skew: const Duration())) {
debugPrint(
'[WatchSync] Watch provided expired token, ignoring and keeping iPhone token');
'[WatchSync] Watch provided expired token, ignoring and keeping iPhone token',
);
if (currentToken != null &&
currentToken.accessToken != null &&
currentToken.refreshToken != null &&
currentToken.expiryDate != null &&
_isAccessTokenUsable(currentToken.expiryDate,
skew: const Duration()) &&
_isAccessTokenUsable(
currentToken.expiryDate,
skew: const Duration(),
) &&
!KretaClient.needsReauth) {
await _sendTokenToWatchInternal(
currentToken,
@@ -1063,10 +1104,12 @@ class WatchSyncHelper {
}
debugPrint(
'[WatchSync] Token updated from Watch. New expiry: $watchExpiryDate');
'[WatchSync] Token updated from Watch. New expiry: $watchExpiryDate',
);
} else {
debugPrint(
'[WatchSync] iPhone token is same or newer, sending to Watch');
'[WatchSync] iPhone token is same or newer, sending to Watch',
);
await _sendTokenToWatchInternal(
currentToken,
allowExpiredAccessToken: true,

View File

@@ -131,7 +131,7 @@ Future<void> initLang(AppInitialization data) async {
languageCode = 'de';
break;
default: // auto
switch (ui.window.locale.languageCode) {
switch (ui.PlatformDispatcher.instance.locale.languageCode) {
case 'hu':
data.l10n = AppLocalizationsHu();
languageCode = 'hu';
@@ -218,7 +218,8 @@ Future<void> _initData(AppInitialization init) async {
final nextLocale = init.l10n.localeName;
if (previousLocale != nextLocale) {
logger.info(
"[Init] System locale changed in auto mode: $previousLocale -> $nextLocale");
"[Init] System locale changed in auto mode: $previousLocale -> $nextLocale",
);
}
globalUpdate.update();
}());
@@ -234,7 +235,8 @@ Future<void> _initData(AppInitialization init) async {
await WatchSyncHelper.runFreshInstallCleanupIfNeeded(isar: init.isar);
if (didRunFreshInstallCleanup) {
logger.info(
'[Init] Fresh-install cleanup completed; skipping startup iCloud recovery on this launch');
'[Init] Fresh-install cleanup completed; skipping startup iCloud recovery on this launch',
);
} else {
await WatchSyncHelper.checkAndRecoverFromiCloud(
isar: init.isar,
@@ -253,7 +255,8 @@ Future<void> _initData(AppInitialization init) async {
final token = pickActiveToken(tokens: allTokens, settings: init.settings);
if (token == null) {
logger.warning(
"[Init] Tokens disappeared during initialization; skipping client setup");
"[Init] Tokens disappeared during initialization; skipping client setup",
);
return;
}
logger.fine("Initializing kréta client as: ${token.studentId}");
@@ -305,8 +308,8 @@ Future<AppInitialization> initializeApp() async {
try {
if (Platform.isAndroid) {
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(";");
devInfo = DeviceInfo(rawInfo[0], rawInfo[1], rawInfo[2]);
devInfoFetched = true;
@@ -335,8 +338,9 @@ Future<AppInitialization> initializeApp() async {
if (Platform.isIOS) {
try {
await LiveActivityService.initialize()
.timeout(const Duration(seconds: 8));
await LiveActivityService.initialize().timeout(
const Duration(seconds: 8),
);
} on TimeoutException catch (e, st) {
logger.warning('LiveActivity init timed out: $e', e, st);
} catch (e, st) {
@@ -359,131 +363,142 @@ void main() async {
dio.options.receiveTimeout = Duration(seconds: 3);
dio.options.validateStatus = (status) => status != null && status < 500;
runZonedGuarded(() async {
logger.finest("Initializing app");
WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
runZonedGuarded(
() async {
logger.finest("Initializing app");
WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
try {
await dotenv.load(fileName: ".env");
logger.info("Environment variables loaded");
} catch (e, st) {
logger.severe("Failed to load .env: $e", e, st);
}
try {
await dotenv.load(fileName: ".env");
logger.info("Environment variables loaded");
} catch (e, st) {
logger.severe("Failed to load .env: $e", e, st);
}
{
final jwtPattern =
RegExp(r'([A-Za-z0-9-_]+)\.([A-Za-z0-9-_]+)\.([A-Za-z0-9-_]+)');
final omPattern = RegExp(r'(\d{3})(\d{6})([A-Za-z0-9]?)');
final refreshTokenPattern =
RegExp(r'"(?=.{21,}$)([A-Z0-9]+-[A-Z0-9_\-.~+]*)"');
{
final jwtPattern = RegExp(
r'([A-Za-z0-9-_]+)\.([A-Za-z0-9-_]+)\.([A-Za-z0-9-_]+)',
);
final omPattern = RegExp(r'(\d{3})(\d{6})([A-Za-z0-9]?)');
final refreshTokenPattern = RegExp(
r'"(?=.{21,}$)([A-Z0-9]+-[A-Z0-9_\-.~+]*)"',
);
final docs = await getApplicationDocumentsDirectory();
Future<void> deleteOldLogFiles() async {
final docs = await getApplicationDocumentsDirectory();
final dir = Directory(docs.path);
if (!dir.existsSync()) return;
final now = DateTime.now();
final cutoff = now.subtract(Duration(days: 30));
Future<void> deleteOldLogFiles() async {
final docs = await getApplicationDocumentsDirectory();
final dir = Directory(docs.path);
if (!dir.existsSync()) return;
final logFileRegex = RegExp(r'^(\d{4})_(\d{2})_(\d{2})\.log$');
final now = DateTime.now();
final cutoff = now.subtract(Duration(days: 30));
for (final entity in dir.listSync()) {
if (entity is! File) continue;
final name = entity.uri.pathSegments.last;
final m = logFileRegex.firstMatch(name);
if (m == null) continue;
final logFileRegex = RegExp(r'^(\d{4})_(\d{2})_(\d{2})\.log$');
try {
final y = int.parse(m.group(1)!);
final mo = int.parse(m.group(2)!);
final d = int.parse(m.group(3)!);
final fileDate = DateTime(y, mo, d);
if (fileDate
.isBefore(DateTime(cutoff.year, cutoff.month, cutoff.day))) {
logger.info("Removing old log file: $name");
await entity.delete();
for (final entity in dir.listSync()) {
if (entity is! File) continue;
final name = entity.uri.pathSegments.last;
final m = logFileRegex.firstMatch(name);
if (m == null) continue;
try {
final y = int.parse(m.group(1)!);
final mo = int.parse(m.group(2)!);
final d = int.parse(m.group(3)!);
final fileDate = DateTime(y, mo, d);
if (fileDate.isBefore(
DateTime(cutoff.year, cutoff.month, cutoff.day),
)) {
logger.info("Removing old log file: $name");
await entity.delete();
}
} catch (_) {
// ignore parse/delete errors
}
} catch (_) {
// ignore parse/delete errors
}
}
}
String logFilePathForDate(DateTime dt) {
final fileName = "${DateFormat("yyyy_MM_dd").format(dt)}.log";
return Directory(docs.path).uri.resolve(fileName).toFilePath();
}
File fileForDate(DateTime dt) {
final path = logFilePathForDate(dt);
final file = File(path);
if (!file.existsSync()) file.createSync(recursive: true);
return file;
}
String censorLog(String msg) {
return msg.replaceAll(jwtPattern, '***').replaceAllMapped(omPattern,
(match) {
return "${match.group(1)}******${match.group(3)}";
}).replaceAll(refreshTokenPattern, '"***"');
}
hierarchicalLoggingEnabled = true;
logger.level = Level.ALL;
DateTime currentDate = DateTime.now();
IOSink sink = fileForDate(currentDate).openWrite(mode: FileMode.append);
logger.onRecord.listen((record) {
final now = DateTime.now();
if (now.year != currentDate.year ||
now.month != currentDate.month ||
now.day != currentDate.day) {
sink.flush();
sink.close();
currentDate = now;
sink = fileForDate(currentDate).openWrite(mode: FileMode.append);
String logFilePathForDate(DateTime dt) {
final fileName = "${DateFormat("yyyy_MM_dd").format(dt)}.log";
return Directory(docs.path).uri.resolve(fileName).toFilePath();
}
final censored = censorLog(record.message);
final timestamp = DateFormat('yyyy-MM-dd HH:mm:ss.SSS').format(now);
final level = record.level.name;
final line = '[$timestamp] [$level] [$censored]';
sink.writeln(line);
File fileForDate(DateTime dt) {
final path = logFilePathForDate(dt);
final file = File(path);
if (!file.existsSync()) file.createSync(recursive: true);
return file;
}
debugPrint(
"[Firka] [${record.level.name}] ${kDebugMode ? record.message : censored}");
});
String censorLog(String msg) {
return msg
.replaceAll(jwtPattern, '***')
.replaceAllMapped(omPattern, (match) {
return "${match.group(1)}******${match.group(3)}";
})
.replaceAll(refreshTokenPattern, '"***"');
}
(() async {
await deleteOldLogFiles();
})();
}
hierarchicalLoggingEnabled = true;
logger.level = Level.ALL;
try {
logger.finest('loading dirty words');
await loadDirtyWords();
logger.finest('loaded dirty words');
} catch (e, st) {
logger.severe('Failed to load dirty words: $e', e, st);
}
DateTime currentDate = DateTime.now();
IOSink sink = fileForDate(currentDate).openWrite(mode: FileMode.append);
// Run App Initialization
runApp(InitializationScreen());
}, (error, stackTrace) {
logger.shout('Caught error: $error');
logger.shout('Stack trace: $stackTrace');
logger.onRecord.listen((record) {
final now = DateTime.now();
if (now.year != currentDate.year ||
now.month != currentDate.month ||
now.day != currentDate.day) {
sink.flush();
sink.close();
currentDate = now;
sink = fileForDate(currentDate).openWrite(mode: FileMode.append);
}
navigatorKey.currentState?.push(
MaterialPageRoute(
builder: (context) =>
ErrorPage(key: ValueKey('errorPage'), exception: error.toString()),
),
);
});
final censored = censorLog(record.message);
final timestamp = DateFormat('yyyy-MM-dd HH:mm:ss.SSS').format(now);
final level = record.level.name;
final line = '[$timestamp] [$level] [$censored]';
sink.writeln(line);
debugPrint(
"[Firka] [${record.level.name}] ${kDebugMode ? record.message : censored}",
);
});
(() async {
await deleteOldLogFiles();
})();
}
try {
logger.finest('loading dirty words');
await loadDirtyWords();
logger.finest('loaded dirty words');
} catch (e, st) {
logger.severe('Failed to load dirty words: $e', e, st);
}
// Run App Initialization
runApp(InitializationScreen());
},
(error, stackTrace) {
logger.shout('Caught error: $error');
logger.shout('Stack trace: $stackTrace');
navigatorKey.currentState?.push(
MaterialPageRoute(
builder: (context) => ErrorPage(
key: ValueKey('errorPage'),
exception: error.toString(),
),
),
);
},
);
}
final ValueNotifier<bool> isLightMode = ValueNotifier<bool>(true);
@@ -492,8 +507,9 @@ final UpdateNotifier globalUpdate = UpdateNotifier();
class InitializationScreen extends StatelessWidget {
InitializationScreen({super.key});
final Future<AppInitialization> _init =
initializeApp().timeout(const Duration(seconds: 20));
final Future<AppInitialization> _init = initializeApp().timeout(
const Duration(seconds: 20),
);
@override
Widget build(BuildContext context) {
@@ -503,8 +519,11 @@ class InitializationScreen extends StatelessWidget {
// Check if initialization is complete
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
logger.shout("Error in InitializationScreen",
snapshot.error.toString(), snapshot.stackTrace);
logger.shout(
"Error in InitializationScreen",
snapshot.error.toString(),
snapshot.stackTrace,
);
FlutterNativeSplash.remove();
@@ -512,15 +531,16 @@ class InitializationScreen extends StatelessWidget {
return MaterialApp(
key: ValueKey('errorPage'),
home: DefaultAssetBundle(
bundle: FirkaBundle(),
child: Scaffold(
body: Center(
child: Text(
'Error initializing app: ${snapshot.error}',
style: TextStyle(color: Colors.red),
),
bundle: FirkaBundle(),
child: Scaffold(
body: Center(
child: Text(
'Error initializing app: ${snapshot.error}',
style: TextStyle(color: Colors.red),
),
)),
),
),
),
);
}
@@ -540,7 +560,8 @@ class InitializationScreen extends StatelessWidget {
await WatchSyncHelper.sendLanguageToWatch();
} catch (e) {
logger.warning(
'[Init] Failed to publish language to Watch after sync init: $e');
'[Init] Failed to publish language to Watch after sync init: $e',
);
}
}());
}
@@ -562,8 +583,11 @@ class InitializationScreen extends StatelessWidget {
watch.sendMessage({"id": "pong"});
navigatorKey.currentState?.push(
MaterialPageRoute(
builder: (context) => HomeScreen(initData, true,
model: msg["model"] as String),
builder: (context) => HomeScreen(
initData,
true,
model: msg["model"] as String,
),
),
);
}
@@ -572,16 +596,9 @@ class InitializationScreen extends StatelessWidget {
}
if (snapshot.data!.tokens.isEmpty) {
screen = LoginScreen(
initData,
key: ValueKey('loginScreen'),
);
screen = LoginScreen(initData, key: ValueKey('loginScreen'));
} else {
screen = HomeScreen(
initData,
false,
key: ValueKey('homeScreen'),
);
screen = HomeScreen(initData, false, key: ValueKey('homeScreen'));
}
return MaterialApp(
@@ -606,10 +623,12 @@ class InitializationScreen extends StatelessWidget {
builder: (context, isLight, _) {
final overlay = SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness:
isLight ? Brightness.dark : Brightness.light,
statusBarBrightness:
isLight ? Brightness.light : Brightness.dark,
statusBarIconBrightness: isLight
? Brightness.dark
: Brightness.light,
statusBarBrightness: isLight
? Brightness.light
: Brightness.dark,
systemStatusBarContrastEnforced: false,
);
@@ -625,27 +644,17 @@ class InitializationScreen extends StatelessWidget {
),
routes: {
'/login': (context) => DefaultAssetBundle(
bundle: FirkaBundle(),
child: LoginScreen(
initData,
key: ValueKey('loginScreen'),
),
),
bundle: FirkaBundle(),
child: LoginScreen(initData, key: ValueKey('loginScreen')),
),
'/home': (context) => DefaultAssetBundle(
bundle: FirkaBundle(),
child: HomeScreen(
initData,
false,
key: ValueKey('homeScreen'),
),
),
bundle: FirkaBundle(),
child: HomeScreen(initData, false, key: ValueKey('homeScreen')),
),
'/debug': (context) => DefaultAssetBundle(
bundle: FirkaBundle(),
child: DebugScreen(
initData,
key: ValueKey('debugScreen'),
),
),
bundle: FirkaBundle(),
child: DebugScreen(initData, key: ValueKey('debugScreen')),
),
},
);
}

View File

@@ -1,5 +1,8 @@
import 'package:flutter/material.dart';
// Design system token names; ignore non_constant_identifier_names for consistency with design specs
// ignore_for_file: non_constant_identifier_names
class FirkaFonts {
TextStyle H_H1;
TextStyle H_18px;
@@ -126,8 +129,11 @@ class FirkaStyle {
FirkaFonts fonts;
bool isLight;
FirkaStyle(
{required this.isLight, required this.colors, required this.fonts});
FirkaStyle({
required this.isLight,
required this.colors,
required this.fonts,
});
}
final _defaultFonts = FirkaFonts(
@@ -222,80 +228,82 @@ final _defaultFonts = FirkaFonts(
);
final FirkaStyle lightStyle = FirkaStyle(
isLight: true,
colors: FirkaColors(
background: Color(0xFFFAFFF0),
backgroundAmoled: Colors.black,
background0p: Color(0x00fafff0),
success: Color(0xFF92EA3B),
shadowBlur: 2,
textPrimary: Color(0xFF394C0A),
textSecondary: Color(0xCC394C0A),
textTertiary: Color(0x80394C0A),
textPrimaryLight: Color(0xFF394C0A),
textSecondaryLight: Color(0xCC394C0A),
textTertiaryLight: Color(0x80394C0A),
card: Color(0xFFF3FBDE),
cardTranslucent: Color(0x80F3FBDE),
buttonSecondaryFill: Color(0xFFFEFFFD),
accent: Color(0xFFA7DC22),
secondary: Color(0xFF6E8F1B),
shadowColor: Color(0x33647e22),
a10p: Color(0x1aa7dc22),
a15p: Color(0x26a7dc22),
warningAccent: Color(0xFFFFA046),
warningText: Color(0xFF8F531B),
warning15p: Color(0x26FFA046),
warningCard: Color(0xFFFAEBDC),
errorAccent: Color(0xFFFF54A1),
errorText: Color(0xFF8F1B4F),
error15p: Color(0x26FF54A1),
errorCard: Color(0xFFFADCE9),
grade5: Color(0xFF22CCAD),
grade4: Color(0xFF92EA3B),
grade3: Color(0xFFF9CF00),
grade2: Color(0xFFFFA046),
grade1: Color(0xFFFF54A1),
),
fonts: _defaultFonts);
isLight: true,
colors: FirkaColors(
background: Color(0xFFFAFFF0),
backgroundAmoled: Colors.black,
background0p: Color(0x00fafff0),
success: Color(0xFF92EA3B),
shadowBlur: 2,
textPrimary: Color(0xFF394C0A),
textSecondary: Color(0xCC394C0A),
textTertiary: Color(0x80394C0A),
textPrimaryLight: Color(0xFF394C0A),
textSecondaryLight: Color(0xCC394C0A),
textTertiaryLight: Color(0x80394C0A),
card: Color(0xFFF3FBDE),
cardTranslucent: Color(0x80F3FBDE),
buttonSecondaryFill: Color(0xFFFEFFFD),
accent: Color(0xFFA7DC22),
secondary: Color(0xFF6E8F1B),
shadowColor: Color(0x33647e22),
a10p: Color(0x1aa7dc22),
a15p: Color(0x26a7dc22),
warningAccent: Color(0xFFFFA046),
warningText: Color(0xFF8F531B),
warning15p: Color(0x26FFA046),
warningCard: Color(0xFFFAEBDC),
errorAccent: Color(0xFFFF54A1),
errorText: Color(0xFF8F1B4F),
error15p: Color(0x26FF54A1),
errorCard: Color(0xFFFADCE9),
grade5: Color(0xFF22CCAD),
grade4: Color(0xFF92EA3B),
grade3: Color(0xFFF9CF00),
grade2: Color(0xFFFFA046),
grade1: Color(0xFFFF54A1),
),
fonts: _defaultFonts,
);
final FirkaStyle darkStyle = FirkaStyle(
isLight: false,
colors: FirkaColors(
background: Color(0xFF0D1202),
backgroundAmoled: Colors.black,
background0p: Color(0x00fafff0),
success: Color(0xFF92EA3B),
shadowBlur: 0,
textPrimary: Color(0xFFEAF7CC),
textSecondary: Color(0xB3EAF7CC),
textTertiary: Color(0x80EAF7CC),
textPrimaryLight: Color(0xFF394C0A),
textSecondaryLight: Color(0xCC394C0A),
textTertiaryLight: Color(0x80394C0A),
card: Color(0xFF141905),
cardTranslucent: Color(0x80141905),
buttonSecondaryFill: Color(0xFF20290B),
accent: Color(0xFFA7DC22),
secondary: Color(0xFFCBEE71),
shadowColor: Color(0x26CBEE71),
a10p: Color(0x1AA7DC22),
a15p: Color(0x26A7DC22),
warningAccent: Color(0xFFFFA046),
warningText: Color(0xFFF0B37A),
warning15p: Color(0x26FFA046),
warningCard: Color(0xFF201203),
errorAccent: Color(0xFFFF54A1),
errorText: Color(0xFFF59EC5),
error15p: Color(0x26FF54A1),
errorCard: Color(0xFF1E030F),
grade5: Color(0xFF22CCAD),
grade4: Color(0xFF92EA3B),
grade3: Color(0xFFF9CF00),
grade2: Color(0xFFFFA046),
grade1: Color(0xFFFF54A1),
),
fonts: _defaultFonts);
isLight: false,
colors: FirkaColors(
background: Color(0xFF0D1202),
backgroundAmoled: Colors.black,
background0p: Color(0x00fafff0),
success: Color(0xFF92EA3B),
shadowBlur: 0,
textPrimary: Color(0xFFEAF7CC),
textSecondary: Color(0xB3EAF7CC),
textTertiary: Color(0x80EAF7CC),
textPrimaryLight: Color(0xFF394C0A),
textSecondaryLight: Color(0xCC394C0A),
textTertiaryLight: Color(0x80394C0A),
card: Color(0xFF141905),
cardTranslucent: Color(0x80141905),
buttonSecondaryFill: Color(0xFF20290B),
accent: Color(0xFFA7DC22),
secondary: Color(0xFFCBEE71),
shadowColor: Color(0x26CBEE71),
a10p: Color(0x1AA7DC22),
a15p: Color(0x26A7DC22),
warningAccent: Color(0xFFFFA046),
warningText: Color(0xFFF0B37A),
warning15p: Color(0x26FFA046),
warningCard: Color(0xFF201203),
errorAccent: Color(0xFFFF54A1),
errorText: Color(0xFFF59EC5),
error15p: Color(0x26FF54A1),
errorCard: Color(0xFF1E030F),
grade5: Color(0xFF22CCAD),
grade4: Color(0xFF92EA3B),
grade3: Color(0xFFF9CF00),
grade2: Color(0xFFFFA046),
grade1: Color(0xFFFF54A1),
),
fonts: _defaultFonts,
);
FirkaStyle appStyle = lightStyle;
FirkaStyle wearStyle = darkStyle;

View File

@@ -12,167 +12,147 @@ class ErrorPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: appStyle.colors.background,
body: Padding(
padding: const EdgeInsets.symmetric(vertical: 64, horizontal: 24),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(children: [
SizedBox(height: 48),
Container(
width: 50,
height: 50,
clipBehavior: Clip.antiAlias,
decoration: ShapeDecoration(
image: DecorationImage(
image: PreloadedImageProvider(FirkaBundle(),
('assets/images/logos/dave_error.png')),
fit: BoxFit.cover,
),
shape: ContinuousRectangleBorder(),
backgroundColor: appStyle.colors.background,
body: Padding(
padding: const EdgeInsets.symmetric(vertical: 64, horizontal: 24),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
children: [
SizedBox(height: 48),
Container(
width: 50,
height: 50,
clipBehavior: Clip.antiAlias,
decoration: ShapeDecoration(
image: DecorationImage(
image: PreloadedImageProvider(
FirkaBundle(),
('assets/images/logos/dave_error.png'),
),
fit: BoxFit.cover,
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 24, horizontal: 32),
child: Column(children: [
Text(
'e-Kréta, te',
style: appStyle.fonts.H_16px
.copyWith(color: appStyle.colors.textSecondary),
textAlign: TextAlign.center,
),
Text(
//'a,',
generateSwearSentence(),
style: appStyle.fonts.H_H2.copyWith(
color: appStyle.colors.textPrimary,
fontSize: 24),
textAlign: TextAlign.center,
),
])),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
"Valami probléma történt, ez természetesen az EduDev Zrt. hibája minden esetben",
style: appStyle.fonts.B_14R.copyWith(
shape: ContinuousRectangleBorder(),
),
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 24,
horizontal: 32,
),
child: Column(
children: [
Text(
'e-Kréta, te',
style: appStyle.fonts.H_16px.copyWith(
color: appStyle.colors.textSecondary,
),
textAlign: TextAlign.center,
),
),
]),
Column(
children: [
Stack(children: [
Container(
height: 300,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
shape: BoxShape.rectangle,
color: appStyle.colors.card,
borderRadius: BorderRadius.circular(12)),
child: Text(exception,
style: appStyle.fonts.B_14R.copyWith(
color: appStyle.colors.textPrimary,
fontFamily: 'RobotoMono'))),
Positioned(
bottom: 0,
right: 16,
child: Container(
width: 60,
height: 76,
clipBehavior: Clip.antiAlias,
decoration: ShapeDecoration(
image: DecorationImage(
image: PreloadedImageProvider(FirkaBundle(),
('assets/images/cactus_error_screen.png')),
fit: BoxFit.cover,
colorFilter: ColorFilter.srgbToLinearGamma()),
shape: ContinuousRectangleBorder(),
),
),
)
]),
SizedBox(
height: 20,
Text(
//'a,',
generateSwearSentence(),
style: appStyle.fonts.H_H2.copyWith(
color: appStyle.colors.textPrimary,
fontSize: 24,
),
textAlign: TextAlign.center,
),
ElevatedButton(
// TODO: report bug
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
foregroundColor: appStyle.colors.textPrimary,
backgroundColor: appStyle.colors.accent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
elevation: 1,
shadowColor: appStyle.colors.shadowColor,
minimumSize: Size.fromHeight(48)),
child: Text("Hiba jelentése",
style: appStyle.fonts.H_18px
.copyWith(fontWeight: FontWeight.w700))),
SizedBox(
height: 8,
),
ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
foregroundColor: appStyle.colors.textSecondary,
backgroundColor:
appStyle.colors.buttonSecondaryFill,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
elevation: 1,
shadowColor: appStyle.colors.shadowColor,
minimumSize: Size.fromHeight(48)),
child: Text("Vissza", style: appStyle.fonts.B_16R))
],
)
])));
return Scaffold(
appBar: AppBar(
title: Text('Error Occurred'),
backgroundColor: Colors.red,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error,
size: 80,
color: Colors.red,
),
const SizedBox(height: 20),
Text(
'An error occurred!',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
color: Colors.red,
),
),
const SizedBox(height: 10),
Text(
'Details:',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
"Valami probléma történt, ez természetesen az EduDev Zrt. hibája minden esetben",
style: appStyle.fonts.B_14R.copyWith(
color: appStyle.colors.textSecondary,
),
textAlign: TextAlign.center,
),
),
],
),
const SizedBox(height: 10),
Text(
exception,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.redAccent,
Column(
children: [
Stack(
children: [
Container(
height: 300,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
shape: BoxShape.rectangle,
color: appStyle.colors.card,
borderRadius: BorderRadius.circular(12),
),
child: Text(
exception,
style: appStyle.fonts.B_14R.copyWith(
color: appStyle.colors.textPrimary,
fontFamily: 'RobotoMono',
),
),
),
Positioned(
bottom: 0,
right: 16,
child: Container(
width: 60,
height: 76,
clipBehavior: Clip.antiAlias,
decoration: ShapeDecoration(
image: DecorationImage(
image: PreloadedImageProvider(
FirkaBundle(),
('assets/images/cactus_error_screen.png'),
),
fit: BoxFit.cover,
colorFilter: ColorFilter.srgbToLinearGamma(),
),
shape: ContinuousRectangleBorder(),
),
),
),
],
),
SizedBox(height: 20),
ElevatedButton(
// TODO: report bug
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
foregroundColor: appStyle.colors.textPrimary,
backgroundColor: appStyle.colors.accent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 1,
shadowColor: appStyle.colors.shadowColor,
minimumSize: Size.fromHeight(48),
),
),
const SizedBox(height: 30),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('Go Back'),
child: Text(
"Hiba jelentése",
style: appStyle.fonts.H_18px.copyWith(
fontWeight: FontWeight.w700,
),
),
),
SizedBox(height: 8),
ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
foregroundColor: appStyle.colors.textSecondary,
backgroundColor: appStyle.colors.buttonSecondaryFill,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 1,
shadowColor: appStyle.colors.shadowColor,
minimumSize: Size.fromHeight(48),
),
child: Text("Vissza", style: appStyle.fonts.B_16R),
),
],
),
],
),

View File

@@ -17,31 +17,27 @@ class WearErrorPage extends StatelessWidget {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error,
size: 80,
color: Colors.red,
),
Icon(Icons.error, size: 80, color: Colors.red),
const SizedBox(height: 20),
Text(
'An error occurred!',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
color: Colors.red,
),
style: Theme.of(
context,
).textTheme.headlineMedium?.copyWith(color: Colors.red),
),
const SizedBox(height: 10),
Text(
'Details:',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
style: Theme.of(
context,
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
Text(
exception,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.redAccent,
),
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(color: Colors.redAccent),
),
const SizedBox(height: 30),
ElevatedButton(

View File

@@ -18,14 +18,19 @@ void showExtrasBottomSheet(BuildContext context, AppInitialization data) {
logger.finest("showExtrasBottomSheet() developer mode: ${isDeveloper()}");
if (isDeveloper()) {
debugBtn = (double itemWidth) => GestureDetector( // Fejlesztői menü
debugBtn = (double itemWidth) => GestureDetector(
// Fejlesztői menü
onTap: () => {
Navigator.pop(context),
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DefaultAssetBundle(
bundle: FirkaBundle(), child: DebugScreen(data))))
context,
MaterialPageRoute(
builder: (context) => DefaultAssetBundle(
bundle: FirkaBundle(),
child: DebugScreen(data),
),
),
),
},
child: SizedBox(
height: 60,
@@ -51,7 +56,9 @@ void showExtrasBottomSheet(BuildContext context, AppInitialization data) {
Text(
data.l10n.debug_screen,
textAlign: TextAlign.right,
style: appStyle.fonts.B_16R.apply(color: appStyle.colors.textPrimary),
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
),
],
),
@@ -97,34 +104,44 @@ void showExtrasBottomSheet(BuildContext context, AppInitialization data) {
child: Stack(
children: [
Column(
children: [
Align(
alignment: Alignment.centerLeft,
child: Text(
children: [
Align(
alignment: Alignment.centerLeft,
child: Text(
data.l10n.other,
style: appStyle.fonts.H_H2.apply(color: appStyle.colors.textPrimary),
style: appStyle.fonts.H_H2.apply(
color: appStyle.colors.textPrimary,
),
),
SizedBox(height: 8),
LayoutBuilder(builder: (context, constraints) {
),
SizedBox(height: 8),
LayoutBuilder(
builder: (context, constraints) {
final itemWidth = (constraints.maxWidth - 8) / 2;
return Wrap(
spacing: 2,
runSpacing: 2,
children: [
debugBtn(itemWidth),
GestureDetector( // Fiókod
GestureDetector(
// Fiókod
onTap: () {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DefaultAssetBundle(
bundle: FirkaBundle(),
child: SettingsScreen(
data,
data.settings.items
.group("profile_settings")))));
builder: (context) =>
DefaultAssetBundle(
bundle: FirkaBundle(),
child: SettingsScreen(
data,
data.settings.items.group(
"profile_settings",
),
),
),
),
);
},
child: SizedBox(
height: 60,
@@ -133,11 +150,15 @@ void showExtrasBottomSheet(BuildContext context, AppInitialization data) {
shadow: true,
child: Card(
color: appStyle.colors.card,
shadowColor: isLightMode.value ? null : Colors.transparent,
shadowColor: isLightMode.value
? null
: Colors.transparent,
child: Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
),
child: Row(
children: [
FirkaIconWidget(
@@ -150,7 +171,12 @@ void showExtrasBottomSheet(BuildContext context, AppInitialization data) {
Text(
data.l10n.s_your_account,
textAlign: TextAlign.right,
style: appStyle.fonts.B_16R.apply(color: appStyle.colors.textPrimary),
style: appStyle.fonts.B_16R
.apply(
color: appStyle
.colors
.textPrimary,
),
),
],
),
@@ -160,16 +186,23 @@ void showExtrasBottomSheet(BuildContext context, AppInitialization data) {
),
),
),
GestureDetector( // Beállítás
GestureDetector(
// Beállítás
onTap: () {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DefaultAssetBundle(
bundle: FirkaBundle(),
child: SettingsScreen(
data, data.settings.items))));
builder: (context) =>
DefaultAssetBundle(
bundle: FirkaBundle(),
child: SettingsScreen(
data,
data.settings.items,
),
),
),
);
},
child: SizedBox(
height: 60,
@@ -178,11 +211,15 @@ void showExtrasBottomSheet(BuildContext context, AppInitialization data) {
shadow: true,
child: Card(
color: appStyle.colors.card,
shadowColor: isLightMode.value ? null : Colors.transparent,
shadowColor: isLightMode.value
? null
: Colors.transparent,
child: Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
),
child: Row(
children: [
FirkaIconWidget(
@@ -195,7 +232,12 @@ void showExtrasBottomSheet(BuildContext context, AppInitialization data) {
Text(
data.l10n.settings_screen,
textAlign: TextAlign.right,
style: appStyle.fonts.B_16R.apply(color: appStyle.colors.textPrimary),
style: appStyle.fonts.B_16R
.apply(
color: appStyle
.colors
.textPrimary,
),
),
],
),
@@ -208,7 +250,8 @@ void showExtrasBottomSheet(BuildContext context, AppInitialization data) {
// Ide jön a többi gomb majd
],
);
}),
},
),
],
),
Padding(
@@ -219,17 +262,22 @@ void showExtrasBottomSheet(BuildContext context, AppInitialization data) {
SizedBox(),
GestureDetector(
child: Text(
"v${data.packageInfo.version} ${isBeta ? "beta" : ""}",
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textTertiary)),
"v${data.packageInfo.version} ${isBeta ? "beta" : ""}",
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textTertiary,
),
),
onTap: () async {
if (isDebug()) return;
if (debugCounter == 10) {
data.settings.group("settings").setBoolean(
"developer_enabled",
!data.settings
.group("settings")
.boolean("developer_enabled"));
data.settings
.group("settings")
.setBoolean(
"developer_enabled",
!data.settings
.group("settings")
.boolean("developer_enabled"),
);
await data.isar.writeTxn(() async {
await data.settings
@@ -241,18 +289,21 @@ void showExtrasBottomSheet(BuildContext context, AppInitialization data) {
.group("settings")["developer_enabled"]!
.postUpdate();
Navigator.of(navigatorKey.currentContext!)
.popUntil((route) => false);
Navigator.of(
navigatorKey.currentContext!,
).popUntil((route) => false);
Navigator.push(
navigatorKey.currentContext!,
MaterialPageRoute(
builder: (context) => DefaultAssetBundle(
bundle: FirkaBundle(),
child: HomeScreen(
data,
false,
key: ValueKey('homeScreen'),
))),
builder: (context) => DefaultAssetBundle(
bundle: FirkaBundle(),
child: HomeScreen(
data,
false,
key: ValueKey('homeScreen'),
),
),
),
);
} else if (debugCounter < 10) {
debugCounter++;

View File

@@ -31,12 +31,7 @@ void showErrorBottomSheet(BuildContext context, String err) {
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: Text(
err,
style: appStyle.fonts.B_16R,
),
),
child: Center(child: Text(err, style: appStyle.fonts.B_16R)),
),
),
),

View File

@@ -4,12 +4,17 @@ import 'package:firka/ui/model/style.dart';
import 'package:firka/ui/phone/widgets/login_webview.dart';
import 'package:flutter/material.dart';
void showReauthBottomSheet(BuildContext context, AppInitialization data, String message) {
final accountPicker = (data.settings
.group("profile_settings")["e_kreta_account_picker"]
as SettingsKretenAccountPicker);
void showReauthBottomSheet(
BuildContext context,
AppInitialization data,
String message,
) {
final accountPicker =
(data.settings.group("profile_settings")["e_kreta_account_picker"]
as SettingsKretenAccountPicker);
final currentToken = data.tokens.isNotEmpty && accountPicker.accountIndex < data.tokens.length
final currentToken =
data.tokens.isNotEmpty && accountPicker.accountIndex < data.tokens.length
? data.tokens[accountPicker.accountIndex]
: null;

View File

@@ -8,14 +8,20 @@ import 'package:watch_connectivity/watch_connectivity.dart';
import '../../../model/style.dart';
void showWearBottomSheet(
BuildContext context, AppInitialization data, String model) async {
BuildContext context,
AppInitialization data,
String model,
) async {
final watch = WatchConnectivity();
final timetable = await data.client
.getTimeTable(timeNow(), timeNow().add(Duration(days: 7)));
final timetable = await data.client.getTimeTable(
timeNow(),
timeNow().add(Duration(days: 7)),
);
if (timetable.err != null) {
return;
}
if (!context.mounted) return;
List<Map<String, dynamic>> timetableArray = List.empty(growable: true);
@@ -58,22 +64,29 @@ void showWearBottomSheet(
SvgPicture.asset("assets/images/wear_pair.svg"),
SizedBox(height: 32),
Center(
child: Text(data.l10n.pairing,
style: appStyle.fonts.H_14px
.apply(color: appStyle.colors.secondary)),
child: Text(
data.l10n.pairing,
style: appStyle.fonts.H_14px.apply(
color: appStyle.colors.secondary,
),
),
),
Center(
child: Text(model,
style: appStyle.fonts.H_H2
.apply(color: appStyle.colors.textPrimary)),
child: Text(
model,
style: appStyle.fonts.H_H2.apply(
color: appStyle.colors.textPrimary,
),
),
),
Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6),
child: Text(
data.l10n.pairing_description,
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textPrimary),
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
textAlign: TextAlign.center,
),
),
@@ -87,9 +100,10 @@ void showWearBottomSheet(
center: [
Text(
data.l10n.pair,
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textSecondary),
)
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
),
],
color: appStyle.colors.accent,
),
@@ -104,7 +118,10 @@ void showWearBottomSheet(
"idToken": data.client.model.idToken,
"accessToken": data.client.model.accessToken,
"refreshToken": data.client.model.refreshToken,
"expiryDate": data.client.model.expiryDate!
"expiryDate": data
.client
.model
.expiryDate!
.millisecondsSinceEpoch,
},
});
@@ -120,9 +137,10 @@ void showWearBottomSheet(
center: [
Text(
data.l10n.cancel,
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textSecondary),
)
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
),
],
color: appStyle.colors.buttonSecondaryFill,
),

View File

@@ -10,10 +10,10 @@ class ReauthToastWidget extends StatefulWidget {
final Function() onDismiss;
const ReauthToastWidget({
Key? key,
super.key,
required this.data,
required this.onDismiss,
}) : super(key: key);
});
@override
State<ReauthToastWidget> createState() => _ReauthToastWidgetState();
@@ -32,7 +32,11 @@ class _ReauthToastWidgetState extends State<ReauthToastWidget> {
child: Center(
child: GestureDetector(
onTap: () {
showReauthBottomSheet(context, widget.data, widget.data.l10n.reauth);
showReauthBottomSheet(
context,
widget.data,
widget.data.l10n.reauth,
);
},
onVerticalDragUpdate: (details) {
setState(() {
@@ -62,8 +66,9 @@ class _ReauthToastWidgetState extends State<ReauthToastWidget> {
children: [
Text(
widget.data.l10n.reauth,
style: appStyle.fonts.B_16SB
.copyWith(color: appStyle.colors.errorText),
style: appStyle.fonts.B_16SB.copyWith(
color: appStyle.colors.errorText,
),
),
SizedBox(width: 8),
FirkaIconWidget(
@@ -82,9 +87,10 @@ class _ReauthToastWidgetState extends State<ReauthToastWidget> {
}
}
Widget buildReauthToast(BuildContext context, AppInitialization data, Function() onDismiss) {
return ReauthToastWidget(
data: data,
onDismiss: onDismiss,
);
Widget buildReauthToast(
BuildContext context,
AppInitialization data,
Function() onDismiss,
) {
return ReauthToastWidget(data: data, onDismiss: onDismiss);
}

View File

@@ -26,8 +26,12 @@ class HomeGradesScreen extends StatefulWidget {
final void Function(int) pageController;
const HomeGradesScreen(
this.data, this.updateNotifier, this.finishNotifier, this.pageController,
{super.key});
this.data,
this.updateNotifier,
this.finishNotifier,
this.pageController, {
super.key,
});
@override
State<StatefulWidget> createState() => _HomeGradesScreen();
@@ -53,23 +57,25 @@ class _HomeGradesScreen extends FirkaState<HomeGradesScreen> {
widget.updateNotifier.addListener(updateListener);
}
void updateListener() async {
var now = timeNow();
var start = now.subtract(Duration(days: now.weekday - 1));
var end = start.add(Duration(days: 6));
void updateListener() async {
var now = timeNow();
var start = now.subtract(Duration(days: now.weekday - 1));
var end = start.add(Duration(days: 6));
grades = await widget.data.client.getGrades(forceCache: false);
week = await widget.data.client.getTimeTable(start, end, forceCache: false);
classGroups = await widget.data.client.getClassGroups(forceCache: false);
if (classGroups?.response?.isNotEmpty ?? false) {
var group = classGroups!.response!.first;
lessons = await widget.data.client.getSubjectAverage(group, forceCache: false);
await Future.delayed(Duration(milliseconds: 100));
grades = await widget.data.client.getGrades(forceCache: false);
week = await widget.data.client.getTimeTable(start, end, forceCache: false);
classGroups = await widget.data.client.getClassGroups(forceCache: false);
if (classGroups?.response?.isNotEmpty ?? false) {
var group = classGroups!.response!.first;
lessons = await widget.data.client.getSubjectAverage(
group,
forceCache: false,
);
await Future.delayed(Duration(milliseconds: 100));
}
if (mounted) setState(() {});
widget.finishNotifier.update();
}
if (mounted) setState(() {});
widget.finishNotifier.update();
}
@override
void initState() {
@@ -89,7 +95,7 @@ void updateListener() async {
var group = classGroups!.response!.first;
lessons = await widget.data.client.getSubjectAverage(group);
await Future.delayed(Duration(milliseconds: 100));
}
}
if (mounted) setState(() {});
})();
}
@@ -107,11 +113,7 @@ void updateListener() async {
height: MediaQuery.of(context).size.height / 1.35,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(),
DelayedSpinnerWidget(),
SizedBox(),
],
children: [SizedBox(), DelayedSpinnerWidget(), SizedBox()],
),
);
} else {
@@ -130,10 +132,9 @@ void updateListener() async {
if (lessons != null && lessons!.response != null) {
for (var lesson in lessons!.response!) {
if (subjects
.where((s) => s.uid == lesson.uid)
.isEmpty) {
subjects.add(Subject(
if (subjects.where((s) => s.uid == lesson.uid).isEmpty) {
subjects.add(
Subject(
uid: lesson.uid,
name: lesson.name,
category: NameUidDesc(
@@ -142,7 +143,8 @@ void updateListener() async {
description: lesson.subjectCategoryDescription,
),
sortIndex: lesson.sortIndex,
));
),
);
}
}
}
@@ -159,7 +161,10 @@ void updateListener() async {
for (var grade in subjectGrades) {
if (grade.valueType.name == "Szazalekos") {
grade.valueType = NameUidDesc(
uid: "1,Osztalyzat", name: "Osztalyzat", description: "");
uid: "1,Osztalyzat",
name: "Osztalyzat",
description: "",
);
if (grade.numericValue != null) {
grade.numericValue = percentageToGrade(grade.numericValue!);
}
@@ -169,29 +174,37 @@ void updateListener() async {
}
if (avg.isNaN) {
gradeCards.add(GestureDetector(
child: GradeSmallCard(grades!.response!, subject),
onTap: () {
activeSubjectUid = subject.uid;
subjectName = subject.name;
subjectId = subject.uid;
subjectCategory = subject.category.name!;
subjectInfo = subjects.where((s) => s.uid == subject.uid).toList();
widget.pageController(1);
},
));
gradeCards.add(
GestureDetector(
child: GradeSmallCard(grades!.response!, subject),
onTap: () {
activeSubjectUid = subject.uid;
subjectName = subject.name;
subjectId = subject.uid;
subjectCategory = subject.category.name!;
subjectInfo = subjects
.where((s) => s.uid == subject.uid)
.toList();
widget.pageController(1);
},
),
);
} else {
gradeCards.add(GestureDetector(
child: GradeSmallCard(grades!.response!, subject),
onTap: () {
activeSubjectUid = subject.uid;
subjectName = subject.name;
subjectId = subject.uid;
subjectCategory = subject.category.name!;
subjectInfo = subjects.where((s) => s.uid == subject.uid).toList();
widget.pageController(1);
},
));
gradeCards.add(
GestureDetector(
child: GradeSmallCard(grades!.response!, subject),
onTap: () {
activeSubjectUid = subject.uid;
subjectName = subject.name;
subjectId = subject.uid;
subjectCategory = subject.category.name!;
subjectInfo = subjects
.where((s) => s.uid == subject.uid)
.toList();
widget.pageController(1);
},
),
);
}
if (!avg.isNaN) {
@@ -212,11 +225,7 @@ void updateListener() async {
var subjectAvgColor = getGradeColor(subjectAvg);
return Padding(
padding: const EdgeInsets.only(
left: 20.0,
right: 20.0,
top: 12.0,
),
padding: const EdgeInsets.only(left: 20.0, right: 20.0, top: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -224,9 +233,10 @@ void updateListener() async {
children: [
Text(
widget.data.l10n.subjects,
style: appStyle.fonts.H_H2
.apply(color: appStyle.colors.textPrimary),
)
style: appStyle.fonts.H_H2.apply(
color: appStyle.colors.textPrimary,
),
),
],
),
// SizedBox(height: 16), // TODO: Add graphs here
@@ -237,24 +247,27 @@ void updateListener() async {
children: [
Text(
widget.data.l10n.your_subjects,
style: appStyle.fonts.H_14px
.apply(color: appStyle.colors.textSecondary),
style: appStyle.fonts.H_14px.apply(
color: appStyle.colors.textSecondary,
),
),
SizedBox(height: 16),
...gradeCards,
SizedBox(height: 16),
Text(
widget.data.l10n.data,
style: appStyle.fonts.B_16SB
.apply(color: appStyle.colors.textSecondary),
style: appStyle.fonts.B_16SB.apply(
color: appStyle.colors.textSecondary,
),
),
SizedBox(height: 16),
FirkaCard(
left: [
Text(
widget.data.l10n.subject_avg,
style: appStyle.fonts.B_16SB
.apply(color: appStyle.colors.textPrimary),
style: appStyle.fonts.B_16SB.apply(
color: appStyle.colors.textPrimary,
),
),
],
right: [
@@ -263,11 +276,16 @@ void updateListener() async {
color: subjectAvgColor.withAlpha(38),
child: Padding(
padding: EdgeInsets.only(
left: 8, right: 8, top: 4, bottom: 4),
left: 8,
right: 8,
top: 4,
bottom: 4,
),
child: Text(
subjectAvg.toStringAsFixed(2),
style: appStyle.fonts.B_16SB
.apply(color: subjectAvgColor),
style: appStyle.fonts.B_16SB.apply(
color: subjectAvgColor,
),
),
),
),
@@ -277,8 +295,9 @@ void updateListener() async {
left: [
Text(
widget.data.l10n.subject_avg_rounded,
style: appStyle.fonts.B_16SB
.apply(color: appStyle.colors.textPrimary),
style: appStyle.fonts.B_16SB.apply(
color: appStyle.colors.textPrimary,
),
),
],
right: [
@@ -287,11 +306,16 @@ void updateListener() async {
color: subjectAvgColor.withAlpha(38),
child: Padding(
padding: EdgeInsets.only(
left: 8, right: 8, top: 4, bottom: 4),
left: 8,
right: 8,
top: 4,
bottom: 4,
),
child: Text(
subjectAvgRounded.toStringAsFixed(2),
style: appStyle.fonts.B_16SB
.apply(color: subjectAvgColor),
style: appStyle.fonts.B_16SB.apply(
color: subjectAvgColor,
),
),
),
),
@@ -301,8 +325,9 @@ void updateListener() async {
left: [
Text(
"Összesített átlag",
style: appStyle.fonts.B_16SB
.apply(color: appStyle.colors.textPrimary),
style: appStyle.fonts.B_16SB.apply(
color: appStyle.colors.textPrimary,
),
),
],
right: [
@@ -311,40 +336,52 @@ void updateListener() async {
color: subjectAvgColor.withAlpha(38),
child: Padding(
padding: EdgeInsets.only(
left: 8, right: 8, top: 4, bottom: 4),
left: 8,
right: 8,
top: 4,
bottom: 4,
),
child: Text(
summaryAvg2.toStringAsFixed(2),
style: appStyle.fonts.B_16SB
.apply(color: subjectAvgColor),
style: appStyle.fonts.B_16SB.apply(
color: subjectAvgColor,
),
),
),
),
],
),
FirkaCard(left: [
Text(
widget.data.l10n.class_avg,
style: appStyle.fonts.B_16SB
.apply(color: appStyle.colors.textPrimary),
),
]),
FirkaCard(
left: [
Text(
widget.data.l10n.class_avg,
style: appStyle.fonts.B_16SB.apply(
color: appStyle.colors.textPrimary,
),
),
],
),
FirkaCard(
left: [
Text(
widget.data.l10n.class_n,
style: appStyle.fonts.B_16SB
.apply(color: appStyle.colors.textPrimary),
style: appStyle.fonts.B_16SB.apply(
color: appStyle.colors.textPrimary,
),
),
],
right: [
Text(
week!.response!
.where((lesson) =>
lesson.type.name != TimetableConsts.event)
.where(
(lesson) =>
lesson.type.name != TimetableConsts.event,
)
.length
.toString(),
style: appStyle.fonts.B_16SB
.apply(color: appStyle.colors.textPrimary),
style: appStyle.fonts.B_16SB.apply(
color: appStyle.colors.textPrimary,
),
),
],
),

View File

@@ -22,8 +22,12 @@ class HomeGradesSubjectScreen extends StatefulWidget {
final void Function(int) pageController;
const HomeGradesSubjectScreen(
this.data, this.updateNotifier, this.finishNotifier, this.pageController,
{super.key});
this.data,
this.updateNotifier,
this.finishNotifier,
this.pageController, {
super.key,
});
@override
State<StatefulWidget> createState() => _HomeGradesSubjectScreen();
@@ -41,8 +45,7 @@ class _HomeGradesSubjectScreen extends FirkaState<HomeGradesSubjectScreen> {
}
void updateListener() async {
grades = (await widget.data.client.getGrades(forceCache: false))
.response!
grades = (await widget.data.client.getGrades(forceCache: false)).response!
.where((grade) => grade.subject.uid == activeSubjectUid)
.where((grade) => grade.type.name != "felevi_jegy_ertekeles");
@@ -58,8 +61,7 @@ class _HomeGradesSubjectScreen extends FirkaState<HomeGradesSubjectScreen> {
widget.updateNotifier.addListener(updateListener);
(() async {
grades = (await widget.data.client.getGrades())
.response!
grades = (await widget.data.client.getGrades()).response!
.where((grade) => grade.subject.uid == activeSubjectUid)
.where((grade) => grade.type.name != "felevi_jegy_ertekeles");
@@ -82,64 +84,66 @@ class _HomeGradesSubjectScreen extends FirkaState<HomeGradesSubjectScreen> {
var gradeWidgets = List<Widget>.empty(growable: true);
for (var group in groups.entries) {
gradeWidgets.add(SizedBox(
height: 8,
));
gradeWidgets.add(Text(
group.key.format(widget.data.l10n, FormatMode.grades),
style:
appStyle.fonts.H_14px.apply(color: appStyle.colors.textPrimary),
));
gradeWidgets.add(SizedBox(
height: 8,
));
gradeWidgets.add(SizedBox(height: 8));
gradeWidgets.add(
Text(
group.key.format(widget.data.l10n, FormatMode.grades),
style: appStyle.fonts.H_14px.apply(
color: appStyle.colors.textPrimary,
),
),
);
gradeWidgets.add(SizedBox(height: 8));
for (var grade in group.value) {
gradeWidgets.add(GestureDetector(
child: FirkaCard(
left: [
Row(
children: [
GradeWidget(grade),
SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: MediaQuery.of(context).size.width / 1.45,
child: Text(
gradeWidgets.add(
GestureDetector(
child: FirkaCard(
left: [
Row(
children: [
GradeWidget(grade),
SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: MediaQuery.of(context).size.width / 1.45,
child: Text(
(grade.topic ?? grade.type.description!)
.firstUpper(),
style: appStyle.fonts.B_16SB
.apply(color: appStyle.colors.textPrimary)),
),
grade.mode?.description != null
? SizedBox(
width: MediaQuery.of(context).size.width / 1.45,
child: Text(
grade.mode!.description!.firstUpper(),
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary),
),
)
: SizedBox(),
],
)
],
)
],
style: appStyle.fonts.B_16SB.apply(
color: appStyle.colors.textPrimary,
),
),
),
grade.mode?.description != null
? SizedBox(
width:
MediaQuery.of(context).size.width / 1.45,
child: Text(
grade.mode!.description!.firstUpper(),
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
),
)
: SizedBox(),
],
),
],
),
],
),
onTap: () {
showGradeBottomSheet(context, widget.data, grade);
},
),
onTap: () {
showGradeBottomSheet(context, widget.data, grade);
},
));
);
}
}
return Padding(
padding: const EdgeInsets.only(
left: 16.0,
right: 16.0,
),
padding: const EdgeInsets.only(left: 16.0, right: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -149,52 +153,54 @@ class _HomeGradesSubjectScreen extends FirkaState<HomeGradesSubjectScreen> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Transform.translate(
offset: const Offset(-4, 0),
child: GestureDetector(
child: FirkaIconWidget(
FirkaIconType.majesticons, Majesticon.chevronLeftLine,
color: appStyle.colors.textSecondary
Row(
children: [
Transform.translate(
offset: const Offset(-4, 0),
child: GestureDetector(
child: FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.chevronLeftLine,
color: appStyle.colors.textSecondary,
),
onTap: () {
widget.pageController(0);
},
),
onTap: () {
widget.pageController(0);
}
),
),
Transform.translate(
offset: const Offset(-4, 0),
child: Text(
widget.data.l10n.subjects,
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textPrimary),
Transform.translate(
offset: const Offset(-4, 0),
child: Text(
widget.data.l10n.subjects,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
),
),
),
],
),
GestureDetector(
child: Card(
color: appStyle.colors.buttonSecondaryFill,
child: Padding(
padding: const EdgeInsets.all(4),
child: FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.menuSolid,
size: 26.0,
color: appStyle.colors.accent,
),
),
],
),
GestureDetector(
child: Card(
color: appStyle.colors.buttonSecondaryFill,
child: Padding(
padding: const EdgeInsets.all(4),
child: FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.menuSolid,
size: 26.0,
color: appStyle.colors.accent,
),
),
),
onTap: () {
// Navigator.push(context, Settings)
// showSubjectBottomSheetSettings(
// context,
// widget.data,
// aGrade.subject,
// );
},
),
onTap: () {
// Navigator.push(context, Settings)
// showSubjectBottomSheetSettings(
// context,
// widget.data,
// aGrade.subject,
// );
},
)
],
),
],
@@ -202,7 +208,8 @@ class _HomeGradesSubjectScreen extends FirkaState<HomeGradesSubjectScreen> {
// SizedBox(height: 16),
// GradeChart(grades: grades?.toList() ?? []),
SizedBox(
height: MediaQuery.of(context).size.height -
height:
MediaQuery.of(context).size.height -
MediaQuery.of(context).padding.top -
230,
child: ListView(
@@ -214,7 +221,8 @@ class _HomeGradesSubjectScreen extends FirkaState<HomeGradesSubjectScreen> {
shadowColor: const Color.fromRGBO(0, 0, 0, 0),
color: appStyle.colors.a15p,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16)),
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: EdgeInsetsGeometry.all(6),
child: ClassIconWidget(
@@ -228,24 +236,27 @@ class _HomeGradesSubjectScreen extends FirkaState<HomeGradesSubjectScreen> {
SizedBox(height: 8),
Text(
aGrade.subject.name,
style: appStyle.fonts.H_H2
.apply(color: appStyle.colors.textPrimary),
style: appStyle.fonts.H_H2.apply(
color: appStyle.colors.textPrimary,
),
),
SizedBox(height: 2),
Text(
aGrade.teacher,
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textSecondary),
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
),
SizedBox(height: 15)
]),
SizedBox(height: 15),
],
),
Padding(
padding: EdgeInsets.only(left: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: gradeWidgets,
),
)
),
],
),
),
@@ -254,10 +265,7 @@ class _HomeGradesSubjectScreen extends FirkaState<HomeGradesSubjectScreen> {
);
} else {
return Padding(
padding: const EdgeInsets.only(
left: 16.0,
right: 16.0,
),
padding: const EdgeInsets.only(left: 16.0, right: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -269,8 +277,10 @@ class _HomeGradesSubjectScreen extends FirkaState<HomeGradesSubjectScreen> {
offset: const Offset(-4, 0),
child: GestureDetector(
child: FirkaIconWidget(
FirkaIconType.majesticons, Majesticon.chevronLeftLine,
color: appStyle.colors.textSecondary),
FirkaIconType.majesticons,
Majesticon.chevronLeftLine,
color: appStyle.colors.textSecondary,
),
onTap: () {
widget.pageController(0);
},
@@ -280,17 +290,19 @@ class _HomeGradesSubjectScreen extends FirkaState<HomeGradesSubjectScreen> {
offset: const Offset(-4, 1),
child: Text(
widget.data.l10n.subjects,
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textPrimary),
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
),
)
),
],
),
],
),
SizedBox(height: 16),
SizedBox(
height: MediaQuery.of(context).size.height -
height:
MediaQuery.of(context).size.height -
MediaQuery.of(context).padding.top -
230,
child: ListView(
@@ -302,7 +314,8 @@ class _HomeGradesSubjectScreen extends FirkaState<HomeGradesSubjectScreen> {
shadowColor: const Color.fromRGBO(0, 0, 0, 0),
color: appStyle.colors.a15p,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16)),
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: EdgeInsetsGeometry.all(6),
child: ClassIconWidget(
@@ -316,28 +329,40 @@ class _HomeGradesSubjectScreen extends FirkaState<HomeGradesSubjectScreen> {
SizedBox(height: 8),
Text(
subjectName,
style: appStyle.fonts.H_H2
.apply(color: appStyle.colors.textPrimary),
style: appStyle.fonts.H_H2.apply(
color: appStyle.colors.textPrimary,
),
),
SizedBox(height: 2),
Text(
widget.data.l10n.unknown_teacher,
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textSecondary),
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
),
]),
],
),
SizedBox(
height: MediaQuery.of(context).size.height - MediaQuery.of(context).padding.top - 320,
height:
MediaQuery.of(context).size.height -
MediaQuery.of(context).padding.top -
320,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset("assets/images/logos/dave.svg",
width: 48, height: 48),
SizedBox(height: 12),
Text(widget.data.l10n.no_grades,
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textSecondary)),
SvgPicture.asset(
"assets/images/logos/dave.svg",
width: 48,
height: 48,
),
SizedBox(height: 12),
Text(
widget.data.l10n.no_grades,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
),
],
),
),

View File

@@ -34,8 +34,12 @@ class HomeMainScreen extends StatefulWidget {
final UpdateNotifier updateNotifier;
final UpdateNotifier finishNotifier;
const HomeMainScreen(this.data, this.updateNotifier, this.finishNotifier,
{super.key});
const HomeMainScreen(
this.data,
this.updateNotifier,
this.finishNotifier, {
super.key,
});
@override
State<HomeMainScreen> createState() => _HomeMainScreen();
@@ -81,21 +85,23 @@ class _HomeMainScreen extends FirkaState<HomeMainScreen> {
widget.data.client
.getTimeTableStream(
midnight, midnight.add(Duration(hours: 23, minutes: 59)),
cacheOnly: cacheOnly)
midnight,
midnight.add(Duration(hours: 23, minutes: 59)),
cacheOnly: cacheOnly,
)
.forEach((lessons) {
lessonsFetched++;
lessonsFetched++;
if (mounted) {
setState(() {
this.lessons = lessons.response;
if (mounted) {
setState(() {
this.lessons = lessons.response;
});
}
});
}
});
widget.data.client
.getNoticeBoardStream(cacheOnly: cacheOnly)
.forEach((items) {
widget.data.client.getNoticeBoardStream(cacheOnly: cacheOnly).forEach((
items,
) {
noticeBoardFetched++;
if (mounted) {
@@ -105,9 +111,9 @@ class _HomeMainScreen extends FirkaState<HomeMainScreen> {
}
});
widget.data.client
.getInfoBoardStream(cacheOnly: cacheOnly)
.forEach((items) {
widget.data.client.getInfoBoardStream(cacheOnly: cacheOnly).forEach((
items,
) {
infoBoardFetched++;
if (mounted) {
@@ -117,9 +123,9 @@ class _HomeMainScreen extends FirkaState<HomeMainScreen> {
}
});
widget.data.client
.getStudentStream(cacheOnly: cacheOnly)
.forEach((student) {
widget.data.client.getStudentStream(cacheOnly: cacheOnly).forEach((
student,
) {
studentFetched++;
if (mounted) {
@@ -149,9 +155,9 @@ class _HomeMainScreen extends FirkaState<HomeMainScreen> {
}
});
widget.data.client
.getHomeworkStream(cacheOnly: cacheOnly)
.forEach((homework) {
widget.data.client.getHomeworkStream(cacheOnly: cacheOnly).forEach((
homework,
) {
homeworkFetched++;
if (mounted) {
@@ -222,7 +228,8 @@ class _HomeMainScreen extends FirkaState<HomeMainScreen> {
welcomeWidget = StartingSoonWidget(widget.data.l10n, now, lessons!);
} else {
var currentLesson = lessons!.firstWhereOrNull(
(lesson) => now.isAfter(lesson.start) && now.isBefore(lesson.end));
(lesson) => now.isAfter(lesson.start) && now.isBefore(lesson.end),
);
// "fun" fact if your clock was exactly when the class ends then isBefore
// and isAfter would fail, so to work around that we just add 1ms to the
// current time
@@ -235,26 +242,43 @@ class _HomeMainScreen extends FirkaState<HomeMainScreen> {
lessonActive = true;
}
welcomeWidget = LessonBigWidget(widget.data.l10n, now, lessonIndex,
currentLesson, prevLesson, nextLesson, lessons!, tests!);
welcomeWidget = LessonBigWidget(
widget.data.l10n,
now,
lessonIndex,
currentLesson,
prevLesson,
nextLesson,
lessons!,
tests!,
);
}
}
if (lessons != null && lessons!.isNotEmpty) {
var nextLesson = lessons!.getNextLesson(now);
if (nextLesson != null) {
nextClass =
LessonSmallWidget(widget.data.l10n, nextLesson, lessonActive);
nextClass = LessonSmallWidget(
widget.data.l10n,
nextLesson,
lessonActive,
);
if (tests != null) {
final testsOnDate = tests!
.where((test) =>
test.date.isAfter(nextLesson.start
.getMidnight()
.subtract(Duration(seconds: 1))) &&
test.date.isBefore(nextLesson.end
.getMidnight()
.add(Duration(hours: 23, minutes: 59))) &&
test.subject.uid == nextLesson.subject?.uid)
.where(
(test) =>
test.date.isAfter(
nextLesson.start.getMidnight().subtract(
Duration(seconds: 1),
),
) &&
test.date.isBefore(
nextLesson.end.getMidnight().add(
Duration(hours: 23, minutes: 59),
),
) &&
test.subject.uid == nextLesson.subject?.uid,
)
.toList();
if (testsOnDate.isNotEmpty) {
@@ -268,14 +292,20 @@ class _HomeMainScreen extends FirkaState<HomeMainScreen> {
color: appStyle.colors.accent,
),
SizedBox(width: 6),
Text(test.theme,
style: appStyle.fonts.B_16SB
.apply(color: appStyle.colors.textSecondary))
Text(
test.theme,
style: appStyle.fonts.B_16SB.apply(
color: appStyle.colors.textSecondary,
),
),
],
right: [
Text(test.method.description ?? "N/A",
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textTertiary))
Text(
test.method.description ?? "N/A",
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textTertiary,
),
),
],
);
}
@@ -296,11 +326,14 @@ class _HomeMainScreen extends FirkaState<HomeMainScreen> {
GestureDetector(
child: InfoBoardItemWidget(item),
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => MessageScreen(widget.data, item)));
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => MessageScreen(widget.data, item),
),
);
},
),
item.date
item.date,
));
}
@@ -319,10 +352,12 @@ class _HomeMainScreen extends FirkaState<HomeMainScreen> {
SizedBox(
width: MediaQuery.of(context).size.width / 1.45,
child: Text(
(grade.topic ?? grade.type.description!)
.firstUpper(),
style: appStyle.fonts.B_16SB
.apply(color: appStyle.colors.textPrimary)),
(grade.topic ?? grade.type.description!)
.firstUpper(),
style: appStyle.fonts.B_16SB.apply(
color: appStyle.colors.textPrimary,
),
),
),
grade.mode?.description != null
? SizedBox(
@@ -330,38 +365,38 @@ class _HomeMainScreen extends FirkaState<HomeMainScreen> {
child: Text(
grade.mode!.description!.firstUpper(),
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary),
color: appStyle.colors.textSecondary,
),
),
)
: SizedBox(),
],
)
),
],
)
),
],
),
onTap: () {
showGradeBottomSheet(context, widget.data, grade);
},
),
grade.recordDate
grade.recordDate,
));
}
for (final entry in homework!) {
noticeBoardWidgets
.add((HomeworkWidget(widget.data, entry), entry.creationDate));
noticeBoardWidgets.add((
HomeworkWidget(widget.data, entry),
entry.creationDate,
));
}
noticeBoardWidgets
.sort((item1, item2) => item2.$2.difference(item1.$2).inMilliseconds);
noticeBoardWidgets.sort(
(item1, item2) => item2.$2.difference(item1.$2).inMilliseconds,
);
return Padding(
padding: const EdgeInsets.only(
left: 20.0,
top: 24.0,
right: 20.0,
),
padding: const EdgeInsets.only(left: 20.0, top: 24.0, right: 20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
@@ -377,7 +412,7 @@ class _HomeMainScreen extends FirkaState<HomeMainScreen> {
child: ListView(
children: noticeBoardWidgets.map((e) => e.$1).toList(),
),
)
),
],
),
);
@@ -390,7 +425,7 @@ class _HomeMainScreen extends FirkaState<HomeMainScreen> {
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [DelayedSpinnerWidget()],
)
),
],
),
);

View File

@@ -11,15 +11,19 @@ class PageWithSubPages extends StatefulWidget {
final ValueNotifier<bool> subPageActive;
final UpdateNotifier back;
const PageWithSubPages(this.subPages, this.subPageActive, this.back,
{Key? key, required this.pageIndex})
: super(key: key);
const PageWithSubPages(
this.subPages,
this.subPageActive,
this.back, {
super.key,
required this.pageIndex,
});
@override
_PageWithSubPagesState createState() => _PageWithSubPagesState();
PageWithSubPagesState createState() => PageWithSubPagesState();
}
class _PageWithSubPagesState extends FirkaState<PageWithSubPages> {
class PageWithSubPagesState extends FirkaState<PageWithSubPages> {
int _currentSubPage = 0;
@override

View File

@@ -32,8 +32,12 @@ class HomeTimetableScreen extends StatefulWidget {
final void Function(int) pageController;
const HomeTimetableScreen(
this.data, this.updateNotifier, this.finishNotifier, this.pageController,
{super.key});
this.data,
this.updateNotifier,
this.finishNotifier,
this.pageController, {
super.key,
});
@override
State<HomeTimetableScreen> createState() => _HomeTimetableScreen();
@@ -81,19 +85,24 @@ class _HomeTimetableScreen extends FirkaState<HomeTimetableScreen>
void setActiveToToday() {
final todayMid = now!.getMidnight();
int idx = dates!.indexWhere((d) =>
d.year == todayMid.year &&
d.month == todayMid.month &&
d.day == todayMid.day);
int idx = dates!.indexWhere(
(d) =>
d.year == todayMid.year &&
d.month == todayMid.month &&
d.day == todayMid.day,
);
if (idx >= 0) {
final todaysLessons = lessons?.where((lesson) =>
lesson.start.isAfter(todayMid) &&
lesson.end.isBefore(todayMid.add(Duration(hours: 23, minutes: 59))));
final todaysLessons = lessons?.where(
(lesson) =>
lesson.start.isAfter(todayMid) &&
lesson.end.isBefore(todayMid.add(Duration(hours: 23, minutes: 59))),
);
if (todaysLessons != null && todaysLessons.isNotEmpty) {
final lastLessonToday =
todaysLessons.reduce((a, b) => a.end.isAfter(b.end) ? a : b);
final lastLessonToday = todaysLessons.reduce(
(a, b) => a.end.isAfter(b.end) ? a : b,
);
if (now!.isAfter(lastLessonToday.end)) {
int nextIdx = idx + 1;
@@ -146,8 +155,11 @@ class _HomeTimetableScreen extends FirkaState<HomeTimetableScreen>
await widget.data.client.getTests();
}
Future<void> _updateState(DateTime now, ApiResponse<List<Lesson>> lessonsResp,
ApiResponse<List<Test>> testsResp) async {
Future<void> _updateState(
DateTime now,
ApiResponse<List<Lesson>> lessonsResp,
ApiResponse<List<Test>> testsResp,
) async {
var monday = now.getMonday().getMidnight();
List<DateTime> dates = List.empty(growable: true);
@@ -164,7 +176,8 @@ class _HomeTimetableScreen extends FirkaState<HomeTimetableScreen>
for (var i = 0; i < 7; i++) {
var t = monday.add(Duration(days: i));
var hasLessons = i < 5 ||
var hasLessons =
i < 5 ||
lessons!.firstWhereOrNull((lesson) {
return lesson.start.getMidnight().millisecondsSinceEpoch ==
t.getMidnight().millisecondsSinceEpoch;
@@ -204,9 +217,9 @@ class _HomeTimetableScreen extends FirkaState<HomeTimetableScreen>
widget.data.client
.getTimeTableStream(monday, sunday, cacheOnly: forceCache)
.forEach((lessons) {
lessonsResp = lessons;
lessonsFetched++;
});
lessonsResp = lessons;
lessonsFetched++;
});
widget.data.client.getTestsStream(cacheOnly: forceCache).forEach((tests) {
testsResp = tests;
@@ -278,8 +291,9 @@ class _HomeTimetableScreen extends FirkaState<HomeTimetableScreen>
// If the target is not adjacent, create temporary order
if ((targetIndex - oldIndex).abs() > 1) {
// Determine the temporary target position next to the current position
int tempTargetIndex =
oldIndex < targetIndex ? oldIndex + 1 : oldIndex - 1;
int tempTargetIndex = oldIndex < targetIndex
? oldIndex + 1
: oldIndex - 1;
// Create a new order where target day is next to current day
List<DateTime> reorderedDates = List.from(_animationDates!);
@@ -307,13 +321,13 @@ class _HomeTimetableScreen extends FirkaState<HomeTimetableScreen>
final double end = targetDisplayIndex * totalCardWidth;
_cardAnimationController!.reset();
_cardOffsetAnimation = Tween<Offset>(
begin: Offset(start, 0),
end: Offset(end, 0),
).animate(CurvedAnimation(
parent: _cardAnimationController!,
curve: Curves.easeInOut,
));
_cardOffsetAnimation =
Tween<Offset>(begin: Offset(start, 0), end: Offset(end, 0)).animate(
CurvedAnimation(
parent: _cardAnimationController!,
curve: Curves.easeInOut,
),
);
setState(() {
_showAnimatedCard = true;
@@ -371,27 +385,45 @@ class _HomeTimetableScreen extends FirkaState<HomeTimetableScreen>
final realIndex = i; // Always use real index for nav icons
final testsOnDate = tests!
.where((test) =>
test.date.isAfter(date.subtract(Duration(seconds: 1))) &&
test.date.isBefore(date.add(Duration(hours: 23, minutes: 59))))
.where(
(test) =>
test.date.isAfter(date.subtract(Duration(seconds: 1))) &&
test.date.isBefore(
date.add(Duration(hours: 23, minutes: 59)),
),
)
.toList();
if (testsOnDate.isNotEmpty) {
ttWidgets.add(Stack(
children: [
BottomTimeTableNavIconWidget(widget.data.l10n, () {
_handleNavTap(active, realIndex);
}, active == i, date),
Transform.translate(
offset: Offset(38, -10),
child: BubbleTest(),
),
],
));
ttWidgets.add(
Stack(
children: [
BottomTimeTableNavIconWidget(
widget.data.l10n,
() {
_handleNavTap(active, realIndex);
},
active == i,
date,
),
Transform.translate(
offset: Offset(38, -10),
child: BubbleTest(),
),
],
),
);
} else {
ttWidgets.add(BottomTimeTableNavIconWidget(widget.data.l10n, () {
_handleNavTap(active, realIndex);
}, active == i, date));
ttWidgets.add(
BottomTimeTableNavIconWidget(
widget.data.l10n,
() {
_handleNavTap(active, realIndex);
},
active == i,
date,
),
);
}
}
@@ -400,28 +432,49 @@ class _HomeTimetableScreen extends FirkaState<HomeTimetableScreen>
final date = _animationDates![i];
final lessonsOnDate = lessons!
.where((lesson) =>
lesson.start.isAfter(date) &&
lesson.end.isBefore(date.add(Duration(hours: 24))))
.where(
(lesson) =>
lesson.start.isAfter(date) &&
lesson.end.isBefore(date.add(Duration(hours: 24))),
)
.toList();
final lessonsOnDay = lessons!
.where((lesson) =>
lesson.start.getMidnight().millisecondsSinceEpoch ==
date.getMidnight().millisecondsSinceEpoch)
.where(
(lesson) =>
lesson.start.getMidnight().millisecondsSinceEpoch ==
date.getMidnight().millisecondsSinceEpoch,
)
.toList();
final eventsOnDate = events!
.where((lesson) =>
lesson.start.isAfter(date.subtract(Duration(seconds: 1))) &&
lesson.end.isBefore(date.add(Duration(hours: 23, minutes: 59))))
.where(
(lesson) =>
lesson.start.isAfter(date.subtract(Duration(seconds: 1))) &&
lesson.end.isBefore(
date.add(Duration(hours: 23, minutes: 59)),
),
)
.toList();
final testsOnDate = tests!
.where((test) =>
test.date.isAfter(date.subtract(Duration(seconds: 1))) &&
test.date.isBefore(date.add(Duration(hours: 23, minutes: 59))))
.where(
(test) =>
test.date.isAfter(date.subtract(Duration(seconds: 1))) &&
test.date.isBefore(
date.add(Duration(hours: 23, minutes: 59)),
),
)
.toList();
ttDays.add(TimeTableDayWidget(widget.data, date, lessons!,
lessonsOnDate, eventsOnDate, testsOnDate, lessonsOnDay));
ttDays.add(
TimeTableDayWidget(
widget.data,
date,
lessons!,
lessonsOnDate,
eventsOnDate,
testsOnDate,
lessonsOnDay,
),
);
}
List<Widget> ttEmptyCards = List.empty(growable: true);
@@ -436,70 +489,76 @@ class _HomeTimetableScreen extends FirkaState<HomeTimetableScreen>
);
if (_showAnimatedCard && _cardOffsetAnimation != null) {
ttEmptyCards.add(AnimatedBuilder(
animation: _cardOffsetAnimation!,
builder: (context, child) {
return Transform.translate(
offset: _cardOffsetAnimation!.value,
child: cardWidget,
);
},
));
ttEmptyCards.add(
AnimatedBuilder(
animation: _cardOffsetAnimation!,
builder: (context, child) {
return Transform.translate(
offset: _cardOffsetAnimation!.value,
child: cardWidget,
);
},
),
);
} else {
ttEmptyCards.add(Transform.translate(
offset: Offset(0, 0),
child: cardWidget,
));
ttEmptyCards.add(
Transform.translate(offset: Offset(0, 0), child: cardWidget),
);
}
} else {
ttEmptyCards.add(Card(
color: Colors.transparent,
shadowColor: Colors.transparent,
child: SizedBox(width: 40, height: 54),
));
ttEmptyCards.add(
Card(
color: Colors.transparent,
shadowColor: Colors.transparent,
child: SizedBox(width: 40, height: 54),
),
);
}
}
} else {
ttEmptyCards.clear();
}
return Stack(children: [
Column(children: [
SizedBox(
width: MediaQuery.of(context).size.width,
height: 74 + 16,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
return Stack(
children: [
Column(
children: [
SizedBox(
width: MediaQuery.of(context).size.width,
height: 74 + 16,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
Text(
widget.data.l10n.timetable,
style: appStyle.fonts.H_H2
.apply(color: appStyle.colors.textPrimary),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
child: Card(
color: appStyle.colors.buttonSecondaryFill,
child: Padding(
padding: const EdgeInsets.all(8),
child: FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.tableSolid,
size: 26.0,
color: appStyle.colors.accent,
),
),
Text(
widget.data.l10n.timetable,
style: appStyle.fonts.H_H2.apply(
color: appStyle.colors.textPrimary,
),
onTap: () {
widget.pageController(1);
},
),
/* TODO: 1.1.0
Row(
children: [
GestureDetector(
child: Card(
color: appStyle.colors.buttonSecondaryFill,
child: Padding(
padding: const EdgeInsets.all(8),
child: FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.tableSolid,
size: 26.0,
color: appStyle.colors.accent,
),
),
),
onTap: () {
widget.pageController(1);
},
),
/* TODO: 1.1.0
Card(
color: appStyle.colors.buttonSecondaryFill,
@@ -514,179 +573,195 @@ class _HomeTimetableScreen extends FirkaState<HomeTimetableScreen>
),
),
*/
GestureDetector(
child: Card(
color: appStyle.colors.buttonSecondaryFill,
child: Padding(
padding: const EdgeInsets.all(8),
child: FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.settingsCogSolid,
size: 26.0,
color: appStyle.colors.accent,
GestureDetector(
child: Card(
color: appStyle.colors.buttonSecondaryFill,
child: Padding(
padding: const EdgeInsets.all(8),
child: FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.settingsCogSolid,
size: 26.0,
color: appStyle.colors.accent,
),
),
),
onTap: () {
showSettingsSheet(
context,
MediaQuery.of(context).size.height * 0.4,
widget.data,
widget.data.settings
.group("settings")
.subGroup("timetable_toast"),
);
},
),
],
),
],
),
SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
behavior: HitTestBehavior.translucent,
child: SizedBox(
width: 24,
height: 24,
child: FirkaIconWidget(
FirkaIconType.icons,
"dropdownLeft",
size: 24,
color: appStyle.colors.accent,
),
),
onTap: () {
showSettingsSheet(
context,
MediaQuery.of(context).size.height * 0.4,
widget.data,
widget.data.settings
.group("settings")
.subGroup("timetable_toast"));
onTap: () async {
var newNow = now!.subtract(Duration(days: 7));
if (!mounted) return;
setState(() {
now = newNow;
lessons = null;
dates = null;
});
await initForWeek(newNow);
setState(() {
now = newNow;
});
},
)
),
GestureDetector(
child: Row(
children: [
Text(
now!.format(
widget.data.l10n,
FormatMode.yyyymmddwedd,
),
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
),
SizedBox(width: 4),
if (showABTimetable) ...[
Text(
"",
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.accent,
),
),
SizedBox(width: 4),
Text(
now!.isAWeek()
? widget.data.l10n.a_week
: widget.data.l10n.b_week,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
),
],
],
),
onTap: () {
now = timeNow();
setActiveToToday();
_controller.jumpToPage(active);
},
),
GestureDetector(
behavior: HitTestBehavior.translucent,
child: SizedBox(
width: 24,
height: 24,
child: FirkaIconWidget(
FirkaIconType.icons,
"dropdownRight",
size: 24,
color: appStyle.colors.accent,
),
),
onTap: () async {
var newNow = now!.add(Duration(days: 7));
if (!mounted) return;
setState(() {
now = newNow;
lessons = null;
dates = null;
});
await initForWeek(newNow);
setState(() {
now = newNow;
});
},
),
],
),
],
),
SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
behavior: HitTestBehavior.translucent,
child: SizedBox(
width: 24,
height: 24,
child: FirkaIconWidget(
FirkaIconType.icons,
"dropdownLeft",
size: 24,
color: appStyle.colors.accent,
),
),
onTap: () async {
var newNow = now!.subtract(Duration(days: 7));
if (!mounted) return;
setState(() {
now = newNow;
lessons = null;
dates = null;
});
await initForWeek(newNow);
setState(() {
now = newNow;
});
},
),
GestureDetector(
child: Row(
children: [
Text(
now!.format(
widget.data.l10n, FormatMode.yyyymmddwedd),
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textPrimary)),
SizedBox(width: 4),
if (showABTimetable) ...[
Text("",
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.accent)),
SizedBox(width: 4),
Text(
now!.isAWeek()
? widget.data.l10n.a_week
: widget.data.l10n.b_week,
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textPrimary)),
]
],
),
onTap: () {
now = timeNow();
setActiveToToday();
_controller.jumpToPage(active);
},
),
GestureDetector(
behavior: HitTestBehavior.translucent,
child: SizedBox(
width: 24,
height: 24,
child: FirkaIconWidget(
FirkaIconType.icons,
"dropdownRight",
size: 24,
color: appStyle.colors.accent,
),
),
onTap: () async {
var newNow = now!.add(Duration(days: 7));
if (!mounted) return;
setState(() {
now = newNow;
lessons = null;
dates = null;
});
await initForWeek(newNow);
setState(() {
now = newNow;
});
},
),
],
)
],
),
),
),
]),
Column(mainAxisAlignment: MainAxisAlignment.end, children: [
Expanded(
child: TransparentPointer(
child: CarouselSlider(
items: ttDays,
carouselController: _controller,
options: CarouselOptions(
height: MediaQuery.of(context).size.height,
viewportFraction: 1,
enableInfiniteScroll: false,
initialPage: active,
onPageChanged: (i, _) {
if (animating || !mounted) return;
HapticFeedback.mediumImpact();
// Convert animation index to display index
final displayIndex = dates!.indexOf(_animationDates![i]);
maybeCacheNextWeek(displayIndex);
maybeCachePreviousWeek(displayIndex);
setState(() {
active = displayIndex;
});
}),
))),
Container(
padding: EdgeInsets.only(bottom: 12),
decoration: ShapeDecoration(
color: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(0),
),
shadows: [
BoxShadow(
),
],
),
Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Expanded(
child: TransparentPointer(
child: CarouselSlider(
items: ttDays,
carouselController: _controller,
options: CarouselOptions(
height: MediaQuery.of(context).size.height,
viewportFraction: 1,
enableInfiniteScroll: false,
initialPage: active,
onPageChanged: (i, _) {
if (animating || !mounted) return;
HapticFeedback.mediumImpact();
// Convert animation index to display index
final displayIndex = dates!.indexOf(
_animationDates![i],
);
maybeCacheNextWeek(displayIndex);
maybeCachePreviousWeek(displayIndex);
setState(() {
active = displayIndex;
});
},
),
),
),
),
Container(
padding: EdgeInsets.only(bottom: 12),
decoration: ShapeDecoration(
color: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(0),
),
shadows: [
BoxShadow(
color: appStyle.colors.background,
blurRadius: 30,
spreadRadius: 20,
offset: Offset(0, -25)),
],
offset: Offset(0, -25),
),
],
),
child: Stack(
children: [
Wrap(spacing: 10, children: ttEmptyCards),
Wrap(spacing: 10, children: ttWidgets),
],
),
),
child: Stack(
children: [
Wrap(
spacing: 10,
children: ttEmptyCards,
),
Wrap(
spacing: 10,
children: ttWidgets,
),
],
)),
])
]);
],
),
],
);
} else {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -694,7 +769,7 @@ class _HomeTimetableScreen extends FirkaState<HomeTimetableScreen>
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [DelayedSpinnerWidget()],
)
),
],
);
}

View File

@@ -25,8 +25,12 @@ class HomeTimetableMonthlyScreen extends StatefulWidget {
final void Function(int) pageController;
const HomeTimetableMonthlyScreen(
this.data, this.updateNotifier, this.finishNotifier, this.pageController,
{super.key});
this.data,
this.updateNotifier,
this.finishNotifier,
this.pageController, {
super.key,
});
@override
State<HomeTimetableMonthlyScreen> createState() =>
@@ -49,20 +53,28 @@ class _HomeTimetableMonthlyScreen
Future<void> initForMonth(DateTime now, {bool forceCache = true}) async {
final monthStart = DateTime.utc(now.year, now.month, 1);
final monthEnd =
DateTime.utc(now.year, now.month + 1).subtract(Duration(days: 1));
final monthEnd = DateTime.utc(
now.year,
now.month + 1,
).subtract(Duration(days: 1));
final start = monthStart.subtract(Duration(days: 7)).getMonday();
var end =
monthEnd.add(Duration(days: 7)).getMonday().add(Duration(days: 7));
var end = monthEnd
.add(Duration(days: 7))
.getMonday()
.add(Duration(days: 7));
var days = end.difference(start).inDays;
var lessonsResp = await widget.data.client
.getTimeTable(monthStart, monthEnd, forceCache: forceCache);
var lessonsResp = await widget.data.client.getTimeTable(
monthStart,
monthEnd,
forceCache: forceCache,
);
var testsResp = await widget.data.client.getTests(forceCache: forceCache);
var omissionsResp =
await widget.data.client.getOmissions(forceCache: forceCache);
var omissionsResp = await widget.data.client.getOmissions(
forceCache: forceCache,
);
List<DateTime> dates = List.empty(growable: true);
for (var i = 0; i < days; i++) {
@@ -127,8 +139,10 @@ class _HomeTimetableMonthlyScreen
final meow = dates![20];
final currentMonthStart = DateTime.utc(meow.year, meow.month, 1);
final currentMonthEnd =
DateTime.utc(meow.year, meow.month + 1).subtract(Duration(days: 1));
final currentMonthEnd = DateTime.utc(
meow.year,
meow.month + 1,
).subtract(Duration(days: 1));
// column-major -> row-major
for (var day = 0; day < 7; day++) {
@@ -136,56 +150,73 @@ class _HomeTimetableMonthlyScreen
final d = dates![week * 7 + day];
if (d.isBefore(currentMonthStart) || d.isAfter(currentMonthEnd)) {
ttDays.add(Column(
children: [
Container(
ttDays.add(
Column(
children: [
Container(
width: 40,
height: 40,
clipBehavior: Clip.antiAlias,
decoration: ShapeDecoration(
color: appStyle.colors.cardTranslucent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
)),
SizedBox(height: 4),
Text(d.format(widget.data.l10n, FormatMode.d),
borderRadius: BorderRadius.circular(12),
),
),
),
SizedBox(height: 4),
Text(
d.format(widget.data.l10n, FormatMode.d),
style: appStyle.fonts.B_16R.apply(
color: (d.weekday == DateTime.saturday ||
d.weekday == DateTime.sunday)
? appStyle.colors.errorText
: appStyle.colors.textTertiary)),
],
));
color:
(d.weekday == DateTime.saturday ||
d.weekday == DateTime.sunday)
? appStyle.colors.errorText
: appStyle.colors.textTertiary,
),
),
],
),
);
} else {
Widget body = SizedBox();
Color bodyBgColor = appStyle.colors.a15p;
var lessonsToday = lessons!.where((lesson) =>
lesson.start.isAfter(d.getMidnight()) &&
lesson.end.isBefore(
d.getMidnight().add(Duration(hours: 23, minutes: 59))));
var lessonsToday = lessons!.where(
(lesson) =>
lesson.start.isAfter(d.getMidnight()) &&
lesson.end.isBefore(
d.getMidnight().add(Duration(hours: 23, minutes: 59)),
),
);
var omissionType = lessonsToday.firstWhereOrNull((lesson) =>
lesson.studentPresence != null &&
lesson.studentPresence?.name != OmissionConsts.na &&
lesson.studentPresence?.name != OmissionConsts.present);
var omissionType = lessonsToday.firstWhereOrNull(
(lesson) =>
lesson.studentPresence != null &&
lesson.studentPresence?.name != OmissionConsts.na &&
lesson.studentPresence?.name != OmissionConsts.present,
);
switch (activeFilter) {
case ActiveFilter.lessonNo:
if (lessonsToday.isNotEmpty) {
body = Center(
child: Text(lessonsToday.length.toString(),
style: appStyle.fonts.H_16px.apply(
color: omissionType != null &&
(omissionType.studentPresence!.name ==
OmissionConsts.absence ||
omissionType.studentPresence!.name ==
OmissionConsts.na)
? appStyle.colors.errorText
: timeNow().day == d.day &&
timeNow().month == d.month
? appStyle.colors.accent
: appStyle.colors.secondary)),
child: Text(
lessonsToday.length.toString(),
style: appStyle.fonts.H_16px.apply(
color:
omissionType != null &&
(omissionType.studentPresence!.name ==
OmissionConsts.absence ||
omissionType.studentPresence!.name ==
OmissionConsts.na)
? appStyle.colors.errorText
: timeNow().day == d.day &&
timeNow().month == d.month
? appStyle.colors.accent
: appStyle.colors.secondary,
),
),
);
if (omissionType != null &&
@@ -198,13 +229,18 @@ class _HomeTimetableMonthlyScreen
}
break;
case ActiveFilter.tests:
if (lessonsToday.firstWhereOrNull((lesson) => tests!.any(
if (lessonsToday.firstWhereOrNull(
(lesson) => tests!.any(
(test) =>
test.lessonNumber == lesson.lessonNumber &&
lesson.start.isAfter(test.date.getMidnight()) &&
lesson.end.isBefore(test.date
.getMidnight()
.add(Duration(hours: 23, minutes: 59))))) !=
lesson.end.isBefore(
test.date.getMidnight().add(
Duration(hours: 23, minutes: 59),
),
),
),
) !=
null) {
body = Center(
child: FirkaIconWidget(
@@ -265,7 +301,8 @@ class _HomeTimetableMonthlyScreen
break;
default:
logger.fine(
"omission: ${omissionType.studentPresence!.name}");
"omission: ${omissionType.studentPresence!.name}",
);
body = Center(
child: FirkaIconWidget(
FirkaIconType.majesticons,
@@ -279,30 +316,37 @@ class _HomeTimetableMonthlyScreen
break;
}
ttDays.add(Column(
children: [
Container(
width: 40,
height: 40,
clipBehavior: Clip.antiAlias,
decoration: ShapeDecoration(
color: bodyBgColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
ttDays.add(
Column(
children: [
Container(
width: 40,
height: 40,
clipBehavior: Clip.antiAlias,
decoration: ShapeDecoration(
color: bodyBgColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: body,
),
child: body,
),
SizedBox(height: 4),
Text(d.format(widget.data.l10n, FormatMode.d),
SizedBox(height: 4),
Text(
d.format(widget.data.l10n, FormatMode.d),
style: appStyle.fonts.B_16R.apply(
color: (d.weekday == DateTime.saturday ||
d.weekday == DateTime.sunday) &&
lessonsToday.isEmpty
? appStyle.colors.errorText
: appStyle.colors.textSecondary)),
SizedBox(height: 12),
],
));
color:
(d.weekday == DateTime.saturday ||
d.weekday == DateTime.sunday) &&
lessonsToday.isEmpty
? appStyle.colors.errorText
: appStyle.colors.textSecondary,
),
),
SizedBox(height: 12),
],
),
);
if (timeNow().getMidnight().millisecondsSinceEpoch ==
d.toLocal().getMidnight().millisecondsSinceEpoch) {
@@ -313,8 +357,9 @@ class _HomeTimetableMonthlyScreen
}
return Scaffold(
backgroundColor: appStyle.colors.background,
body: Stack(children: [
backgroundColor: appStyle.colors.background,
body: Stack(
children: [
SizedBox(
width: MediaQuery.of(context).size.width,
height: 74 + 16,
@@ -327,8 +372,9 @@ class _HomeTimetableMonthlyScreen
children: [
Text(
widget.data.l10n.timetable,
style: appStyle.fonts.H_H2
.apply(color: appStyle.colors.textPrimary),
style: appStyle.fonts.H_H2.apply(
color: appStyle.colors.textPrimary,
),
),
Row(
children: [
@@ -377,12 +423,13 @@ class _HomeTimetableMonthlyScreen
),
onTap: () {
showSettingsSheet(
context,
MediaQuery.of(context).size.height * 0.4,
widget.data,
widget.data.settings
.group("settings")
.subGroup("timetable_toast"));
context,
MediaQuery.of(context).size.height * 0.4,
widget.data,
widget.data.settings
.group("settings")
.subGroup("timetable_toast"),
);
},
),
],
@@ -419,11 +466,13 @@ class _HomeTimetableMonthlyScreen
},
),
Text(
now!
.format(widget.data.l10n, FormatMode.yyyymmmm)
.toLowerCase(),
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textPrimary)),
now!
.format(widget.data.l10n, FormatMode.yyyymmmm)
.toLowerCase(),
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
),
GestureDetector(
child: FirkaIconWidget(
FirkaIconType.icons,
@@ -445,31 +494,30 @@ class _HomeTimetableMonthlyScreen
},
),
],
)
),
],
),
),
),
TransparentPointer(
child: Padding(
padding: const EdgeInsets.only(top: 74 + 16 + 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
StaggeredGrid.count(
crossAxisCount: 7,
mainAxisSpacing: 8,
children: ttDays,
)
],
child: Padding(
padding: const EdgeInsets.only(top: 74 + 16 + 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
StaggeredGrid.count(
crossAxisCount: 7,
mainAxisSpacing: 8,
children: ttDays,
),
],
),
),
)),
),
TransparentPointer(
child: Column(
children: [
SizedBox(
height: MediaQuery.of(context).size.height / 1.3,
),
SizedBox(height: MediaQuery.of(context).size.height / 1.3),
SizedBox(
width: MediaQuery.of(context).size.width,
child: Row(
@@ -479,70 +527,99 @@ class _HomeTimetableMonthlyScreen
Row(
children: [
_StatusToast(
FirkaIconWidget(FirkaIconType.majesticons,
Majesticon.clockSolid,
color: appStyle.colors.accent, size: 16),
lessons!
.where((lesson) =>
lesson.start
.isAfter(currentMonthStart) &&
lesson.end.isBefore(currentMonthEnd))
.length,
activeFilter == ActiveFilter.lessonNo, () {
setState(() {
activeFilter = ActiveFilter.lessonNo;
});
}),
FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.clockSolid,
color: appStyle.colors.accent,
size: 16,
),
lessons!
.where(
(lesson) =>
lesson.start.isAfter(
currentMonthStart,
) &&
lesson.end.isBefore(currentMonthEnd),
)
.length,
activeFilter == ActiveFilter.lessonNo,
() {
setState(() {
activeFilter = ActiveFilter.lessonNo;
});
},
),
_StatusToast(
FirkaIconWidget(FirkaIconType.majesticons,
Majesticon.editPen4Solid,
color: appStyle.colors.accent, size: 16),
lessons!
.where((lesson) => tests!.any((test) =>
test.lessonNumber ==
lesson.lessonNumber &&
lesson.start
.isAfter(test.date.getMidnight()) &&
lesson.end.isBefore(test.date
.getMidnight()
.add(Duration(
hours: 23, minutes: 59)))))
.length,
activeFilter == ActiveFilter.tests, () {
setState(() {
activeFilter = ActiveFilter.tests;
});
}),
FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.editPen4Solid,
color: appStyle.colors.accent,
size: 16,
),
lessons!
.where(
(lesson) => tests!.any(
(test) =>
test.lessonNumber ==
lesson.lessonNumber &&
lesson.start.isAfter(
test.date.getMidnight(),
) &&
lesson.end.isBefore(
test.date.getMidnight().add(
Duration(hours: 23, minutes: 59),
),
),
),
)
.length,
activeFilter == ActiveFilter.tests,
() {
setState(() {
activeFilter = ActiveFilter.tests;
});
},
),
_StatusToast(
FirkaIconWidget(FirkaIconType.majesticons,
Majesticon.timerLine,
color: appStyle.colors.accent, size: 16),
lessons!
.where((lesson) =>
lesson.start
.isAfter(currentMonthStart) &&
FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.timerLine,
color: appStyle.colors.accent,
size: 16,
),
lessons!
.where(
(lesson) =>
lesson.start.isAfter(
currentMonthStart,
) &&
lesson.end.isBefore(currentMonthEnd) &&
lesson.studentPresence != null &&
lesson.studentPresence?.name !=
OmissionConsts.na &&
lesson.studentPresence?.name !=
OmissionConsts.present)
.length,
activeFilter == ActiveFilter.omissions, () {
setState(() {
activeFilter = ActiveFilter.omissions;
});
}),
OmissionConsts.present,
)
.length,
activeFilter == ActiveFilter.omissions,
() {
setState(() {
activeFilter = ActiveFilter.omissions;
});
},
),
],
),
SizedBox()
SizedBox(),
],
),
)
),
],
),
),
]));
],
),
);
} else {
return Scaffold(
backgroundColor: appStyle.colors.background,
@@ -552,7 +629,7 @@ class _HomeTimetableMonthlyScreen
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [DelayedSpinnerWidget()],
)
),
],
),
);
@@ -578,8 +655,9 @@ class _StatusToast extends StatelessWidget {
color: _active
? appStyle.colors.buttonSecondaryFill
: appStyle.colors.cardTranslucent,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
@@ -587,9 +665,12 @@ class _StatusToast extends StatelessWidget {
children: [
_icon,
SizedBox(width: 6),
Text(_count.toString(),
style: appStyle.fonts.H_16px
.apply(color: appStyle.colors.textPrimary))
Text(
_count.toString(),
style: appStyle.fonts.H_16px.apply(
color: appStyle.colors.textPrimary,
),
),
],
),
),

View File

@@ -49,10 +49,7 @@ class _DebugScreen extends FirkaState<DebugScreen> {
}
return Scaffold(
appBar: AppBar(
title: const Text('Debug'),
centerTitle: true,
),
appBar: AppBar(title: const Text('Debug'), centerTitle: true),
body: Center(
child: SingleChildScrollView(
child: Column(
@@ -60,10 +57,7 @@ class _DebugScreen extends FirkaState<DebugScreen> {
children: [
const Text(
'Debug Screen',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 20),
Row(
@@ -76,7 +70,7 @@ class _DebugScreen extends FirkaState<DebugScreen> {
useCache = value;
});
},
)
),
],
),
const SizedBox(height: 5),
@@ -90,7 +84,7 @@ class _DebugScreen extends FirkaState<DebugScreen> {
debugTimeAdvance = value;
});
},
)
),
],
),
const SizedBox(height: 20),
@@ -111,17 +105,22 @@ class _DebugScreen extends FirkaState<DebugScreen> {
ElevatedButton(
onPressed: () async {
var d = await showDatePicker(
context: context,
firstDate: DateTime.now().subtract(Duration(days: 365)),
lastDate: DateTime.now().add(Duration(days: 365)));
context: context,
firstDate: DateTime.now().subtract(Duration(days: 365)),
lastDate: DateTime.now().add(Duration(days: 365)),
);
if (!context.mounted) return;
var t = await showTimePicker(
context: context, initialTime: TimeOfDay.now());
context: context,
initialTime: TimeOfDay.now(),
);
if (!context.mounted) return;
if (d != null && t != null) {
debugFakeTime = d
.getMidnight()
.add(Duration(hours: t.hour, minutes: t.minute));
debugFakeTime = d.getMidnight().add(
Duration(hours: t.hour, minutes: t.minute),
);
debugSetAt = DateTime.now();
}
@@ -140,21 +139,24 @@ class _DebugScreen extends FirkaState<DebugScreen> {
ElevatedButton(
onPressed: () async {
logger.finest(
"getStudent(): ${await widget.data.client.getStudent(forceCache: useCache)}");
"getStudent(): ${await widget.data.client.getStudent(forceCache: useCache)}",
);
},
child: const Text('getStudent()'),
),
ElevatedButton(
onPressed: () async {
logger.finest(
"getNoticeBoard(): ${await widget.data.client.getNoticeBoard(forceCache: useCache)}");
"getNoticeBoard(): ${await widget.data.client.getNoticeBoard(forceCache: useCache)}",
);
},
child: const Text('getNoticeBoard()'),
),
ElevatedButton(
onPressed: () async {
logger.finest(
"getGrades(): ${await widget.data.client.getGrades(forceCache: useCache)}");
"getGrades(): ${await widget.data.client.getGrades(forceCache: useCache)}",
);
},
child: const Text('getGrades()'),
),
@@ -166,28 +168,32 @@ class _DebugScreen extends FirkaState<DebugScreen> {
var end = now.add(Duration(days: 7));
logger.finest(
"getLessons(): ${await widget.data.client.getTimeTable(start, end, forceCache: useCache)}");
"getLessons(): ${await widget.data.client.getTimeTable(start, end, forceCache: useCache)}",
);
},
child: const Text('getLessons()'),
),
ElevatedButton(
onPressed: () async {
logger.finest(
"getHomework(): ${await widget.data.client.getHomework(forceCache: useCache)}");
"getHomework(): ${await widget.data.client.getHomework(forceCache: useCache)}",
);
},
child: const Text('getHomework()'),
),
ElevatedButton(
onPressed: () async {
logger.finest(
"getTests(): ${await widget.data.client.getTests(forceCache: useCache)}");
"getTests(): ${await widget.data.client.getTests(forceCache: useCache)}",
);
},
child: const Text('getTests()'),
),
ElevatedButton(
onPressed: () async {
logger.finest(
"getOmissions(): ${await widget.data.client.getOmissions(forceCache: useCache)}");
"getOmissions(): ${await widget.data.client.getOmissions(forceCache: useCache)}",
);
},
child: const Text('getOmissions()'),
),
@@ -199,10 +205,11 @@ class _DebugScreen extends FirkaState<DebugScreen> {
),
ElevatedButton(
style: ButtonStyle(
backgroundColor: WidgetStateProperty<
Color>.fromMap(<WidgetStatesConstraint, Color>{
WidgetState.any: Colors.red,
}),
backgroundColor: WidgetStateProperty<Color>.fromMap(
<WidgetStatesConstraint, Color>{
WidgetState.any: Colors.red,
},
),
),
onPressed: () async {
var isar = widget.data.isar;
@@ -213,12 +220,16 @@ class _DebugScreen extends FirkaState<DebugScreen> {
widget.data.tokens = List.empty(growable: true);
if (!context.mounted) return;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DefaultAssetBundle(
bundle: FirkaBundle(),
child: LoginScreen(widget.data))));
context,
MaterialPageRoute(
builder: (context) => DefaultAssetBundle(
bundle: FirkaBundle(),
child: LoginScreen(widget.data),
),
),
);
},
child: const Text('wipe users'),
),
@@ -237,9 +248,11 @@ class _DebugScreen extends FirkaState<DebugScreen> {
),
Center(
child: FirkaIconWidget(
FirkaIconType.majesticons, getIconData(e),
color: Colors.black),
)
FirkaIconType.majesticons,
getIconData(e),
color: Colors.black,
),
),
],
);
}).toList(),

View File

@@ -45,28 +45,32 @@ class _BetaScreenState extends FirkaState<BetaScreen> {
return Scaffold(
backgroundColor: appStyle.colors.background,
body: SafeArea(
child: Column(
children: [
Spacer(),
Center(
child: Text(widget.data.l10n.beta_title,
style: appStyle.fonts.H_H1
.apply(color: appStyle.colors.textPrimary)),
),
SizedBox(height: 32),
Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
children: [
Spacer(),
Center(
child: Text(
widget.data.l10n.beta_body,
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textPrimary),
textAlign: TextAlign.center,
widget.data.l10n.beta_title,
style: appStyle.fonts.H_H1.apply(
color: appStyle.colors.textPrimary,
),
),
),
),
Spacer(),
Padding(
SizedBox(height: 32),
Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Text(
widget.data.l10n.beta_body,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
textAlign: TextAlign.center,
),
),
),
Spacer(),
Padding(
padding: const EdgeInsets.only(left: 32, right: 32, bottom: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -75,10 +79,13 @@ class _BetaScreenState extends FirkaState<BetaScreen> {
child: FirkaButton(
text: widget.data.l10n.cancel,
bgColor: appStyle.colors.buttonSecondaryFill,
fontStyle: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textPrimaryLight),
icon: Icon(Icons.close,
color: appStyle.colors.textPrimaryLight),
fontStyle: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimaryLight,
),
icon: Icon(
Icons.close,
color: appStyle.colors.textPrimaryLight,
),
),
onTap: () {
exit(0);
@@ -92,10 +99,13 @@ class _BetaScreenState extends FirkaState<BetaScreen> {
bgColor: counter == 0
? appStyle.colors.accent
: appStyle.colors.secondary,
fontStyle: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textPrimaryLight),
icon: Icon(Icons.check,
color: appStyle.colors.textPrimaryLight),
fontStyle: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimaryLight,
),
icon: Icon(
Icons.check,
color: appStyle.colors.textPrimaryLight,
),
),
onTap: () async {
if (counter != 0) return;
@@ -112,18 +122,21 @@ class _BetaScreenState extends FirkaState<BetaScreen> {
await widget.data.settings
.group("settings")["beta_warning"]!
.postUpdate();
if (!context.mounted) return;
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (context) =>
HomeScreen(widget.data, false)),
builder: (context) => HomeScreen(widget.data, false),
),
(route) => false,
);
},
),
],
)),
],
)),
),
),
],
),
),
);
}

View File

@@ -44,11 +44,18 @@ class PageNavData {
String subjectName;
String? subjectId;
String? subjectCategory;
PageNavData(this.page, this.subPageParams, this.subjectName, {this.subjectId, this.subjectCategory});
PageNavData(
this.page,
this.subPageParams,
this.subjectName, {
this.subjectId,
this.subjectCategory,
});
}
final ValueNotifier<PageNavData> pageNavNotifier =
ValueNotifier(PageNavData(HomePage.home, null, ""));
final ValueNotifier<PageNavData> pageNavNotifier = ValueNotifier(
PageNavData(HomePage.home, null, ""),
);
bool forcedNavPage = false; // TODO: this is a hack!
class HomeScreen extends StatefulWidget {
@@ -89,8 +96,9 @@ class _HomeScreenState extends FirkaState<HomeScreen> {
bool _preloadDone = false;
int forcedNav = 0;
bool _didRunSecondaryICloudRecovery = false;
final RefreshController _refreshController =
RefreshController(initialRefresh: false);
final RefreshController _refreshController = RefreshController(
initialRefresh: false,
);
ActiveToastType activeToast = ActiveToastType.none;
@@ -214,7 +222,8 @@ class _HomeScreenState extends FirkaState<HomeScreen> {
);
final now = DateTime.now();
final shouldRunRecovery = KretaClient.needsReauth ||
final shouldRunRecovery =
KretaClient.needsReauth ||
activeToken == null ||
activeToken.expiryDate == null ||
activeToken.expiryDate!.isBefore(now.add(const Duration(seconds: 60)));
@@ -224,7 +233,8 @@ class _HomeScreenState extends FirkaState<HomeScreen> {
}
logger.info(
'[Home] Secondary iCloud recovery scheduled (5s delay, startup safety pass)');
'[Home] Secondary iCloud recovery scheduled (5s delay, startup safety pass)',
);
await Future.delayed(const Duration(seconds: 5));
if (_disposed) return;
@@ -282,12 +292,15 @@ class _HomeScreenState extends FirkaState<HomeScreen> {
if (Platform.isAndroid) {
await WidgetCacheHelper.updateWidgetCache(appStyle, widget.data.client);
await HomeWidget.updateWidget(
qualifiedAndroidName: "app.firka.naplo.glance.TimetableWidget");
qualifiedAndroidName: "app.firka.naplo.glance.TimetableWidget",
);
}
if (Platform.isIOS) {
await WidgetCacheHelper.refreshIOSWidgets(
widget.data.client, widget.data.settings);
widget.data.client,
widget.data.settings,
);
final token = pickActiveToken(
tokens: widget.data.tokens,
@@ -372,14 +385,18 @@ class _HomeScreenState extends FirkaState<HomeScreen> {
children: [
Text(
widget.data.l10n.api_error,
style: appStyle.fonts.B_16SB
.copyWith(color: appStyle.colors.errorText),
style: appStyle.fonts.B_16SB.copyWith(
color: appStyle.colors.errorText,
),
),
SizedBox(width: 8),
GestureDetector(
child: FirkaIconWidget(FirkaIconType.majesticons,
Majesticon.questionCircleSolid,
color: appStyle.colors.errorAccent, size: 24),
child: FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.questionCircleSolid,
color: appStyle.colors.errorAccent,
size: 24,
),
onTap: () {
var stackTrace = "";
if (e is Error && e.stackTrace != null) {
@@ -419,11 +436,13 @@ class _HomeScreenState extends FirkaState<HomeScreen> {
widget.data.client
.getTimeTableStream(
midnight, midnight.add(Duration(hours: 23, minutes: 59)),
cacheOnly: false)
midnight,
midnight.add(Duration(hours: 23, minutes: 59)),
cacheOnly: false,
)
.forEach((lessons) {
lessonsFetched++;
});
lessonsFetched++;
});
widget.data.client.getNoticeBoardStream(cacheOnly: false).forEach((items) {
noticeBoardFetched++;
@@ -617,9 +636,10 @@ class _HomeScreenState extends FirkaState<HomeScreen> {
SizedBox(width: 16),
Text(
widget.data.l10n.refreshing,
style: appStyle.fonts.B_16SB
.copyWith(color: appStyle.colors.textPrimary),
)
style: appStyle.fonts.B_16SB.copyWith(
color: appStyle.colors.textPrimary,
),
),
],
),
),
@@ -631,111 +651,118 @@ class _HomeScreenState extends FirkaState<HomeScreen> {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
return DefaultAssetBundle(
bundle: FirkaBundle(),
child: PopScope(
canPop: canPop || subPageActive.value,
child: Scaffold(
backgroundColor: appStyle.colors.background,
body: SafeArea(
child: SizedBox(
height: MediaQuery.of(context).size.height,
child: Stack(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: RefreshConfiguration(
springDescription: SpringDescription(
mass: 1.9, stiffness: 85, damping: 16),
child: SmartScroll(
controller: _refreshController,
onLoading: _onLoading,
onRefresh: _onRefresh,
header: MaterialClassicHeader(
color: appStyle.colors.accent,
backgroundColor: appStyle.colors.background,
offset: 24,
),
physics: ClampingScrollPhysics(),
child: PageView(
controller: _pageController,
physics: ClampingScrollPhysics(),
onPageChanged: (index) {
HapticFeedback.heavyImpact();
bundle: FirkaBundle(),
child: PopScope(
canPop: canPop || subPageActive.value,
child: Scaffold(
backgroundColor: appStyle.colors.background,
body: SafeArea(
child: SizedBox(
height: MediaQuery.of(context).size.height,
child: Stack(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: RefreshConfiguration(
springDescription: SpringDescription(
mass: 1.9,
stiffness: 85,
damping: 16,
),
child: SmartScroll(
controller: _refreshController,
onLoading: _onLoading,
onRefresh: _onRefresh,
header: MaterialClassicHeader(
color: appStyle.colors.accent,
backgroundColor: appStyle.colors.background,
offset: 24,
),
physics: ClampingScrollPhysics(),
child: PageView(
controller: _pageController,
physics: ClampingScrollPhysics(),
onPageChanged: (index) {
HapticFeedback.heavyImpact();
if (forcedNav > 0) {
forcedNav--;
if (forcedNav > 0) {
forcedNav--;
if (previousPages.isEmpty) {
canPop = true;
}
return;
}
if (previousPages.isEmpty) {
canPop = true;
}
return;
}
setState(() {
previousPages.add(homeScreenPage);
canPop = false;
homeScreenPage = HomePage.values[index];
});
},
children: [
HomeSubPage(
HomePage.home,
widget.data,
widget.updateNotifier,
widget.updateFinishedNotifier,
),
HomeSubPage(
HomePage.grades,
widget.data,
widget.updateNotifier,
widget.updateFinishedNotifier,
),
HomeSubPage(
HomePage.timetable,
widget.data,
widget.updateNotifier,
widget.updateFinishedNotifier,
),
],
))),
),
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
appStyle.colors.background,
appStyle.colors.background
.withValues(alpha: 0.0),
setState(() {
previousPages.add(homeScreenPage);
canPop = false;
homeScreenPage = HomePage.values[index];
});
},
children: [
HomeSubPage(
HomePage.home,
widget.data,
widget.updateNotifier,
widget.updateFinishedNotifier,
),
HomeSubPage(
HomePage.grades,
widget.data,
widget.updateNotifier,
widget.updateFinishedNotifier,
),
HomeSubPage(
HomePage.timetable,
widget.data,
widget.updateNotifier,
widget.updateFinishedNotifier,
),
],
stops: const [0.0, 1.0],
),
),
width: MediaQuery.of(context).size.width,
child: Padding(
padding: const EdgeInsets.fromLTRB(55, 0, 55, 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Home Button
BottomNavIconWidget(() {
),
),
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
appStyle.colors.background,
appStyle.colors.background.withValues(alpha: 0.0),
],
stops: const [0.0, 1.0],
),
),
width: MediaQuery.of(context).size.width,
child: Padding(
padding: const EdgeInsets.fromLTRB(55, 0, 55, 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Home Button
BottomNavIconWidget(
() {
if (homeScreenPage != HomePage.home) {
if (previousPages.length > 1 &&
forcedNavPage) {
forcedNavPage = false;
_pageController.jumpToPage(previousPages[
previousPages.length - 2]
.index);
_pageController.jumpToPage(
previousPages[previousPages.length - 2]
.index,
);
}
if (previousPages.length > 1 &&
forcedNavPage) {
forcedNavPage = false;
_pageController.jumpToPage(previousPages[
previousPages.length - 2]
.index);
_pageController.jumpToPage(
previousPages[previousPages.length - 2]
.index,
);
}
_pageController.animateToPage(
HomePage.home.index,
@@ -744,24 +771,27 @@ class _HomeScreenState extends FirkaState<HomeScreen> {
);
}
},
homeScreenPage == HomePage.home,
homeScreenPage == HomePage.home
? Majesticon.homeSolid
: Majesticon.homeLine,
widget.data.l10n.home,
homeScreenPage == HomePage.home
? appStyle.colors.accent
: appStyle.colors.secondary,
appStyle.colors.textPrimary),
// Grades Button
BottomNavIconWidget(() {
homeScreenPage == HomePage.home,
homeScreenPage == HomePage.home
? Majesticon.homeSolid
: Majesticon.homeLine,
widget.data.l10n.home,
homeScreenPage == HomePage.home
? appStyle.colors.accent
: appStyle.colors.secondary,
appStyle.colors.textPrimary,
),
// Grades Button
BottomNavIconWidget(
() {
if (homeScreenPage != HomePage.grades) {
if (previousPages.length > 1 &&
forcedNavPage) {
forcedNavPage = false;
_pageController.jumpToPage(previousPages[
previousPages.length - 2]
.index);
_pageController.jumpToPage(
previousPages[previousPages.length - 2]
.index,
);
}
_pageController.animateToPage(
HomePage.grades.index,
@@ -770,24 +800,27 @@ class _HomeScreenState extends FirkaState<HomeScreen> {
);
}
},
homeScreenPage == HomePage.grades,
homeScreenPage == HomePage.grades
? Majesticon.bookmarkSolid
: Majesticon.bookmarkLine,
widget.data.l10n.grades,
homeScreenPage == HomePage.grades
? appStyle.colors.accent
: appStyle.colors.secondary,
appStyle.colors.textPrimary),
// Timetable Button
BottomNavIconWidget(() {
homeScreenPage == HomePage.grades,
homeScreenPage == HomePage.grades
? Majesticon.bookmarkSolid
: Majesticon.bookmarkLine,
widget.data.l10n.grades,
homeScreenPage == HomePage.grades
? appStyle.colors.accent
: appStyle.colors.secondary,
appStyle.colors.textPrimary,
),
// Timetable Button
BottomNavIconWidget(
() {
if (homeScreenPage != HomePage.timetable) {
if (previousPages.length > 1 &&
forcedNavPage) {
forcedNavPage = false;
_pageController.jumpToPage(previousPages[
previousPages.length - 2]
.index);
_pageController.jumpToPage(
previousPages[previousPages.length - 2]
.index,
);
}
_pageController.animateToPage(
HomePage.timetable.index,
@@ -796,73 +829,76 @@ class _HomeScreenState extends FirkaState<HomeScreen> {
);
}
},
homeScreenPage == HomePage.timetable,
homeScreenPage == HomePage.timetable
? Majesticon.calendarSolid
: Majesticon.calendarLine,
widget.data.l10n.timetable,
homeScreenPage == HomePage.timetable
? appStyle.colors.accent
: appStyle.colors.secondary,
appStyle.colors.textPrimary),
// More Button
BottomNavIconWidget(
() {
HapticFeedback.lightImpact();
showExtrasBottomSheet(context, widget.data);
},
false,
widget.data.profilePicture != null
? widget.data.profilePicture!
: Majesticon.menuLine,
widget.data.l10n.other,
appStyle.colors.secondary,
appStyle.colors.textPrimary,
isProfilePicture:
widget.data.profilePicture != null,
),
],
),
homeScreenPage == HomePage.timetable,
homeScreenPage == HomePage.timetable
? Majesticon.calendarSolid
: Majesticon.calendarLine,
widget.data.l10n.timetable,
homeScreenPage == HomePage.timetable
? appStyle.colors.accent
: appStyle.colors.secondary,
appStyle.colors.textPrimary,
),
// More Button
BottomNavIconWidget(
() {
HapticFeedback.lightImpact();
showExtrasBottomSheet(context, widget.data);
},
false,
widget.data.profilePicture != null
? widget.data.profilePicture!
: Majesticon.menuLine,
widget.data.l10n.other,
appStyle.colors.secondary,
appStyle.colors.textPrimary,
isProfilePicture:
widget.data.profilePicture != null,
),
],
),
),
],
),
toast ?? SizedBox(),
],
),
),
],
),
toast ?? SizedBox(),
],
),
),
),
onPopInvokedWithResult: (_, __) {
if (subPageActive.value) {
subPageBack.update();
return;
}
),
onPopInvokedWithResult: (bool didPop, dynamic result) {
if (subPageActive.value) {
subPageBack.update();
return;
}
if (previousPages.isNotEmpty &&
homeScreenPage != previousPages.last) {
setState(() {
homeScreenPage = previousPages.removeLast();
if (previousPages.isNotEmpty &&
homeScreenPage != previousPages.last) {
setState(() {
homeScreenPage = previousPages.removeLast();
forcedNav++;
_pageController.animateToPage(
homeScreenPage.index,
duration: Duration(milliseconds: 175),
curve: Curves.easeInOut,
);
canPop = previousPages.isEmpty;
});
}
},
));
forcedNav++;
_pageController.animateToPage(
homeScreenPage.index,
duration: Duration(milliseconds: 175),
curve: Curves.easeInOut,
);
canPop = previousPages.isEmpty;
});
}
},
),
);
}
@override
void dispose() {
_pageController.dispose();
widget.data.settingsUpdateNotifier.removeListener(settingsUpdateListener);
widget.data.profilePictureUpdateNotifier
.removeListener(settingsUpdateListener);
widget.data.profilePictureUpdateNotifier.removeListener(
settingsUpdateListener,
);
widget.data.profilePictureUpdateNotifier.removeListener(() {
if (mounted) setState(() {});
@@ -886,8 +922,12 @@ class HomeSubPage extends StatefulWidget {
final UpdateNotifier _updateFinishNotifier;
const HomeSubPage(
this.page, this.data, this._updateNotifier, this._updateFinishNotifier,
{super.key});
this.page,
this.data,
this._updateNotifier,
this._updateFinishNotifier, {
super.key,
});
@override
State<HomeSubPage> createState() => _HomeSubPage();
@@ -910,43 +950,101 @@ class _HomeSubPage extends State<HomeSubPage> {
if (subPageData == null) {
switch (p) {
case HomePage.home:
return HomeMainScreen(widget.data, widget._updateNotifier,
widget._updateFinishNotifier);
return HomeMainScreen(
widget.data,
widget._updateNotifier,
widget._updateFinishNotifier,
);
case HomePage.grades:
return PageWithSubPages([
(cb) => HomeGradesScreen(widget.data, widget._updateNotifier,
widget._updateFinishNotifier, cb),
(cb) => HomeGradesSubjectScreen(widget.data, widget._updateNotifier,
widget._updateFinishNotifier, cb),
], subPageActive, subPageBack, pageIndex: 0);
return PageWithSubPages(
[
(cb) => HomeGradesScreen(
widget.data,
widget._updateNotifier,
widget._updateFinishNotifier,
cb,
),
(cb) => HomeGradesSubjectScreen(
widget.data,
widget._updateNotifier,
widget._updateFinishNotifier,
cb,
),
],
subPageActive,
subPageBack,
pageIndex: 0,
);
case HomePage.timetable:
return PageWithSubPages([
(cb) => HomeTimetableScreen(widget.data, widget._updateNotifier,
widget._updateFinishNotifier, cb),
(cb) => HomeTimetableMonthlyScreen(widget.data,
widget._updateNotifier, widget._updateFinishNotifier, cb)
], subPageActive, subPageBack, pageIndex: 0);
return PageWithSubPages(
[
(cb) => HomeTimetableScreen(
widget.data,
widget._updateNotifier,
widget._updateFinishNotifier,
cb,
),
(cb) => HomeTimetableMonthlyScreen(
widget.data,
widget._updateNotifier,
widget._updateFinishNotifier,
cb,
),
],
subPageActive,
subPageBack,
pageIndex: 0,
);
}
} else {
switch (p) {
case HomePage.home:
return HomeMainScreen(widget.data, widget._updateNotifier,
widget._updateFinishNotifier);
return HomeMainScreen(
widget.data,
widget._updateNotifier,
widget._updateFinishNotifier,
);
case HomePage.grades:
activeSubjectUid = subPageData!;
return PageWithSubPages([
(cb) => HomeGradesSubjectScreen(widget.data, widget._updateNotifier,
widget._updateFinishNotifier, cb),
(cb) => HomeGradesScreen(widget.data, widget._updateNotifier,
widget._updateFinishNotifier, cb),
], subPageActive, subPageBack, pageIndex: 0);
return PageWithSubPages(
[
(cb) => HomeGradesSubjectScreen(
widget.data,
widget._updateNotifier,
widget._updateFinishNotifier,
cb,
),
(cb) => HomeGradesScreen(
widget.data,
widget._updateNotifier,
widget._updateFinishNotifier,
cb,
),
],
subPageActive,
subPageBack,
pageIndex: 0,
);
case HomePage.timetable:
return PageWithSubPages([
(cb) => HomeTimetableMonthlyScreen(widget.data,
widget._updateNotifier, widget._updateFinishNotifier, cb),
(cb) => HomeTimetableScreen(widget.data, widget._updateNotifier,
widget._updateFinishNotifier, cb)
], subPageActive, subPageBack, pageIndex: 0);
return PageWithSubPages(
[
(cb) => HomeTimetableMonthlyScreen(
widget.data,
widget._updateNotifier,
widget._updateFinishNotifier,
cb,
),
(cb) => HomeTimetableScreen(
widget.data,
widget._updateNotifier,
widget._updateFinishNotifier,
cb,
),
],
subPageActive,
subPageBack,
pageIndex: 0,
);
}
}
}
@@ -954,21 +1052,50 @@ class _HomeSubPage extends State<HomeSubPage> {
switch (widget.page) {
case HomePage.home:
return HomeMainScreen(
widget.data, widget._updateNotifier, widget._updateFinishNotifier);
widget.data,
widget._updateNotifier,
widget._updateFinishNotifier,
);
case HomePage.grades:
return PageWithSubPages([
(cb) => HomeGradesScreen(widget.data, widget._updateNotifier,
widget._updateFinishNotifier, cb),
(cb) => HomeGradesSubjectScreen(widget.data, widget._updateNotifier,
widget._updateFinishNotifier, cb),
], subPageActive, subPageBack, pageIndex: 0);
return PageWithSubPages(
[
(cb) => HomeGradesScreen(
widget.data,
widget._updateNotifier,
widget._updateFinishNotifier,
cb,
),
(cb) => HomeGradesSubjectScreen(
widget.data,
widget._updateNotifier,
widget._updateFinishNotifier,
cb,
),
],
subPageActive,
subPageBack,
pageIndex: 0,
);
case HomePage.timetable:
return PageWithSubPages([
(cb) => HomeTimetableScreen(widget.data, widget._updateNotifier,
widget._updateFinishNotifier, cb),
(cb) => HomeTimetableMonthlyScreen(widget.data,
widget._updateNotifier, widget._updateFinishNotifier, cb)
], subPageActive, subPageBack, pageIndex: 0);
return PageWithSubPages(
[
(cb) => HomeTimetableScreen(
widget.data,
widget._updateNotifier,
widget._updateFinishNotifier,
cb,
),
(cb) => HomeTimetableMonthlyScreen(
widget.data,
widget._updateNotifier,
widget._updateFinishNotifier,
cb,
),
],
subPageActive,
subPageBack,
pageIndex: 0,
);
}
}
@@ -986,7 +1113,7 @@ class _HomeSubPage extends State<HomeSubPage> {
forcedHomePage = pageNavNotifier.value.page;
subPageData = pageNavNotifier.value.subPageParams;
subjectName = pageNavNotifier.value.subjectName;
subjectId = pageNavNotifier.value.subjectId ?? "";
subjectId = pageNavNotifier.value.subjectId ?? "";
subjectCategory = pageNavNotifier.value.subjectCategory ?? "";
previousPages.add(homeScreenPage);
homeScreenPage = forcedHomePage!;

View File

@@ -12,10 +12,12 @@ class FullPrivacyPolicyScreen extends StatefulWidget {
const FullPrivacyPolicyScreen({required this.data, super.key});
@override
State<FullPrivacyPolicyScreen> createState() => _FullPrivacyPolicyScreenState();
State<FullPrivacyPolicyScreen> createState() =>
_FullPrivacyPolicyScreenState();
}
class _FullPrivacyPolicyScreenState extends FirkaState<FullPrivacyPolicyScreen> {
class _FullPrivacyPolicyScreenState
extends FirkaState<FullPrivacyPolicyScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -41,8 +43,9 @@ class _FullPrivacyPolicyScreenState extends FirkaState<FullPrivacyPolicyScreen>
Expanded(
child: Text(
widget.data.l10n.la_privacy_header,
style: appStyle.fonts.H_H2
.apply(color: appStyle.colors.textPrimary),
style: appStyle.fonts.H_H2.apply(
color: appStyle.colors.textPrimary,
),
),
),
],
@@ -58,8 +61,9 @@ class _FullPrivacyPolicyScreenState extends FirkaState<FullPrivacyPolicyScreen>
children: [
Text(
widget.data.l10n.la_privacy_intro,
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textSecondary),
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
),
SizedBox(height: 24),
_buildPrivacySection(
@@ -85,14 +89,16 @@ class _FullPrivacyPolicyScreenState extends FirkaState<FullPrivacyPolicyScreen>
SizedBox(height: 16),
Text(
widget.data.l10n.la_privacy_footer,
style: appStyle.fonts.B_12R
.apply(color: appStyle.colors.textTertiary),
style: appStyle.fonts.B_12R.apply(
color: appStyle.colors.textTertiary,
),
),
SizedBox(height: 8),
Text(
widget.data.l10n.la_privacy_contact,
style: appStyle.fonts.B_12R
.apply(color: appStyle.colors.textTertiary),
style: appStyle.fonts.B_12R.apply(
color: appStyle.colors.textTertiary,
),
),
SizedBox(height: 24),
],
@@ -114,14 +120,16 @@ class _FullPrivacyPolicyScreenState extends FirkaState<FullPrivacyPolicyScreen>
children: [
Text(
title,
style:
appStyle.fonts.H_16px.apply(color: appStyle.colors.textPrimary),
style: appStyle.fonts.H_16px.apply(
color: appStyle.colors.textPrimary,
),
),
SizedBox(height: 10),
Text(
body,
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textSecondary),
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
),
],
),

View File

@@ -14,10 +14,7 @@ import '../../../../helpers/firka_state.dart';
class LiveActivityConsentScreen extends StatefulWidget {
final AppInitialization data;
const LiveActivityConsentScreen({
required this.data,
super.key,
});
const LiveActivityConsentScreen({required this.data, super.key});
@override
State<LiveActivityConsentScreen> createState() =>
@@ -48,8 +45,9 @@ class _LiveActivityConsentScreenState
Expanded(
child: Text(
widget.data.l10n.la_title,
style: appStyle.fonts.H_H1
.apply(color: appStyle.colors.textPrimary),
style: appStyle.fonts.H_H1.apply(
color: appStyle.colors.textPrimary,
),
),
),
],
@@ -59,8 +57,9 @@ class _LiveActivityConsentScreenState
width: double.infinity,
child: Text(
widget.data.l10n.la_subtitle,
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textSecondary),
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
maxLines: null,
softWrap: true,
),
@@ -89,8 +88,9 @@ class _LiveActivityConsentScreenState
Expanded(
child: Text(
widget.data.l10n.la_privacy_title,
style: appStyle.fonts.H_16px
.apply(color: appStyle.colors.warningText),
style: appStyle.fonts.H_16px.apply(
color: appStyle.colors.warningText,
),
),
),
],
@@ -98,8 +98,9 @@ class _LiveActivityConsentScreenState
SizedBox(height: 8),
Text(
widget.data.l10n.la_privacy_required,
style: appStyle.fonts.B_14R
.apply(color: appStyle.colors.warningText),
style: appStyle.fonts.B_14R.apply(
color: appStyle.colors.warningText,
),
softWrap: true,
),
],
@@ -115,8 +116,9 @@ class _LiveActivityConsentScreenState
children: [
Text(
widget.data.l10n.la_privacy_intro,
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textPrimary),
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
),
SizedBox(height: 20),
_buildPrivacySummaryItem(
@@ -138,10 +140,12 @@ class _LiveActivityConsentScreenState
GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
FullPrivacyPolicyScreen(data: widget.data)));
context,
MaterialPageRoute(
builder: (context) =>
FullPrivacyPolicyScreen(data: widget.data),
),
);
},
child: FirkaCard(
color: appStyle.colors.buttonSecondaryFill,
@@ -149,8 +153,9 @@ class _LiveActivityConsentScreenState
center: [
Text(
widget.data.l10n.la_learn_more,
style: appStyle.fonts.B_16SB
.apply(color: appStyle.colors.textSecondary),
style: appStyle.fonts.B_16SB.apply(
color: appStyle.colors.textSecondary,
),
),
SizedBox(width: 8),
FirkaIconWidget(
@@ -179,7 +184,8 @@ class _LiveActivityConsentScreenState
text: widget.data.l10n.la_accept,
bgColor: appStyle.colors.accent,
fontStyle: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary),
color: appStyle.colors.textPrimary,
),
),
),
SizedBox(height: 12),
@@ -190,8 +196,9 @@ class _LiveActivityConsentScreenState
child: FirkaButton(
text: widget.data.l10n.la_decline,
bgColor: appStyle.colors.buttonSecondaryFill,
fontStyle: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textSecondary),
fontStyle: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
),
),
],
@@ -226,14 +233,16 @@ class _LiveActivityConsentScreenState
children: [
Text(
title,
style: appStyle.fonts.B_16SB
.apply(color: appStyle.colors.textPrimary),
style: appStyle.fonts.B_16SB.apply(
color: appStyle.colors.textPrimary,
),
),
SizedBox(height: 6),
Text(
description,
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textSecondary),
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
),
],
),

View File

@@ -12,8 +12,10 @@ import '../../../../helpers/firka_state.dart';
import '../../../../helpers/image_preloader.dart';
import '../../../model/style.dart';
import '../../../widget/delayed_spinner.dart';
// TODO: Replace these with actual privacy policy URLs
const String _privacyUrlHungarian = 'https://github.com/QwIT-Development/privacy-policy/blob/master/README.md';
const String _privacyUrlHungarian =
'https://github.com/QwIT-Development/privacy-policy/blob/master/README.md';
const String _privacyUrlOther = 'https://firka.app/privacy';
class LoginScreen extends StatefulWidget {
@@ -107,7 +109,8 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
final carousel = isLightMode.value ? "carousel" : "carousel_dark";
final paddingWidthHorizontal = MediaQuery.of(context).size.width -
final paddingWidthHorizontal =
MediaQuery.of(context).size.width -
MediaQuery.of(context).size.width * 0.95;
List<Map<String, Object>> slides = [
{
@@ -155,7 +158,7 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
'scale': 1.35,
'x': -5.00,
'y': 80.00,
}
},
//TODO: implement simulated physics so that the little boxes can move like the phone moves
];
@@ -163,260 +166,286 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
home: Scaffold(
backgroundColor: appStyle.colors.background,
body: SafeArea(
child: Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16),
Padding(
padding: EdgeInsets.only(left: paddingWidthHorizontal),
child: Row(
children: [
Container(
width: 30,
height: 30,
clipBehavior: Clip.antiAlias,
decoration: ShapeDecoration(
image: DecorationImage(
image: PreloadedImageProvider(
DefaultAssetBundle.of(context),
'assets/images/logos/colored_logo.webp'),
fit: BoxFit.cover,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
),
const SizedBox(width: 8),
Text(
'Firka Napló',
style: appStyle.fonts.H_18px
.copyWith(color: appStyle.colors.textPrimary),
),
],
),
),
const SizedBox(height: 16),
Expanded(
child: CarouselSlider.builder(
itemCount: slides.length,
itemBuilder: (context, index, realIndex) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
child: Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16),
Padding(
padding: EdgeInsets.only(left: paddingWidthHorizontal),
child: Row(
children: [
Padding(
padding: EdgeInsetsGeometry.symmetric(
horizontal: paddingWidthHorizontal),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
slides[index]['title']! as String,
style: appStyle.fonts.H_18px.copyWith(
color: appStyle.colors.textPrimary),
softWrap: true,
overflow: TextOverflow.visible,
Container(
width: 30,
height: 30,
clipBehavior: Clip.antiAlias,
decoration: ShapeDecoration(
image: DecorationImage(
image: PreloadedImageProvider(
DefaultAssetBundle.of(context),
'assets/images/logos/colored_logo.webp',
),
const SizedBox(height: 8),
Text(
slides[index]['subtitle']! as String,
style: appStyle.fonts.B_16R.copyWith(
color: appStyle.colors.textPrimary),
softWrap: true,
overflow: TextOverflow.visible,
),
],
fit: BoxFit.cover,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
Stack(
children: [
slides[index]['background']! == ''
? SizedBox()
: ClipRect(
clipper:
ImageClipper(MediaQuery.of(context)),
child: Transform.rotate(
angle: -math.pi /
(slides[index]['rotation']!
as double),
child: Transform.translate(
offset: Offset(
const SizedBox(width: 8),
Text(
'Firka Napló',
style: appStyle.fonts.H_18px.copyWith(
color: appStyle.colors.textPrimary,
),
),
],
),
),
const SizedBox(height: 16),
Expanded(
child: CarouselSlider.builder(
itemCount: slides.length,
itemBuilder: (context, index, realIndex) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Padding(
padding: EdgeInsetsGeometry.symmetric(
horizontal: paddingWidthHorizontal,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
slides[index]['title']! as String,
style: appStyle.fonts.H_18px.copyWith(
color: appStyle.colors.textPrimary,
),
softWrap: true,
overflow: TextOverflow.visible,
),
const SizedBox(height: 8),
Text(
slides[index]['subtitle']! as String,
style: appStyle.fonts.B_16R.copyWith(
color: appStyle.colors.textPrimary,
),
softWrap: true,
overflow: TextOverflow.visible,
),
],
),
),
Stack(
children: [
slides[index]['background']! == ''
? SizedBox()
: ClipRect(
clipper: ImageClipper(
MediaQuery.of(context),
),
child: Transform.rotate(
angle:
-math.pi /
(slides[index]['rotation']!
as double),
child: Transform.translate(
offset: Offset(
slides[index]['x'] as double,
slides[index]['y'] as double),
child: SizedBox(
width:
MediaQuery.of(context).size.width,
child: Transform.scale(
scale: slides[index]['scale']
as double,
slides[index]['y'] as double,
),
child: SizedBox(
width: MediaQuery.of(
context,
).size.width,
child: Transform.scale(
scale:
slides[index]['scale']
as double,
child: Image.asset(
slides[index]['background']!
as String,
bundle: DefaultAssetBundle.of(
context),
context,
),
fit: BoxFit.contain,
width: double.infinity,
)),
),
),
),
),
),
)),
Column(
children: [
SizedBox(height: 73),
Padding(
padding: EdgeInsetsGeometry.symmetric(
horizontal: 18),
child: SizedBox(
width: MediaQuery.of(context).size.width,
child: Image(
image: PreloadedImageProvider(
),
Column(
children: [
SizedBox(height: 73),
Padding(
padding: EdgeInsetsGeometry.symmetric(
horizontal: 18,
),
child: SizedBox(
width: MediaQuery.of(context).size.width,
child: Image(
image: PreloadedImageProvider(
DefaultAssetBundle.of(context),
slides[index]['picture']! as String),
fit: BoxFit.cover,
width: double.infinity,
alignment: Alignment.center,
slides[index]['picture']! as String,
),
fit: BoxFit.cover,
width: double.infinity,
alignment: Alignment.center,
),
),
),
),
],
),
slides[index]['foreground']! == ''
? SizedBox()
: SizedBox(
width: MediaQuery.of(context).size.width,
child: ClipRect(
clipBehavior: Clip.none,
child: Transform.rotate(
angle: -math.pi /
],
),
slides[index]['foreground']! == ''
? SizedBox()
: SizedBox(
width: MediaQuery.of(context).size.width,
child: ClipRect(
clipBehavior: Clip.none,
child: Transform.rotate(
angle:
-math.pi /
(slides[index]['rotation']!
as double),
child: Transform.translate(
offset: Offset(
slides[index]['x'] as double,
slides[index]['y'] as double),
slides[index]['x'] as double,
slides[index]['y'] as double,
),
child: Transform.scale(
scale: slides[index]['scale']
as double,
scale:
slides[index]['scale']
as double,
child: Image.asset(
slides[index]['foreground']!
as String,
bundle: DefaultAssetBundle.of(
context),
context,
),
fit: BoxFit.cover,
width: MediaQuery.of(context)
.size
.width,
width: MediaQuery.of(
context,
).size.width,
alignment: Alignment.center,
),
),
)),
),
),
),
),
),
],
)
],
),
options: CarouselOptions(
height: double.infinity,
autoPlay: false,
autoPlayInterval: const Duration(milliseconds: 3000),
viewportFraction: 1.0,
enableInfiniteScroll: true,
),
),
),
],
),
Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Container(
width: double.infinity,
height: 200,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
appStyle.colors.background.withAlpha(0),
appStyle.colors.background,
], // customize colors
stops: [0.0, 0.5], // percentages (0% → 50% → 100%)
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
)
],
),
Positioned(
bottom: 10,
left: 0,
right: 0,
child: Column(
children: [
Center(
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: paddingWidthHorizontal),
child: InkWell(
onTap: () {
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
builder: (BuildContext context) {
return _loginWebView;
},
);
},
borderRadius: BorderRadius.circular(12),
child: Container(
width: double.infinity,
height: 48,
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: ShapeDecoration(
color: appStyle.colors.accent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
shadows: [
BoxShadow(
color:
appStyle.colors.textPrimary.withAlpha(13),
blurRadius: 2,
offset: Offset(0, 1),
spreadRadius: 0,
)
],
),
child: Center(
child: Text(
widget.data.l10n.loginBtn,
textAlign: TextAlign.center,
style: appStyle.fonts.H_16px.copyWith(
],
),
options: CarouselOptions(
height: double.infinity,
autoPlay: false,
autoPlayInterval: const Duration(milliseconds: 3000),
viewportFraction: 1.0,
enableInfiniteScroll: true,
),
),
),
],
),
Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Container(
width: double.infinity,
height: 200,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
appStyle.colors.background.withAlpha(0),
appStyle.colors.background,
], // customize colors
stops: [0.0, 0.5], // percentages (0% → 50% → 100%)
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
),
],
),
Positioned(
bottom: 10,
left: 0,
right: 0,
child: Column(
children: [
Center(
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: paddingWidthHorizontal,
),
child: InkWell(
onTap: () {
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
builder: (BuildContext context) {
return _loginWebView;
},
);
},
borderRadius: BorderRadius.circular(12),
child: Container(
width: double.infinity,
height: 48,
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: ShapeDecoration(
color: appStyle.colors.accent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
shadows: [
BoxShadow(
color: appStyle.colors.textPrimary.withAlpha(
13,
),
blurRadius: 2,
offset: Offset(0, 1),
spreadRadius: 0,
),
],
),
child: Center(
child: Text(
widget.data.l10n.loginBtn,
textAlign: TextAlign.center,
style: appStyle.fonts.H_16px.copyWith(
color: appStyle.colors.textPrimaryLight,
fontVariations: [FontVariation("wght", 800)]),
fontVariations: [FontVariation("wght", 800)],
),
),
),
),
),
),
),
),
const SizedBox(height: 20),
GestureDetector(
child: Text(
widget.data.l10n.privacyLabel,
textAlign: TextAlign.center,
style: appStyle.fonts.H_12px
.copyWith(color: appStyle.colors.textTertiary),
const SizedBox(height: 20),
GestureDetector(
onTap: _launchPrivacyPolicy,
child: Text(
widget.data.l10n.privacyLabel,
textAlign: TextAlign.center,
style: appStyle.fonts.H_12px.copyWith(
color: appStyle.colors.textTertiary,
),
),
),
onTap: _launchPrivacyPolicy,
)
],
],
),
),
),
],
)),
],
),
),
),
);
}
@@ -431,7 +460,11 @@ class ImageClipper extends CustomClipper<Rect> {
@override
Rect getClip(Size size) {
return Rect.fromLTWH(
0, -70, _mediaQuery.size.width, _mediaQuery.size.height);
0,
-70,
_mediaQuery.size.width,
_mediaQuery.size.height,
);
}
@override

View File

@@ -17,149 +17,152 @@ class MessageScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DefaultAssetBundle(
bundle: FirkaBundle(),
child: Scaffold(
backgroundColor: appStyle.colors.background,
body: SafeArea(
child: SizedBox(
height: MediaQuery.of(context).size.height,
child: Align(
alignment: Alignment.bottomCenter,
child: Container(
decoration: BoxDecoration(
color: appStyle.colors.background,
borderRadius:
BorderRadius.vertical(top: Radius.circular(16)),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
bundle: FirkaBundle(),
child: Scaffold(
backgroundColor: appStyle.colors.background,
body: SafeArea(
child: SizedBox(
height: MediaQuery.of(context).size.height,
child: Align(
alignment: Alignment.bottomCenter,
child: Container(
decoration: BoxDecoration(
color: appStyle.colors.background,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Row(
children: [
Transform.translate(
offset: const Offset(-4, 0),
child: GestureDetector(
child: FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.chevronLeftLine,
color: appStyle.colors.textSecondary),
onTap: () {
Navigator.of(context).pop();
},
),
Transform.translate(
offset: const Offset(-4, 0),
child: GestureDetector(
child: FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.chevronLeftLine,
color: appStyle.colors.textSecondary,
),
Transform.translate(
offset: const Offset(-4, 1),
child: Text(
data.l10n.s_a,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary),
),
)
],
onTap: () {
Navigator.of(context).pop();
},
),
),
SizedBox(height: 56),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: MediaQuery.of(context).size.width * 0.85,
child: Text(
info.title,
textAlign: TextAlign.center,
style: appStyle.fonts.H_H2.apply(
color: appStyle.colors.textPrimary),
),
),
],
),
SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
info.date
.format(data.l10n, FormatMode.yyyymmdd),
textAlign: TextAlign.center,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary),
),
],
),
SizedBox(height: 56),
Row(
children: [
Container(
decoration: ShapeDecoration(
color: appStyle.colors.accent,
shape: CircleBorder(
eccentricity: 1,
)),
child: SizedBox(
width: 28,
height: 28,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding:
const EdgeInsets.only(bottom: 6),
child: Text(
info.author[0],
style: appStyle.fonts.H_18px.copyWith(
fontSize: 20,
color:
appStyle.colors.textPrimary),
),
)
],
),
),
),
SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width:
MediaQuery.of(context).size.width / 1.4,
child: Text(
info.author,
style: appStyle.fonts.B_16SB.apply(
color: appStyle.colors.textPrimary),
),
)
],
),
],
),
SizedBox(height: 20),
Padding(
padding: const EdgeInsets.all(4),
child: Container(
decoration: BoxDecoration(
color: appStyle.colors.card,
borderRadius:
BorderRadius.all(Radius.circular(16))),
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
info.contentText,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary),
textAlign: TextAlign.start,
),
Transform.translate(
offset: const Offset(-4, 1),
child: Text(
data.l10n.s_a,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
),
),
],
),
),
SizedBox(height: 56),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: MediaQuery.of(context).size.width * 0.85,
child: Text(
info.title,
textAlign: TextAlign.center,
style: appStyle.fonts.H_H2.apply(
color: appStyle.colors.textPrimary,
),
),
),
],
),
SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
info.date.format(data.l10n, FormatMode.yyyymmdd),
textAlign: TextAlign.center,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
),
],
),
SizedBox(height: 56),
Row(
children: [
Container(
decoration: ShapeDecoration(
color: appStyle.colors.accent,
shape: CircleBorder(eccentricity: 1),
),
child: SizedBox(
width: 28,
height: 28,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Text(
info.author[0],
style: appStyle.fonts.H_18px.copyWith(
fontSize: 20,
color: appStyle.colors.textPrimary,
),
),
),
],
),
),
),
SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: MediaQuery.of(context).size.width / 1.4,
child: Text(
info.author,
style: appStyle.fonts.B_16SB.apply(
color: appStyle.colors.textPrimary,
),
),
),
],
),
],
),
SizedBox(height: 20),
Padding(
padding: const EdgeInsets.all(4),
child: Container(
decoration: BoxDecoration(
color: appStyle.colors.card,
borderRadius: BorderRadius.all(Radius.circular(16)),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
info.contentText,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
textAlign: TextAlign.start,
),
),
),
),
],
),
)),
),
),
),
),
));
),
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -14,9 +14,16 @@ class BottomNavIconWidget extends StatelessWidget {
final Color textColor;
final bool isProfilePicture;
const BottomNavIconWidget(this.onTap, this.active, this.icon, this.text,
this.iconColor, this.textColor,
{this.isProfilePicture = false, super.key});
const BottomNavIconWidget(
this.onTap,
this.active,
this.icon,
this.text,
this.iconColor,
this.textColor, {
this.isProfilePicture = false,
super.key,
});
@override
Widget build(BuildContext context) {
@@ -44,17 +51,22 @@ class BottomNavIconWidget extends StatelessWidget {
),
)
else
FirkaIconWidget(FirkaIconType.majesticons, icon as Uint8List,
color: iconColor, size: 24)
.build(context),
FirkaIconWidget(
FirkaIconType.majesticons,
icon as Uint8List,
color: iconColor,
size: 24,
).build(context),
const SizedBox(height: 4),
Text(
text,
style: active
? appStyle.fonts.B_12SB
.apply(color: appStyle.colors.textPrimary)
: appStyle.fonts.B_12R
.apply(color: appStyle.colors.textSecondary),
? appStyle.fonts.B_12SB.apply(
color: appStyle.colors.textPrimary,
)
: appStyle.fonts.B_12R.apply(
color: appStyle.colors.textSecondary,
),
),
],
),

View File

@@ -11,8 +11,12 @@ class BottomTimeTableNavIconWidget extends StatelessWidget {
final DateTime date;
const BottomTimeTableNavIconWidget(
this.l10n, this.onTap, this.active, this.date,
{super.key});
this.l10n,
this.onTap,
this.active,
this.date, {
super.key,
});
@override
Widget build(BuildContext context) {
@@ -22,25 +26,31 @@ class BottomTimeTableNavIconWidget extends StatelessWidget {
onTap();
},
child: Card(
color:
active ? appStyle.colors.buttonSecondaryFill : Colors.transparent,
color: active
? appStyle.colors.buttonSecondaryFill
: Colors.transparent,
shadowColor: active ? appStyle.colors.shadowColor : Colors.transparent,
child: SizedBox(
width: 40,
height: 54,
child: Column(
children: [
SizedBox(height: 6),
Text(date.format(l10n, FormatMode.da),
style: appStyle.fonts.H_16px
.apply(color: appStyle.colors.textPrimary)),
Text(
date.format(l10n, FormatMode.dd),
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textSecondary),
)
],
)),
width: 40,
height: 54,
child: Column(
children: [
SizedBox(height: 6),
Text(
date.format(l10n, FormatMode.da),
style: appStyle.fonts.H_16px.apply(
color: appStyle.colors.textPrimary,
),
),
Text(
date.format(l10n, FormatMode.dd),
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
),
],
),
),
),
);
}

View File

@@ -1,5 +1,4 @@
import 'package:firka/helpers/api/model/grade.dart';
import 'package:firka/helpers/average_helper.dart';
import 'package:firka/helpers/ui/grade_helpers.dart';
import 'package:firka/ui/model/style.dart';
import 'package:fl_chart/fl_chart.dart';
@@ -25,7 +24,7 @@ class _GradeChartState extends State<GradeChart> {
appStyle.colors.grade2,
appStyle.colors.grade1,
];
late final List<FlSpot> spots;
@override
@@ -60,7 +59,6 @@ class _GradeChartState extends State<GradeChart> {
spots.add(FlSpot(9, 4.00));
spots.add(FlSpot(10, 4.89));
if (spots.isEmpty) {
spots = [const FlSpot(0, 0)];
}
@@ -71,9 +69,7 @@ class _GradeChartState extends State<GradeChart> {
return ClipRRect(
borderRadius: BorderRadius.circular(12),
child: DecoratedBox(
decoration: BoxDecoration(
color: appStyle.colors.card,
),
decoration: BoxDecoration(color: appStyle.colors.card),
child: AspectRatio(
aspectRatio: 1.90,
child: Padding(
@@ -114,11 +110,12 @@ class _GradeChartState extends State<GradeChart> {
child: Text(text, style: style),
);
}
Widget buildCircle({
required String text,
required Color bgColor,
required Color textColor,
}) {
}) {
return SizedBox(
width: 24,
height: 24,
@@ -140,8 +137,8 @@ class _GradeChartState extends State<GradeChart> {
),
);
}
Widget leftTitleWidgets(double value, TitleMeta meta) {
Widget leftTitleWidgets(double value, TitleMeta meta) {
String text = switch (value.toInt()) {
1 => '1',
2 => '2',
@@ -151,7 +148,7 @@ class _GradeChartState extends State<GradeChart> {
_ => '',
};
Color gradeColor;
if (text != ""){
if (text != "") {
gradeColor = getGradeColor(int.parse(text).toDouble());
} else {
gradeColor = getGradeColor(0);
@@ -233,14 +230,14 @@ class _GradeChartState extends State<GradeChart> {
getTooltipColor: (touchedSpot) => appStyle.colors.buttonSecondaryFill,
tooltipBorderRadius: BorderRadius.circular(90),
fitInsideVertically: true,
showOnTopOfTheChartBoxArea: true,
getTooltipItems: (touchedSpots) {
return touchedSpots.map((LineBarSpot touchedSpot) {
final textStyle = TextStyle(
color: colorForY(touchedSpot.y),
fontWeight: FontWeight.bold,
fontSize: 14
fontSize: 14,
);
return LineTooltipItem(touchedSpot.y.toString(), textStyle);
}).toList();
@@ -248,15 +245,10 @@ class _GradeChartState extends State<GradeChart> {
),
getTouchedSpotIndicator: (barData, spotIndexes) {
return spotIndexes.map((index) {
final touchedSpot = barData.spots[index];
final touchedSpot = barData.spots[index];
return TouchedSpotIndicatorData(
FlLine(
color: colorForY(touchedSpot.y),
strokeWidth: 3,
),
FlDotData(
show: false,
),
FlLine(color: colorForY(touchedSpot.y), strokeWidth: 3),
FlDotData(show: false),
);
}).toList();
},
@@ -282,18 +274,18 @@ class _GradeChartState extends State<GradeChart> {
// color: const Color(0xFFC8C8C8),
// strokeWidth: 1.2,
// );
return FlLine(
return FlLine(
color: const Color(0xFFC8C8C8),
strokeWidth: 1.0,
dashArray: [8, 12],
);
}
return FlLine(
color: const Color(0xFFC8C8C8),
strokeWidth: 1.0,
dashArray: [8, 12],
);
}
color: const Color(0xFFC8C8C8),
strokeWidth: 1.0,
dashArray: [8, 12],
);
},
),
titlesData: FlTitlesData(
show: true,
@@ -313,9 +305,7 @@ class _GradeChartState extends State<GradeChart> {
interval: 1,
),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
@@ -331,9 +321,8 @@ class _GradeChartState extends State<GradeChart> {
LineChartBarData(
spots: spots,
isCurved: false,
showingIndicators:
_touchedIndex != null ? [_touchedIndex!] : [],
showingIndicators: _touchedIndex != null ? [_touchedIndex!] : [],
gradient: LinearGradient(
colors: [for (final s in spots) colorForY(s.y)],
),
@@ -344,8 +333,7 @@ class _GradeChartState extends State<GradeChart> {
show: true,
gradient: LinearGradient(
colors: [
for (final s in spots)
colorForY(s.y).withValues(alpha: 0.1),
for (final s in spots) colorForY(s.y).withValues(alpha: 0.1),
],
),
),

View File

@@ -38,60 +38,74 @@ class StartingSoonWidget extends StatelessWidget {
SizedBox(width: 6),
Text(
l10n.starting_soon,
style: appStyle.fonts.H_16px
.apply(color: appStyle.colors.textPrimary),
style: appStyle.fonts.H_16px.apply(
color: appStyle.colors.textPrimary,
),
),
],
),
Row(
children: [
CounterDigitWidget(
hour.toString(),
appStyle.fonts.H_16px
.apply(color: appStyle.colors.textPrimary)),
hour.toString(),
appStyle.fonts.H_16px.apply(
color: appStyle.colors.textPrimary,
),
),
SizedBox(width: 2),
Text(
hourTxt,
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textPrimary),
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
),
SizedBox(width: 4),
CounterDigitWidget(
(min / 10).floor().toString(),
appStyle.fonts.H_16px
.apply(color: appStyle.colors.textPrimary)),
(min / 10).floor().toString(),
appStyle.fonts.H_16px.apply(
color: appStyle.colors.textPrimary,
),
),
CounterDigitWidget(
((min % 10)).toString(),
appStyle.fonts.H_16px
.apply(color: appStyle.colors.textPrimary)),
((min % 10)).toString(),
appStyle.fonts.H_16px.apply(
color: appStyle.colors.textPrimary,
),
),
SizedBox(width: 2),
Text(
minTxt,
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textPrimary),
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
),
SizedBox(width: 4),
CounterDigitWidget(
(sec / 10).floor().toString(),
appStyle.fonts.H_16px
.apply(color: appStyle.colors.textPrimary)),
(sec / 10).floor().toString(),
appStyle.fonts.H_16px.apply(
color: appStyle.colors.textPrimary,
),
),
CounterDigitWidget(
((sec % 10)).toString(),
appStyle.fonts.H_16px
.apply(color: appStyle.colors.textPrimary)),
((sec % 10)).toString(),
appStyle.fonts.H_16px.apply(
color: appStyle.colors.textPrimary,
),
),
SizedBox(width: 2),
Text(
secTxt,
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textPrimary),
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
),
],
)
),
],
)
),
],
right: [],
)
),
],
);
}

View File

@@ -16,8 +16,13 @@ class WelcomeWidget extends StatefulWidget {
final List<Lesson> lessons;
final DateTime now;
const WelcomeWidget(this.l10n, this.now, this.student, this.lessons,
{super.key});
const WelcomeWidget(
this.l10n,
this.now,
this.student,
this.lessons, {
super.key,
});
@override
State<WelcomeWidget> createState() => _WelcomeWidgetState();
@@ -29,7 +34,9 @@ class _WelcomeWidgetState extends State<WelcomeWidget> {
@override
void initState() {
super.initState();
_controllerCenter = ConfettiController(duration: const Duration(seconds: 2));
_controllerCenter = ConfettiController(
duration: const Duration(seconds: 2),
);
final birthDate = DateFormat("MM-dd").format(widget.student.birthdate);
if (birthDate == DateFormat("MM-dd").format(widget.now)) {
@@ -37,28 +44,38 @@ class _WelcomeWidgetState extends State<WelcomeWidget> {
}
}
@override
void dispose() {
_controllerCenter.dispose();
super.dispose();
}
getIconForCycle(Cycle dayCycle) {
Widget getIconForCycle(Cycle dayCycle) {
switch (dayCycle) {
case Cycle.morning:
return FirkaIconWidget(FirkaIconType.majesticonsLocal, "sunSolid",
color: appStyle.colors.accent);
return FirkaIconWidget(
FirkaIconType.majesticonsLocal,
"sunSolid",
color: appStyle.colors.accent,
);
case Cycle.day:
return FirkaIconWidget(
FirkaIconType.majesticonsLocal, "parkSolidSchool",
color: appStyle.colors.accent);
FirkaIconType.majesticonsLocal,
"parkSolidSchool",
color: appStyle.colors.accent,
);
case Cycle.afternoon:
return FirkaIconWidget(FirkaIconType.majesticons, Majesticon.moonSolid,
color: appStyle.colors.accent);
return FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.moonSolid,
color: appStyle.colors.accent,
);
case Cycle.night:
return FirkaIconWidget(FirkaIconType.majesticons, Majesticon.moonSolid,
color: appStyle.colors.accent);
return FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.moonSolid,
color: appStyle.colors.accent,
);
}
}
@@ -87,8 +104,9 @@ class _WelcomeWidgetState extends State<WelcomeWidget> {
final birthDate = DateFormat("MM-dd").format(widget.student.birthdate);
if (birthDate == DateFormat("MM-dd").format(widget.now)) {
return widget.l10n.happy_birthday(name);
} else if (widget.lessons.length > 1 &&widget.now.isBefore(widget.lessons.first.start)) {
return getRawTitle(name, dayCycle);
} else if (widget.lessons.length > 1 &&
widget.now.isBefore(widget.lessons.first.start)) {
return getRawTitle(name, dayCycle);
} else {
return getRawTitle(name, dayCycle);
}
@@ -105,8 +123,9 @@ class _WelcomeWidgetState extends State<WelcomeWidget> {
if (now.isBefore(lessons.first.start)) {
return now.format(l10n, FormatMode.welcome);
}
var lessonsLeft =
lessons.where((lesson) => lesson.end.isAfter(now)).length;
var lessonsLeft = lessons
.where((lesson) => lesson.end.isAfter(now))
.length;
if (lessonsLeft < 1) {
return l10n.tomorrow_subtitle;
}
@@ -140,13 +159,19 @@ class _WelcomeWidgetState extends State<WelcomeWidget> {
children: [
getIconForCycle(dayCycle),
const SizedBox(height: 16.0),
Text(getTitle(dayCycle),
style: appStyle.fonts.H_H2
.copyWith(color: appStyle.colors.textPrimary)),
Text(
getTitle(dayCycle),
style: appStyle.fonts.H_H2.copyWith(
color: appStyle.colors.textPrimary,
),
),
const SizedBox(height: 2.0),
Text(getSubtitle(dayCycle),
style: appStyle.fonts.B_16R
.copyWith(color: appStyle.colors.textSecondary)),
Text(
getSubtitle(dayCycle),
style: appStyle.fonts.B_16R.copyWith(
color: appStyle.colors.textSecondary,
),
),
],
),
],

View File

@@ -41,40 +41,45 @@ class HomeworkWidget extends StatelessWidget {
color: appStyle.colors.accent,
size: 24,
);
}
),
SizedBox(
width: 8,
},
),
SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FutureBuilder<bool>(
future: isHomeworkDone(data.isar, item.uid),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return SizedBox();
}
final done = snapshot.data!;
return done
? Text(data.l10n.homework,
style: appStyle.fonts.B_16SB
.apply(color: appStyle.colors.textPrimary, decoration: TextDecoration.lineThrough))
: Text(data.l10n.homework,
style: appStyle.fonts.B_16SB
.apply(color: appStyle.colors.textPrimary));
},
FutureBuilder<bool>(
future: isHomeworkDone(data.isar, item.uid),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return SizedBox();
}
final done = snapshot.data!;
return done
? Text(
data.l10n.homework,
style: appStyle.fonts.B_16SB.apply(
color: appStyle.colors.textPrimary,
decoration: TextDecoration.lineThrough,
),
)
: Text(
data.l10n.homework,
style: appStyle.fonts.B_16SB.apply(
color: appStyle.colors.textPrimary,
),
);
},
),
Text(
item.subjectName,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
Text(
item.subjectName,
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textPrimary),
)
],
),
],
),
],
)
),
],
),
onTap: () {

View File

@@ -12,55 +12,62 @@ class InfoBoardItemWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FirkaCard(left: [
Row(
children: [
Container(
decoration: ShapeDecoration(
return FirkaCard(
left: [
Row(
children: [
Container(
decoration: ShapeDecoration(
color: appStyle.colors.accent,
shape: CircleBorder(
eccentricity: 1,
// borderRadius: BorderRadius.circular(6)),
)),
child: SizedBox(
width: 28,
height: 28,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Text(
item.author[0],
style: appStyle.fonts.H_18px.copyWith(
fontSize: 20, color: appStyle.colors.textPrimary),
),
)
],
),
),
),
SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: MediaQuery.of(context).size.width / 1.4,
child: Text(
item.title,
style: appStyle.fonts.B_16SB
.apply(color: appStyle.colors.textPrimary),
),
),
Text(
item.author,
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textSecondary),
)
],
)
],
)
]);
child: SizedBox(
width: 28,
height: 28,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Text(
item.author[0],
style: appStyle.fonts.H_18px.copyWith(
fontSize: 20,
color: appStyle.colors.textPrimary,
),
),
),
],
),
),
),
SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: MediaQuery.of(context).size.width / 1.4,
child: Text(
item.title,
style: appStyle.fonts.B_16SB.apply(
color: appStyle.colors.textPrimary,
),
),
),
Text(
item.author,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
),
],
),
],
),
],
);
}
}

View File

@@ -25,9 +25,17 @@ class LessonWidget extends StatelessWidget {
final Lesson? nextLesson;
final bool? placeholderMode;
const LessonWidget(this.data, this.week, this.day, this.lessonNo, this.lesson,
this.test, this.nextLesson,
{super.key, this.placeholderMode});
const LessonWidget(
this.data,
this.week,
this.day,
this.lessonNo,
this.lesson,
this.test,
this.nextLesson, {
super.key,
this.placeholderMode,
});
@override
Widget build(BuildContext context) {
@@ -50,8 +58,10 @@ class LessonWidget extends StatelessWidget {
if (week.isNotEmpty) {
showBreak =
timeNow().isAfter(lesson.start) && timeNow().isBefore(lesson.end) ||
timeNow().isAfter(week.last.end) ||
lesson.start.getMidnight() != timeNow().getMidnight() || timeNow().isAfter(day.last.end) || timeNow().isBefore(day.first.start);
timeNow().isAfter(week.last.end) ||
lesson.start.getMidnight() != timeNow().getMidnight() ||
timeNow().isAfter(day.last.end) ||
timeNow().isBefore(day.first.start);
}
var accent = appStyle.colors.accent;
var secondary = appStyle.colors.secondary;
@@ -81,117 +91,146 @@ class LessonWidget extends StatelessWidget {
roomName = "${roomName.substring(0, 11 - 3)}...";
}
elements.add(GestureDetector(
onTap: () {
if (lessonNo == null) return;
showLessonBottomSheet(
context, data, lesson, lessonNo, accent, secondary, bgColor, test);
},
child: FirkaCard(
color: isDismissed
? appStyle.colors.cardTranslucent
: appStyle.colors.card,
shadow: !isDismissed,
left: [
showLessonNos == false || lessonNo == null
? SizedBox()
: SizedBox(
width: 18,
height: 18,
child: Stack(
alignment: Alignment.center,
children: [
SvgPicture.asset(
"assets/icons/subtract.svg",
color: bgColor,
width: 18,
height: 18,
),
Text(
lessonNo.toString(),
style: appStyle.fonts.B_12R.apply(color: secondary),
textAlign: TextAlign.center,
),
],
elements.add(
GestureDetector(
onTap: () {
if (lessonNo == null) return;
showLessonBottomSheet(
context,
data,
lesson,
lessonNo,
accent,
secondary,
bgColor,
test,
);
},
child: FirkaCard(
color: isDismissed
? appStyle.colors.cardTranslucent
: appStyle.colors.card,
shadow: !isDismissed,
left: [
showLessonNos == false || lessonNo == null
? SizedBox()
: SizedBox(
width: 18,
height: 18,
child: Stack(
alignment: Alignment.center,
children: [
SvgPicture.asset(
"assets/icons/subtract.svg",
color: bgColor,
width: 18,
height: 18,
),
Text(
lessonNo.toString(),
style: appStyle.fonts.B_12R.apply(color: secondary),
textAlign: TextAlign.center,
),
],
),
),
Transform.translate(
offset: Offset(-4, 0),
child: Card(
shadowColor: Colors.transparent,
color: bgColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
Transform.translate(
offset: Offset(-4, 0),
child: Card(
shadowColor: Colors.transparent,
color: bgColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16)),
child: Stack(children: [
Padding(
padding: EdgeInsetsGeometry.all(4),
child: ClassIconWidget(
color: accent,
size: 20,
uid: lesson.uid,
className: lesson.name,
category: lesson.subject?.name != null
? lesson.subject!.name.firstUpper()
: '',
),
),
!showTests && test != null
? Transform.translate(
offset: Offset(26, -18),
child: BubbleTest(),
)
: SizedBox(),
]),
),
),
SizedBox(width: !showTests && test != null ? 16 : 8),
Text(subjectName,
style: appStyle.fonts.B_15SB
.apply(color: appStyle.colors.textPrimary)),
],
right: [
placeholderMode == true
? SizedBox()
: Text(
isDismissed
? data.l10n.class_dismissed
: lesson.start
.toLocal()
.format(data.l10n, FormatMode.hmm),
style: appStyle.fonts.B_14R
.apply(color: appStyle.colors.textPrimary)),
placeholderMode == true
? SizedBox()
: isDismissed
? SizedBox()
: Card(
shadowColor: Colors.transparent,
color: appStyle.colors.a15p,
child: Padding(
padding: EdgeInsets.all(5),
child: Text(roomName,
style: appStyle.fonts.B_12R
.apply(color: appStyle.colors.secondary)),
child: Stack(
children: [
Padding(
padding: EdgeInsetsGeometry.all(4),
child: ClassIconWidget(
color: accent,
size: 20,
uid: lesson.uid,
className: lesson.name,
category: lesson.subject?.name != null
? lesson.subject!.name.firstUpper()
: '',
),
),
],
!showTests && test != null
? Transform.translate(
offset: Offset(26, -18),
child: BubbleTest(),
)
: SizedBox(),
],
),
),
),
SizedBox(width: !showTests && test != null ? 16 : 8),
Text(
subjectName,
style: appStyle.fonts.B_15SB.apply(
color: appStyle.colors.textPrimary,
),
),
],
right: [
placeholderMode == true
? SizedBox()
: Text(
isDismissed
? data.l10n.class_dismissed
: lesson.start.toLocal().format(
data.l10n,
FormatMode.hmm,
),
style: appStyle.fonts.B_14R.apply(
color: appStyle.colors.textPrimary,
),
),
placeholderMode == true
? SizedBox()
: isDismissed
? SizedBox()
: Card(
shadowColor: Colors.transparent,
color: appStyle.colors.a15p,
child: Padding(
padding: EdgeInsets.all(5),
child: Text(
roomName,
style: appStyle.fonts.B_12R.apply(
color: appStyle.colors.secondary,
),
),
),
),
],
),
),
));
);
if (isSubstituted && showSubstitutions) {
elements.add(FirkaCard(
left: [
Text(data.l10n.class_substitution,
style: appStyle.fonts.H_16px
.apply(color: appStyle.colors.textPrimary))
],
right: [
Text(lesson.substituteTeacher!,
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textSecondary))
],
));
elements.add(
FirkaCard(
left: [
Text(
data.l10n.class_substitution,
style: appStyle.fonts.H_16px.apply(
color: appStyle.colors.textPrimary,
),
),
],
right: [
Text(
lesson.substituteTeacher!,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
),
],
),
);
}
if (test != null && showTests) {
@@ -206,29 +245,46 @@ class LessonWidget extends StatelessWidget {
theme = theme.firstUpper();
method = method.firstUpper();
elements.add(GestureDetector(
onTap: () {
showTestBottomSheet(
context, data, lesson, lessonNo, accent, secondary, bgColor, test);
},
child: FirkaCard(
left: [
FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.editPen4Solid,
color: appStyle.colors.accent,
elements.add(
GestureDetector(
onTap: () {
showTestBottomSheet(
context,
data,
lesson,
lessonNo,
accent,
secondary,
bgColor,
test,
);
},
child: FirkaCard(
left: [
FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.editPen4Solid,
color: appStyle.colors.accent,
),
SizedBox(width: 6),
Text(
theme,
style: appStyle.fonts.B_16SB.apply(
color: appStyle.colors.textSecondary,
),
),
],
right: [
Text(
method,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textTertiary,
),
),
],
),
SizedBox(width: 6),
Text(theme,
style: appStyle.fonts.B_16SB
.apply(color: appStyle.colors.textSecondary))
],
right: [
Text(method,
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textTertiary))
],
)));
),
);
}
if (nextLesson != null) {
@@ -237,9 +293,11 @@ class LessonWidget extends StatelessWidget {
if (breakMins > 45) {
final breakEnd = lesson.end.add(Duration(minutes: breakMins));
final emptyClass = seqSchedule.firstWhereOrNull((lesson2) =>
lesson2.start.isAfter(lesson.end) &&
lesson2.end.isBefore(breakEnd));
final emptyClass = seqSchedule.firstWhereOrNull(
(lesson2) =>
lesson2.start.isAfter(lesson.end) &&
lesson2.end.isBefore(breakEnd),
);
if (emptyClass != null) {
final preBreak = emptyClass.start.difference(lesson.end).inMinutes;
@@ -250,136 +308,177 @@ class LessonWidget extends StatelessWidget {
.subGroup("timetable_toast")
.boolean("breaks") &&
showBreak) {
elements.add(FirkaCard(
color: appStyle.colors.cardTranslucent,
shadow: false,
elements.add(
FirkaCard(
color: appStyle.colors.cardTranslucent,
shadow: false,
left: [
Text(
data.l10n.breakTxt,
style: appStyle.fonts.B_16SB.apply(
color: appStyle.colors.textSecondary,
),
),
],
right: [
Text(
"$preBreak ${preBreak == 1 ? data.l10n.starting_min : data.l10n.starting_min_plural}",
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textTertiary,
),
),
],
),
);
}
elements.add(
FirkaCard(
left: [
Text(data.l10n.breakTxt,
style: appStyle.fonts.B_16SB
.apply(color: appStyle.colors.textSecondary))
SizedBox(
width: 18,
height: 18,
child: Stack(
children: [
SvgPicture.asset(
"assets/icons/subtract.svg",
color: bgColor,
width: 18,
height: 18,
),
Padding(
padding: EdgeInsets.only(left: 5),
child: Text(
emptyClass.lessonNumber.toString(),
style: appStyle.fonts.B_12R.apply(color: secondary),
),
),
],
),
),
Transform.translate(
offset: Offset(-4, 0),
child: Card(
shadowColor: Colors.transparent,
color: bgColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: EdgeInsetsGeometry.all(4),
child: FirkaIconWidget(
FirkaIconType.majesticonsLocal,
'cupFilled',
color: appStyle.colors.accent,
size: 24,
),
),
),
),
SizedBox(width: 8),
Text(
data.l10n.empty_class,
style: appStyle.fonts.B_16SB.apply(
color: appStyle.colors.textPrimary,
),
),
],
right: [
Text(
"$preBreak ${preBreak == 1 ? data.l10n.starting_min : data.l10n.starting_min_plural}",
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textTertiary))
],
));
}
elements.add(FirkaCard(
left: [
SizedBox(
width: 18,
height: 18,
child: Stack(
children: [
SvgPicture.asset(
"assets/icons/subtract.svg",
color: bgColor,
width: 18,
height: 18,
),
Padding(
padding: EdgeInsets.only(left: 5),
child: Text(emptyClass.lessonNumber.toString(),
style: appStyle.fonts.B_12R.apply(color: secondary)),
)
],
),
),
Transform.translate(
offset: Offset(-4, 0),
child: Card(
shadowColor: Colors.transparent,
color: bgColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: EdgeInsetsGeometry.all(4),
child: FirkaIconWidget(
FirkaIconType.majesticonsLocal, 'cupFilled',
color: appStyle.colors.accent, size: 24),
),
),
),
SizedBox(width: 8),
Text(data.l10n.empty_class,
style: appStyle.fonts.B_16SB
.apply(color: appStyle.colors.textPrimary)),
],
right: [
Text(
isDismissed
? data.l10n.class_dismissed
: "${emptyClass.start.toLocal().format(data.l10n, FormatMode.hmm)} - ${emptyClass.end.toLocal().format(data.l10n, FormatMode.hmm)}",
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textPrimary))
],
));
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
),
],
),
);
if (data.settings
.group("settings")
.subGroup("timetable_toast")
.boolean("breaks") &&
showBreak) {
elements.add(FirkaCard(
color: appStyle.colors.cardTranslucent,
shadow: false,
left: [
Text(data.l10n.breakTxt,
style: appStyle.fonts.B_16SB
.apply(color: appStyle.colors.textSecondary))
],
right: [
Text(
elements.add(
FirkaCard(
color: appStyle.colors.cardTranslucent,
shadow: false,
left: [
Text(
data.l10n.breakTxt,
style: appStyle.fonts.B_16SB.apply(
color: appStyle.colors.textSecondary,
),
),
],
right: [
Text(
"$postBreak ${postBreak == 1 ? data.l10n.starting_min : data.l10n.starting_min_plural}",
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textTertiary))
],
));
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textTertiary,
),
),
],
),
);
}
} else if (data.settings
.group("settings")
.subGroup("timetable_toast")
.boolean("breaks") &&
showBreak) {
elements.add(FirkaCard(
color: appStyle.colors.cardTranslucent,
shadow: false,
left: [
Text(data.l10n.breakTxt,
style: appStyle.fonts.B_16SB
.apply(color: appStyle.colors.textSecondary))
],
right: [
Text(
elements.add(
FirkaCard(
color: appStyle.colors.cardTranslucent,
shadow: false,
left: [
Text(
data.l10n.breakTxt,
style: appStyle.fonts.B_16SB.apply(
color: appStyle.colors.textSecondary,
),
),
],
right: [
Text(
"$breakMins ${breakMins == 1 ? data.l10n.starting_min : data.l10n.starting_min_plural}",
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textTertiary))
],
));
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textTertiary,
),
),
],
),
);
}
} else if (data.settings
.group("settings")
.subGroup("timetable_toast")
.boolean("breaks") &&
showBreak) {
elements.add(FirkaCard(
color: appStyle.colors.cardTranslucent,
shadow: false,
left: [
Text(data.l10n.breakTxt,
style: appStyle.fonts.B_16SB
.apply(color: appStyle.colors.textSecondary))
],
right: [
Text(
elements.add(
FirkaCard(
color: appStyle.colors.cardTranslucent,
shadow: false,
left: [
Text(
data.l10n.breakTxt,
style: appStyle.fonts.B_16SB.apply(
color: appStyle.colors.textSecondary,
),
),
],
right: [
Text(
"$breakMins ${breakMins == 1 ? data.l10n.starting_min : data.l10n.starting_min_plural}",
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textTertiary))
],
));
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textTertiary,
),
),
],
),
);
}
}

View File

@@ -21,25 +21,36 @@ class LessonBigWidget extends StatelessWidget {
final List<Lesson> lessons;
final List<Test> tests;
const LessonBigWidget(this.l10n, this.now, this.lessonNo, this.lesson,
this.prevLesson, this.nextLesson, this.lessons, this.tests,
{super.key});
const LessonBigWidget(
this.l10n,
this.now,
this.lessonNo,
this.lesson,
this.prevLesson,
this.nextLesson,
this.lessons,
this.tests, {
super.key,
});
@override
Widget build(BuildContext context) {
var hasLesson = lesson != null;
var lessonsLeft =
lessons.where((lesson) => lesson.end.isAfter(now)).length;
var lessonsLeft = lessons.where((lesson) => lesson.end.isAfter(now)).length;
var hasPrevLesson = prevLesson != null;
var hasNextLesson = nextLesson != null;
// TODO: holnapi órák száma kiszámolás
var lessonsTomorrow = 0;
var testsTomorrow = tests.where((test) =>
test.date.isAfter(now) &&
test.date.isBefore(DateTime(now.year, now.month, now.day + 2))).length;
if (lessonsLeft < 1){
var testsTomorrow = tests
.where(
(test) =>
test.date.isAfter(now) &&
test.date.isBefore(DateTime(now.year, now.month, now.day + 2)),
)
.length;
if (lessonsLeft < 1) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
@@ -58,7 +69,9 @@ class LessonBigWidget extends StatelessWidget {
Card(
shadowColor: Colors.transparent,
color: appStyle.colors.a15p,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: Padding(
padding: EdgeInsets.all(6),
child: FirkaIconWidget(
@@ -74,14 +87,16 @@ class LessonBigWidget extends StatelessWidget {
),
Text(
testsTomorrow == 0
? l10n.tt_no_classes_l2
: l10n.get_ready,
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textPrimary)),
? l10n.tt_no_classes_l2
: l10n.get_ready,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
),
],
)
),
],
)
),
],
extra: Column(
children: [
@@ -102,10 +117,10 @@ class LessonBigWidget extends StatelessWidget {
Padding(
padding: EdgeInsets.all(2),
child: FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.editPen4Solid,
size: 32.0,
color: appStyle.colors.accent,
FirkaIconType.majesticons,
Majesticon.editPen4Solid,
size: 32.0,
color: appStyle.colors.accent,
),
),
],
@@ -113,24 +128,27 @@ class LessonBigWidget extends StatelessWidget {
),
SizedBox(width: 8),
Text(
(lessonsTomorrow == 0 && testsTomorrow == 0)
? l10n.no_tests_tomorrow
: (testsTomorrow > 1)
(lessonsTomorrow == 0 && testsTomorrow == 0)
? l10n.no_tests_tomorrow
: (testsTomorrow > 1)
? l10n.tests_tomorrow(testsTomorrow.toString())
: (testsTomorrow < 1 && lessonsTomorrow > 0)
? l10n.lessons_tomorrow(lessonsTomorrow.toString())
: l10n.tests_tomorrow(testsTomorrow.toString()),
textAlign: TextAlign.left,
style: appStyle.fonts.B_16R.apply(
? l10n.lessons_tomorrow(
lessonsTomorrow.toString(),
)
: l10n.tests_tomorrow(testsTomorrow.toString()),
textAlign: TextAlign.left,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
),
])
],
),
),
),
],
)
)
),
),
],
);
}
@@ -152,36 +170,52 @@ class LessonBigWidget extends StatelessWidget {
child: Padding(
padding: EdgeInsets.all(4),
child: FirkaIconWidget(
FirkaIconType.majesticons, 'cupFilled',
color: appStyle.colors.accent, size: 24),
FirkaIconType.majesticons,
'cupFilled',
color: appStyle.colors.accent,
size: 24,
),
),
),
Text(
l10n.breakTxt,
style: appStyle.fonts.B_16SB.apply(
color: appStyle.colors.textPrimary,
),
),
Text(l10n.breakTxt,
style: appStyle.fonts.B_16SB
.apply(color: appStyle.colors.textPrimary)),
],
),
],
)
),
],
right: [
Column(
children: [
Row(children: [
Text('-',
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textPrimary))
]),
Row(children: [
Text('-',
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textPrimary))
])
Row(
children: [
Text(
'-',
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
),
],
),
Row(
children: [
Text(
'-',
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
),
],
),
],
)
),
],
extra: SizedBox.shrink(),
)
),
],
);
}
@@ -208,46 +242,65 @@ class LessonBigWidget extends StatelessWidget {
child: Padding(
padding: EdgeInsets.all(4),
child: FirkaIconWidget(
FirkaIconType.majesticonsLocal, 'cupFilled',
color: appStyle.colors.accent, size: 24),
FirkaIconType.majesticonsLocal,
'cupFilled',
color: appStyle.colors.accent,
size: 24,
),
),
),
Text(
l10n.breakTxt,
style: appStyle.fonts.B_16SB.apply(
color: appStyle.colors.textPrimary,
),
),
Text(l10n.breakTxt,
style: appStyle.fonts.B_16SB
.apply(color: appStyle.colors.textPrimary)),
],
),
Row(
children: [
Text(timeLeftStr,
style: appStyle.fonts.B_12R
.apply(color: appStyle.colors.textSecondary)),
Text(
timeLeftStr,
style: appStyle.fonts.B_12R.apply(
color: appStyle.colors.textSecondary,
),
),
],
),
],
)
),
],
right: [
Column(
children: [
Row(children: [
Text('-',
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textPrimary))
]),
Row(children: [
Text(
nextLesson!.start
.toLocal()
.format(l10n, FormatMode.hmm),
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textPrimary))
])
Row(
children: [
Text(
'-',
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
),
],
),
Row(
children: [
Text(
nextLesson!.start.toLocal().format(
l10n,
FormatMode.hmm,
),
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
),
],
),
],
)
),
],
extra: SizedBox.shrink(),
)
),
],
);
}
@@ -302,76 +355,96 @@ class LessonBigWidget extends StatelessWidget {
),
Padding(
padding: EdgeInsets.only(left: 5),
child: Text(lessonNo.toString(),
style: appStyle.fonts.B_12R
.apply(color: appStyle.colors.secondary)),
)
child: Text(
lessonNo.toString(),
style: appStyle.fonts.B_12R.apply(
color: appStyle.colors.secondary,
),
),
),
],
),
),
Transform.translate(
offset: Offset(-4, 0),
child: Card(
shadowColor: Colors.transparent,
color: appStyle.colors.a15p,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: EdgeInsets.all(4),
child: ClassIconWidget(
color: appStyle.colors.accent,
size: 24,
uid: lesson!.uid,
className: lesson!.name,
category: lesson!.subject?.name ?? '',
),
offset: Offset(-4, 0),
child: Card(
shadowColor: Colors.transparent,
color: appStyle.colors.a15p,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: EdgeInsets.all(4),
child: ClassIconWidget(
color: appStyle.colors.accent,
size: 24,
uid: lesson!.uid,
className: lesson!.name,
category: lesson!.subject?.name ?? '',
),
)),
Text(lesson!.subject?.name ?? 'N/A',
style: appStyle.fonts.B_16SB
.apply(color: appStyle.colors.textPrimary)),
),
),
),
Text(
lesson!.subject?.name ?? 'N/A',
style: appStyle.fonts.B_16SB.apply(
color: appStyle.colors.textPrimary,
),
),
],
),
Row(
children: [
Text(timeLeftStr,
style: appStyle.fonts.B_12R
.apply(color: appStyle.colors.textSecondary)),
Text(
timeLeftStr,
style: appStyle.fonts.B_12R.apply(
color: appStyle.colors.textSecondary,
),
),
],
),
],
)
),
],
right: [
Column(
children: [
Row(
children: [
Text(lesson!.start.toLocal().format(l10n, FormatMode.hmm),
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textPrimary)),
Text(
lesson!.start.toLocal().format(l10n, FormatMode.hmm),
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
),
Card(
shadowColor: Colors.transparent,
color: appStyle.colors.a15p,
child: Padding(
padding: EdgeInsets.all(4),
child: Text(lesson!.roomName ?? '?',
style: appStyle.fonts.B_12R
.apply(color: appStyle.colors.secondary)),
child: Text(
lesson!.roomName ?? '?',
style: appStyle.fonts.B_12R.apply(
color: appStyle.colors.secondary,
),
),
),
)
),
],
),
Row(
children: [
SizedBox(width: 18),
Text(lesson!.end.toLocal().format(l10n, FormatMode.hmm),
style: appStyle.fonts.B_12R
.apply(color: appStyle.colors.textSecondary)),
Text(
lesson!.end.toLocal().format(l10n, FormatMode.hmm),
style: appStyle.fonts.B_12R.apply(
color: appStyle.colors.textSecondary,
),
),
],
)
),
],
)
),
],
extra: ClipRRect(
borderRadius: BorderRadius.circular(8),
@@ -382,12 +455,13 @@ class LessonBigWidget extends StatelessWidget {
minHeight: 8,
),
),
)
),
],
);
} else {
var duration =
nextLesson!.start.difference(prevLesson!.end).inMilliseconds;
var duration = nextLesson!.start
.difference(prevLesson!.end)
.inMilliseconds;
var progress =
duration - nextLesson!.start.difference(now).inMilliseconds;
var timeLeft = nextLesson!.start.difference(now);
@@ -410,24 +484,33 @@ class LessonBigWidget extends StatelessWidget {
child: Padding(
padding: EdgeInsets.all(4),
child: FirkaIconWidget(
FirkaIconType.majesticonsLocal, 'cupFilled',
color: appStyle.colors.accent, size: 24),
FirkaIconType.majesticonsLocal,
'cupFilled',
color: appStyle.colors.accent,
size: 24,
),
),
),
Text(
l10n.breakTxt,
style: appStyle.fonts.B_16SB.apply(
color: appStyle.colors.textPrimary,
),
),
Text(l10n.breakTxt,
style: appStyle.fonts.B_16SB
.apply(color: appStyle.colors.textPrimary)),
],
),
Row(
children: [
Text(timeLeftStr,
style: appStyle.fonts.B_12R
.apply(color: appStyle.colors.textSecondary)),
Text(
timeLeftStr,
style: appStyle.fonts.B_12R.apply(
color: appStyle.colors.textSecondary,
),
),
],
),
],
)
),
],
right: [
Column(
@@ -435,25 +518,28 @@ class LessonBigWidget extends StatelessWidget {
Row(
children: [
Text(
prevLesson!.end
.toLocal()
.format(l10n, FormatMode.hmm),
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textPrimary)),
prevLesson!.end.toLocal().format(l10n, FormatMode.hmm),
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
),
],
),
Row(
children: [
Text(
nextLesson!.start
.toLocal()
.format(l10n, FormatMode.hmm),
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textPrimary)),
nextLesson!.start.toLocal().format(
l10n,
FormatMode.hmm,
),
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
),
],
)
),
],
)
),
],
extra: LinearProgressIndicator(
// TODO: Make this rounded
@@ -461,7 +547,7 @@ class LessonBigWidget extends StatelessWidget {
backgroundColor: appStyle.colors.a15p,
color: appStyle.colors.accent,
),
)
),
],
);
}

View File

@@ -12,8 +12,12 @@ class LessonSmallWidget extends StatelessWidget {
final Lesson lesson;
final bool lessonActive;
const LessonSmallWidget(this.l10n, this.lesson, this.lessonActive,
{super.key});
const LessonSmallWidget(
this.l10n,
this.lesson,
this.lessonActive, {
super.key,
});
@override
Widget build(BuildContext context) {
@@ -44,9 +48,12 @@ class LessonSmallWidget extends StatelessWidget {
: '',
),
SizedBox(width: 8),
Text(subjectName,
style: appStyle.fonts.B_16SB
.apply(color: appStyle.colors.textPrimary)),
Text(
subjectName,
style: appStyle.fonts.B_16SB.apply(
color: appStyle.colors.textPrimary,
),
),
],
right: [
Card(
@@ -54,17 +61,22 @@ class LessonSmallWidget extends StatelessWidget {
color: appStyle.colors.a15p,
child: Padding(
padding: EdgeInsets.all(4),
child: Text(roomName,
style: appStyle.fonts.B_12R
.apply(color: appStyle.colors.secondary)),
child: Text(
roomName,
style: appStyle.fonts.B_12R.apply(
color: appStyle.colors.secondary,
),
),
),
),
Text(
"${lesson.start.toLocal().format(l10n, FormatMode.hmm)} - ${lesson.end.toLocal().format(l10n, FormatMode.hmm)}",
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textPrimary)),
"${lesson.start.toLocal().format(l10n, FormatMode.hmm)} - ${lesson.end.toLocal().format(l10n, FormatMode.hmm)}",
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
),
],
)
),
],
);
}

View File

@@ -5,7 +5,6 @@ import 'package:firka/helpers/db/models/app_settings_model.dart';
import 'package:firka/helpers/live_activity_service.dart';
import 'package:firka/main.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:isar_community/isar.dart';
import 'package:webview_flutter/webview_flutter.dart';
@@ -25,8 +24,12 @@ class LoginWebviewWidget extends StatefulWidget {
final String? username;
final String? schoolId;
const LoginWebviewWidget(this.data,
{super.key, this.username, this.schoolId});
const LoginWebviewWidget(
this.data, {
super.key,
this.username,
this.schoolId,
});
@override
State<LoginWebviewWidget> createState() => _LoginWebviewWidgetState();
@@ -48,14 +51,18 @@ class _LoginWebviewWidgetState extends FirkaState<LoginWebviewWidget>
vsync: this,
);
_fadeAnimation =
Tween<double>(begin: 1.0, end: 0.0).animate(_fadeAnimationController!);
_fadeAnimation = Tween<double>(
begin: 1.0,
end: 0.0,
).animate(_fadeAnimationController!);
var loginUrl = KretaEndpoints.kretaLoginUrl;
if (widget.username != null && widget.schoolId != null) {
loginUrl = KretaEndpoints.kretaLoginUrlRefresh(
widget.username!, widget.schoolId!);
widget.username!,
widget.schoolId!,
);
}
logger.info("Using loginUrl: $loginUrl");
@@ -63,7 +70,8 @@ class _LoginWebviewWidgetState extends FirkaState<LoginWebviewWidget>
_webViewController = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..loadRequest(Uri.parse(loginUrl))
..setNavigationDelegate(NavigationDelegate(
..setNavigationDelegate(
NavigationDelegate(
onPageFinished: (String url) {
Timer(const Duration(milliseconds: 500), () {
if (mounted) {
@@ -83,90 +91,98 @@ class _LoginWebviewWidgetState extends FirkaState<LoginWebviewWidget>
_fadeAnimationController?.reset();
},
onNavigationRequest: (NavigationRequest request) async {
var uri = Uri.parse(request.url);
var uri = Uri.parse(request.url);
if (uri.path == "/ellenorzo-student/prod/oauthredirect") {
var code = uri.queryParameters["code"]!;
if (uri.path == "/ellenorzo-student/prod/oauthredirect") {
var code = uri.queryParameters["code"]!;
try {
var isar = widget.data.isar;
var resp = await getAccessToken(code);
try {
var isar = widget.data.isar;
var resp = await getAccessToken(code);
logger.info("getAccessToken(): $resp");
logger.info("getAccessToken(): $resp");
var tokenModel = TokenModel.fromResp(resp);
var tokenModel = TokenModel.fromResp(resp);
final accountPicker = (widget.data.settings
.group("profile_settings")["e_kreta_account_picker"]
as SettingsKretenAccountPicker);
final accountPicker =
(widget.data.settings.group(
"profile_settings",
)["e_kreta_account_picker"]
as SettingsKretenAccountPicker);
var tokenId = 0;
var om = 0;
await isar.writeTxn(() async {
om = await isar.tokenModels.put(tokenModel);
});
var tokenId = 0;
var om = 0;
await isar.writeTxn(() async {
om = await isar.tokenModels.put(tokenModel);
});
widget.data.tokens = await isar.tokenModels.where().findAll();
for (var i = 0; i < widget.data.tokens.length; i++) {
if (widget.data.tokens[i].studentIdNorm == om) {
tokenId = i;
break;
}
}
await isar.writeTxn(() async {
accountPicker.accountIndex = tokenId;
await accountPicker.save(widget.data.isar.appSettingsModels);
});
await accountPicker.postUpdate();
if (Platform.isIOS) {
final watchInstalled =
await WatchSyncHelper.isWatchAppInstalled();
if (watchInstalled) {
try {
await WatchSyncHelper.saveTokenToiCloud(tokenModel);
} catch (_) {}
try {
await WatchSyncHelper.sendTokenToWatch();
} catch (_) {
// Watch may be unavailable, ignore
widget.data.tokens = await isar.tokenModels.where().findAll();
for (var i = 0; i < widget.data.tokens.length; i++) {
if (widget.data.tokens[i].studentIdNorm == om) {
tokenId = i;
break;
}
}
}
}
if (!mounted) return NavigationDecision.prevent;
await isar.writeTxn(() async {
accountPicker.accountIndex = tokenId;
await accountPicker.save(widget.data.isar.appSettingsModels);
});
KretaClient.clearReauthFlag();
if (Platform.isIOS) {
LiveActivityService.clearTokenExpiration();
}
await accountPicker.postUpdate();
runApp(InitializationScreen());
} catch (ex) {
if (ex is Error) {
logger.shout(
"oauthredirect failed:", ex.toString(), ex.stackTrace);
} else {
logger.shout("oauthredirect failed:", ex.toString());
}
Navigator.push(
context,
MaterialPageRoute(
if (Platform.isIOS) {
final watchInstalled =
await WatchSyncHelper.isWatchAppInstalled();
if (watchInstalled) {
try {
await WatchSyncHelper.saveTokenToiCloud(tokenModel);
} catch (_) {}
try {
await WatchSyncHelper.sendTokenToWatch();
} catch (_) {
// Watch may be unavailable, ignore
}
}
}
if (!mounted) return NavigationDecision.prevent;
KretaClient.clearReauthFlag();
if (Platform.isIOS) {
LiveActivityService.clearTokenExpiration();
}
runApp(InitializationScreen());
} catch (ex) {
if (ex is Error) {
logger.shout(
"oauthredirect failed:",
ex.toString(),
ex.stackTrace,
);
} else {
logger.shout("oauthredirect failed:", ex.toString());
}
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DefaultAssetBundle(
bundle: FirkaBundle(),
child: ErrorPage(
exception: ex.toString(),
))));
}
bundle: FirkaBundle(),
child: ErrorPage(exception: ex.toString()),
),
),
);
}
return NavigationDecision.prevent;
}
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
}));
return NavigationDecision.navigate;
},
),
);
}
@override
@@ -175,14 +191,14 @@ class _LoginWebviewWidgetState extends FirkaState<LoginWebviewWidget>
super.dispose();
}
@override
Widget build(BuildContext context) {
return Material(
color: appStyle.colors.card,
child: Padding(
padding:
EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: FractionallySizedBox(
heightFactor: 0.90,
child: Center(
@@ -198,7 +214,9 @@ class _LoginWebviewWidgetState extends FirkaState<LoginWebviewWidget>
padding: const EdgeInsets.symmetric(vertical: 16),
child: Container(
decoration: BoxDecoration(
color: appStyle.colors.secondary.withValues(alpha: 0.5),
color: appStyle.colors.secondary.withValues(
alpha: 0.5,
),
borderRadius: BorderRadius.all(Radius.circular(2)),
),
width: 40,
@@ -216,18 +234,17 @@ class _LoginWebviewWidgetState extends FirkaState<LoginWebviewWidget>
borderRadius: BorderRadius.circular(20),
child: Stack(
children: [
WebViewWidget(
controller: _webViewController,
),
if (_fadeAnimationController != null && _fadeAnimation != null)
WebViewWidget(controller: _webViewController),
if (_fadeAnimationController != null &&
_fadeAnimation != null)
AnimatedBuilder(
animation: _fadeAnimationController!,
builder: (context, child) => AnimatedOpacity(
opacity: _isLoading
? 1.0
: _fadeAnimationController!.isAnimating
? _fadeAnimation!.value
: 0.0,
? _fadeAnimation!.value
: 0.0,
duration: const Duration(milliseconds: 500),
child: Container(
color: appStyle.colors.background,

View File

@@ -16,11 +16,18 @@ class TimeTableDayWidget extends StatelessWidget {
final List<Lesson> lessons;
final List<Lesson> events;
final List<Test> tests;
final List<Lesson> day;
final List<Lesson> day;
const TimeTableDayWidget(
this.data, this.date, this.week, this.lessons, this.events, this.tests, this.day,
{super.key});
this.data,
this.date,
this.week,
this.lessons,
this.events,
this.tests,
this.day, {
super.key,
});
@override
Widget build(BuildContext context) {
@@ -29,47 +36,72 @@ class TimeTableDayWidget extends StatelessWidget {
if (lessons.isEmpty) {
noLessonsWidget = Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SvgPicture.asset("assets/images/logos/dave.svg",
width: 48, height: 48),
SizedBox(height: 12),
Text(data.l10n.tt_no_classes_l1,
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textSecondary)),
Text(data.l10n.tt_no_classes_l2,
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textSecondary)),
if (events.isNotEmpty)
...events.map((event) =>
Center(
child: Text(event.name.replaceAll(" (Nem órarendi nap)", ""),
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textSecondary)),
))
]);
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SvgPicture.asset(
"assets/images/logos/dave.svg",
width: 48,
height: 48,
),
SizedBox(height: 12),
Text(
data.l10n.tt_no_classes_l1,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
),
Text(
data.l10n.tt_no_classes_l2,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
),
if (events.isNotEmpty)
...events.map(
(event) => Center(
child: Text(
event.name.replaceAll(" (Nem órarendi nap)", ""),
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
),
),
),
],
);
} else {
for (var i = 0; i < events.length; i++) {
var event = events[i];
ttBody.add(FirkaCard(left: [
Text(event.name,
style: appStyle.fonts.B_16R
.apply(color: appStyle.colors.textPrimary))
]));
ttBody.add(
FirkaCard(
left: [
Text(
event.name,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
),
],
),
);
}
for (var i = 0; i < lessons.length; i++) {
var lesson = lessons[i];
Lesson? nextLesson = lessons.length > i + 1 ? lessons[i + 1] : null;
ttBody.add(LessonWidget(
ttBody.add(
LessonWidget(
data,
week,
day,
lessons.getLessonNo(lesson),
lesson,
tests.firstWhereOrNull(
(test) => test.lessonNumber == lesson.lessonNumber),
nextLesson));
(test) => test.lessonNumber == lesson.lessonNumber,
),
nextLesson,
),
);
}
}
@@ -78,18 +110,16 @@ class TimeTableDayWidget extends StatelessWidget {
child: ttBody.isEmpty
? noLessonsWidget
: Padding(
padding:
const EdgeInsets.only(top: 70 + 16 + 20, left: 4, right: 4),
padding: const EdgeInsets.only(
top: 70 + 16 + 20,
left: 4,
right: 4,
),
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...ttBody,
SizedBox(
height: 24,
)
],
children: [...ttBody, SizedBox(height: 24)],
),
),
),

View File

@@ -10,22 +10,26 @@ class ClassIconWidget extends StatelessWidget {
final Color color;
final double? size;
const ClassIconWidget(
{super.key,
required String uid,
required String className,
required String category,
this.color = Colors.white,
this.size})
: _className = className,
_uid = uid,
_category = category;
const ClassIconWidget({
super.key,
required String uid,
required String className,
required String category,
this.color = Colors.white,
this.size,
}) : _className = className,
_uid = uid,
_category = category;
@override
Widget build(BuildContext context) {
var iconCategory = getIconType(_uid, _className, _category);
return FirkaIconWidget(FirkaIconType.majesticons, getIconData(iconCategory),
color: color, size: size);
return FirkaIconWidget(
FirkaIconType.majesticons,
getIconData(iconCategory),
color: color,
size: size,
);
}
}

View File

@@ -14,10 +14,7 @@ class CounterDigitWidget extends StatelessWidget {
color: appStyle.colors.buttonSecondaryFill,
child: Padding(
padding: EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 4),
child: Text(
c,
style: style,
),
child: Text(c, style: style),
),
);
}

View File

@@ -30,9 +30,7 @@ class _DelayedSpinner extends FirkaState<DelayedSpinnerWidget> {
@override
Widget build(BuildContext context) {
if (showSpinner) {
return CircularProgressIndicator(
color: appStyle.colors.accent,
);
return CircularProgressIndicator(color: appStyle.colors.accent);
} else {
return SizedBox();
}

View File

@@ -4,11 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:majesticons_flutter/majesticons_flutter.dart';
enum FirkaIconType {
icons,
majesticons,
majesticonsLocal,
}
enum FirkaIconType { icons, majesticons, majesticonsLocal }
class FirkaIconWidget extends StatelessWidget {
final FirkaIconType iconType;
@@ -16,8 +12,13 @@ class FirkaIconWidget extends StatelessWidget {
final Color color;
final double? size;
const FirkaIconWidget(this.iconType, this.iconData,
{super.key, this.color = Colors.white, this.size});
const FirkaIconWidget(
this.iconType,
this.iconData, {
super.key,
this.color = Colors.white,
this.size,
});
@override
Widget build(BuildContext context) {

View File

@@ -12,41 +12,50 @@ class GradeSmallCard extends FirkaCard {
final Subject subject;
GradeSmallCard(this.grades, this.subject, {super.key})
: super(left: [
: super(
left: [
ClassIconWidget(
uid: subject.uid,
className: subject.name,
category: subject.category.name!,
color: appStyle.colors.accent,
),
SizedBox(
width: 4,
),
SizedBox(width: 4),
SizedBox(
width: 200,
child: Text(
subject.name,
style: appStyle.fonts.B_16SB
.apply(color: appStyle.colors.textPrimary),
style: appStyle.fonts.B_16SB.apply(
color: appStyle.colors.textPrimary,
),
),
),
], right: [
],
right: [
grades.getAverageBySubject(subject).isNaN
? SizedBox()
: Card(
shadowColor: Colors.transparent,
color: getGradeColor(grades.getAverageBySubject(subject))
.withAlpha(38),
color: getGradeColor(
grades.getAverageBySubject(subject),
).withAlpha(38),
child: Padding(
padding:
EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 4),
padding: EdgeInsets.only(
left: 8,
right: 8,
top: 4,
bottom: 4,
),
child: Text(
grades.getAverageBySubject(subject).toStringAsFixed(2),
style: appStyle.fonts.B_16SB.apply(
color: getGradeColor(
grades.getAverageBySubject(subject))),
color: getGradeColor(
grades.getAverageBySubject(subject),
),
),
),
),
),
]);
],
);
}

View File

@@ -50,6 +50,7 @@ dependencies:
confetti: ^0.8.0
flutter_html: ^3.0.0
fl_chart: ^1.1.1
flutter_native_splash: ^2.4.7
dev_dependencies:
flutter_test:
@@ -59,7 +60,6 @@ dev_dependencies:
android_notification_icons: ^0.0.1
integration_test:
sdk: flutter
flutter_native_splash: ^2.4.7
android_notification_icons:
image_path: 'assets/images/logos/dave_monochrome.png'