This commit is contained in:
Márton Kiss
2023-05-26 21:25:00 +02:00
parent 9e3b805fdd
commit 1558794e93
528 changed files with 38239 additions and 37732 deletions

View File

@@ -1,165 +1,165 @@
import 'dart:convert';
import 'dart:developer';
import 'package:filcnaplo/models/config.dart';
import 'package:filcnaplo/models/news.dart';
import 'package:filcnaplo/models/release.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/models/supporter.dart';
import 'package:filcnaplo_kreta_api/models/school.dart';
import 'package:http/http.dart' as http;
import 'package:connectivity_plus/connectivity_plus.dart';
class FilcAPI {
// Public API
static const schoolList = "https://filcnaplo.hu/v2/school_list.json";
static const news = "https://filcnaplo.hu/v2/news.json";
static const supporters = "https://api.filcnaplo.hu/sponsors";
// Private API
static const config = "https://api.filcnaplo.hu/config";
static const reportApi = "https://api.filcnaplo.hu/report";
static const premiumApi = "https://api.filcnaplo.hu/premium/activate";
// static const premiumScopesApi = "https://api.filcnaplo.hu/premium/scopes";
// Updates
static const repo = "filc/naplo";
static const releases = "https://api.github.com/repos/$repo/releases";
static Future<bool> checkConnectivity() async => (await Connectivity().checkConnectivity()) != ConnectivityResult.none;
static Future<List<School>?> getSchools() async {
try {
http.Response res = await http.get(Uri.parse(schoolList));
if (res.statusCode == 200) {
List<School> schools = (jsonDecode(res.body) as List).cast<Map>().map((json) => School.fromJson(json)).toList();
schools.add(School(
city: "Tiszabura",
instituteCode: "supporttest-reni-tiszabura-teszt01",
name: "FILC Éles Reni tiszabura-teszt",
));
return schools;
} else {
throw "HTTP ${res.statusCode}: ${res.body}";
}
} on Exception catch (error, stacktrace) {
log("ERROR: FilcAPI.getSchools: $error $stacktrace");
}
return null;
}
static Future<Config?> getConfig(SettingsProvider settings) async {
final userAgent = SettingsProvider.defaultSettings().config.userAgent;
Map<String, String> headers = {
"x-filc-id": settings.xFilcId,
"user-agent": userAgent,
};
log("[CONFIG] x-filc-id: \"${settings.xFilcId}\"");
log("[CONFIG] user-agent: \"$userAgent\"");
try {
http.Response res = await http.get(Uri.parse(config), headers: headers);
if (res.statusCode == 200) {
return Config.fromJson(jsonDecode(res.body));
} else if (res.statusCode == 429) {
res = await http.get(Uri.parse(config));
if (res.statusCode == 200) return Config.fromJson(jsonDecode(res.body));
}
throw "HTTP ${res.statusCode}: ${res.body}";
} on Exception catch (error, stacktrace) {
log("ERROR: FilcAPI.getConfig: $error $stacktrace");
}
return null;
}
static Future<List<News>?> getNews() async {
try {
http.Response res = await http.get(Uri.parse(news));
if (res.statusCode == 200) {
return (jsonDecode(res.body) as List).cast<Map>().map((e) => News.fromJson(e)).toList();
} else {
throw "HTTP ${res.statusCode}: ${res.body}";
}
} on Exception catch (error, stacktrace) {
log("ERROR: FilcAPI.getNews: $error $stacktrace");
}
return null;
}
static Future<Supporters?> getSupporters() async {
try {
http.Response res = await http.get(Uri.parse(supporters));
if (res.statusCode == 200) {
return Supporters.fromJson(jsonDecode(res.body));
} else {
throw "HTTP ${res.statusCode}: ${res.body}";
}
} on Exception catch (error, stacktrace) {
log("ERROR: FilcAPI.getSupporters: $error $stacktrace");
}
return null;
}
static Future<List<Release>?> getReleases() async {
try {
http.Response res = await http.get(Uri.parse(releases));
if (res.statusCode == 200) {
return (jsonDecode(res.body) as List).cast<Map>().map((e) => Release.fromJson(e)).toList();
} else {
throw "HTTP ${res.statusCode}: ${res.body}";
}
} on Exception catch (error, stacktrace) {
log("ERROR: FilcAPI.getReleases: $error $stacktrace");
}
return null;
}
static Future<http.StreamedResponse?> downloadRelease(ReleaseDownload release) {
try {
var client = http.Client();
var request = http.Request('GET', Uri.parse(release.url));
return client.send(request);
} on Exception catch (error, stacktrace) {
log("ERROR: FilcAPI.downloadRelease: $error $stacktrace");
return Future.value(null);
}
}
static Future<void> sendReport(ErrorReport report) async {
try {
http.Response res = await http.post(Uri.parse(reportApi), body: {
"os": report.os,
"version": report.version,
"error": report.error,
"stack_trace": report.stack,
});
if (res.statusCode != 200) {
throw "HTTP ${res.statusCode}: ${res.body}";
}
} on Exception catch (error, stacktrace) {
log("ERROR: FilcAPI.sendReport: $error $stacktrace");
}
}
}
class ErrorReport {
String stack;
String os;
String version;
String error;
ErrorReport({
required this.stack,
required this.os,
required this.version,
required this.error,
});
}
import 'dart:convert';
import 'dart:developer';
import 'package:filcnaplo/models/config.dart';
import 'package:filcnaplo/models/news.dart';
import 'package:filcnaplo/models/release.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/models/supporter.dart';
import 'package:filcnaplo_kreta_api/models/school.dart';
import 'package:http/http.dart' as http;
import 'package:connectivity_plus/connectivity_plus.dart';
class FilcAPI {
// Public API
static const schoolList = "https://filcnaplo.hu/v2/school_list.json";
static const news = "https://filcnaplo.hu/v2/news.json";
static const supporters = "https://api.filcnaplo.hu/sponsors";
// Private API
static const config = "https://api.filcnaplo.hu/config";
static const reportApi = "https://api.filcnaplo.hu/report";
static const premiumApi = "https://api.filcnaplo.hu/premium/activate";
// static const premiumScopesApi = "https://api.filcnaplo.hu/premium/scopes";
// Updates
static const repo = "filc/naplo";
static const releases = "https://api.github.com/repos/$repo/releases";
static Future<bool> checkConnectivity() async => (await Connectivity().checkConnectivity()) != ConnectivityResult.none;
static Future<List<School>?> getSchools() async {
try {
http.Response res = await http.get(Uri.parse(schoolList));
if (res.statusCode == 200) {
List<School> schools = (jsonDecode(res.body) as List).cast<Map>().map((json) => School.fromJson(json)).toList();
schools.add(School(
city: "Tiszabura",
instituteCode: "supporttest-reni-tiszabura-teszt01",
name: "FILC Éles Reni tiszabura-teszt",
));
return schools;
} else {
throw "HTTP ${res.statusCode}: ${res.body}";
}
} on Exception catch (error, stacktrace) {
log("ERROR: FilcAPI.getSchools: $error $stacktrace");
}
return null;
}
static Future<Config?> getConfig(SettingsProvider settings) async {
final userAgent = SettingsProvider.defaultSettings().config.userAgent;
Map<String, String> headers = {
"x-filc-id": settings.xFilcId,
"user-agent": userAgent,
};
log("[CONFIG] x-filc-id: \"${settings.xFilcId}\"");
log("[CONFIG] user-agent: \"$userAgent\"");
try {
http.Response res = await http.get(Uri.parse(config), headers: headers);
if (res.statusCode == 200) {
return Config.fromJson(jsonDecode(res.body));
} else if (res.statusCode == 429) {
res = await http.get(Uri.parse(config));
if (res.statusCode == 200) return Config.fromJson(jsonDecode(res.body));
}
throw "HTTP ${res.statusCode}: ${res.body}";
} on Exception catch (error, stacktrace) {
log("ERROR: FilcAPI.getConfig: $error $stacktrace");
}
return null;
}
static Future<List<News>?> getNews() async {
try {
http.Response res = await http.get(Uri.parse(news));
if (res.statusCode == 200) {
return (jsonDecode(res.body) as List).cast<Map>().map((e) => News.fromJson(e)).toList();
} else {
throw "HTTP ${res.statusCode}: ${res.body}";
}
} on Exception catch (error, stacktrace) {
log("ERROR: FilcAPI.getNews: $error $stacktrace");
}
return null;
}
static Future<Supporters?> getSupporters() async {
try {
http.Response res = await http.get(Uri.parse(supporters));
if (res.statusCode == 200) {
return Supporters.fromJson(jsonDecode(res.body));
} else {
throw "HTTP ${res.statusCode}: ${res.body}";
}
} on Exception catch (error, stacktrace) {
log("ERROR: FilcAPI.getSupporters: $error $stacktrace");
}
return null;
}
static Future<List<Release>?> getReleases() async {
try {
http.Response res = await http.get(Uri.parse(releases));
if (res.statusCode == 200) {
return (jsonDecode(res.body) as List).cast<Map>().map((e) => Release.fromJson(e)).toList();
} else {
throw "HTTP ${res.statusCode}: ${res.body}";
}
} on Exception catch (error, stacktrace) {
log("ERROR: FilcAPI.getReleases: $error $stacktrace");
}
return null;
}
static Future<http.StreamedResponse?> downloadRelease(ReleaseDownload release) {
try {
var client = http.Client();
var request = http.Request('GET', Uri.parse(release.url));
return client.send(request);
} on Exception catch (error, stacktrace) {
log("ERROR: FilcAPI.downloadRelease: $error $stacktrace");
return Future.value(null);
}
}
static Future<void> sendReport(ErrorReport report) async {
try {
http.Response res = await http.post(Uri.parse(reportApi), body: {
"os": report.os,
"version": report.version,
"error": report.error,
"stack_trace": report.stack,
});
if (res.statusCode != 200) {
throw "HTTP ${res.statusCode}: ${res.body}";
}
} on Exception catch (error, stacktrace) {
log("ERROR: FilcAPI.sendReport: $error $stacktrace");
}
}
}
class ErrorReport {
String stack;
String os;
String version;
String error;
ErrorReport({
required this.stack,
required this.os,
required this.version,
required this.error,
});
}

View File

@@ -1,115 +1,115 @@
// ignore_for_file: avoid_print, use_build_context_synchronously
import 'package:filcnaplo/utils/jwt.dart';
import 'package:filcnaplo_kreta_api/providers/absence_provider.dart';
import 'package:filcnaplo_kreta_api/providers/event_provider.dart';
import 'package:filcnaplo_kreta_api/providers/exam_provider.dart';
import 'package:filcnaplo_kreta_api/providers/grade_provider.dart';
import 'package:filcnaplo_kreta_api/providers/homework_provider.dart';
import 'package:filcnaplo_kreta_api/providers/message_provider.dart';
import 'package:filcnaplo_kreta_api/providers/note_provider.dart';
import 'package:filcnaplo_kreta_api/providers/timetable_provider.dart';
import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/models/user.dart';
import 'package:filcnaplo_kreta_api/client/api.dart';
import 'package:filcnaplo_kreta_api/client/client.dart';
import 'package:filcnaplo_kreta_api/models/student.dart';
import 'package:filcnaplo_kreta_api/models/week.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:filcnaplo/api/nonce.dart';
enum LoginState { missingFields, invalidGrant, failed, normal, inProgress, success }
Nonce getNonce(String nonce, String username, String instituteCode) {
Nonce nonceEncoder = Nonce(key: [98, 97, 83, 115, 120, 79, 119, 108, 85, 49, 106, 77], nonce: nonce);
nonceEncoder.encode(instituteCode.toUpperCase() + nonce + username.toUpperCase());
return nonceEncoder;
}
Future loginApi({
required String username,
required String password,
required String instituteCode,
required BuildContext context,
void Function(User)? onLogin,
void Function()? onSuccess,
}) async {
Provider.of<KretaClient>(context, listen: false).userAgent = Provider.of<SettingsProvider>(context, listen: false).config.userAgent;
Map<String, String> headers = {
"content-type": "application/x-www-form-urlencoded",
};
String nonceStr = await Provider.of<KretaClient>(context, listen: false).getAPI(KretaAPI.nonce, json: false);
Nonce nonce = getNonce(nonceStr, username, instituteCode);
headers.addAll(nonce.header());
Map? res = await Provider.of<KretaClient>(context, listen: false).postAPI(KretaAPI.login,
headers: headers,
body: User.loginBody(
username: username,
password: password,
instituteCode: instituteCode,
));
if (res != null) {
if (res.containsKey("error")) {
if (res["error"] == "invalid_grant") {
return LoginState.invalidGrant;
}
} else {
if (res.containsKey("access_token")) {
try {
Provider.of<KretaClient>(context, listen: false).accessToken = res["access_token"];
Map? studentJson = await Provider.of<KretaClient>(context, listen: false).getAPI(KretaAPI.student(instituteCode));
Student student = Student.fromJson(studentJson!);
var user = User(
username: username,
password: password,
instituteCode: instituteCode,
name: student.name,
student: student,
role: JwtUtils.getRoleFromJWT(res["access_token"])!,
);
if (onLogin != null) onLogin(user);
// Store User in the database
await Provider.of<DatabaseProvider>(context, listen: false).store.storeUser(user);
Provider.of<UserProvider>(context, listen: false).addUser(user);
Provider.of<UserProvider>(context, listen: false).setUser(user.id);
// Get user data
try {
await Future.wait([
Provider.of<GradeProvider>(context, listen: false).fetch(),
Provider.of<TimetableProvider>(context, listen: false).fetch(week: Week.current()),
Provider.of<ExamProvider>(context, listen: false).fetch(),
Provider.of<HomeworkProvider>(context, listen: false).fetch(),
Provider.of<MessageProvider>(context, listen: false).fetchAll(),
Provider.of<NoteProvider>(context, listen: false).fetch(),
Provider.of<EventProvider>(context, listen: false).fetch(),
Provider.of<AbsenceProvider>(context, listen: false).fetch(),
]);
} catch (error) {
print("WARNING: failed to fetch user data: $error");
}
if (onSuccess != null) onSuccess();
return LoginState.success;
} catch (error) {
print("ERROR: loginApi: $error");
// maybe check debug mode
// ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("ERROR: $error")));
return LoginState.failed;
}
}
}
}
return LoginState.failed;
}
// ignore_for_file: avoid_print, use_build_context_synchronously
import 'package:filcnaplo/utils/jwt.dart';
import 'package:filcnaplo_kreta_api/providers/absence_provider.dart';
import 'package:filcnaplo_kreta_api/providers/event_provider.dart';
import 'package:filcnaplo_kreta_api/providers/exam_provider.dart';
import 'package:filcnaplo_kreta_api/providers/grade_provider.dart';
import 'package:filcnaplo_kreta_api/providers/homework_provider.dart';
import 'package:filcnaplo_kreta_api/providers/message_provider.dart';
import 'package:filcnaplo_kreta_api/providers/note_provider.dart';
import 'package:filcnaplo_kreta_api/providers/timetable_provider.dart';
import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/models/user.dart';
import 'package:filcnaplo_kreta_api/client/api.dart';
import 'package:filcnaplo_kreta_api/client/client.dart';
import 'package:filcnaplo_kreta_api/models/student.dart';
import 'package:filcnaplo_kreta_api/models/week.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:filcnaplo/api/nonce.dart';
enum LoginState { missingFields, invalidGrant, failed, normal, inProgress, success }
Nonce getNonce(String nonce, String username, String instituteCode) {
Nonce nonceEncoder = Nonce(key: [98, 97, 83, 115, 120, 79, 119, 108, 85, 49, 106, 77], nonce: nonce);
nonceEncoder.encode(instituteCode.toUpperCase() + nonce + username.toUpperCase());
return nonceEncoder;
}
Future loginApi({
required String username,
required String password,
required String instituteCode,
required BuildContext context,
void Function(User)? onLogin,
void Function()? onSuccess,
}) async {
Provider.of<KretaClient>(context, listen: false).userAgent = Provider.of<SettingsProvider>(context, listen: false).config.userAgent;
Map<String, String> headers = {
"content-type": "application/x-www-form-urlencoded",
};
String nonceStr = await Provider.of<KretaClient>(context, listen: false).getAPI(KretaAPI.nonce, json: false);
Nonce nonce = getNonce(nonceStr, username, instituteCode);
headers.addAll(nonce.header());
Map? res = await Provider.of<KretaClient>(context, listen: false).postAPI(KretaAPI.login,
headers: headers,
body: User.loginBody(
username: username,
password: password,
instituteCode: instituteCode,
));
if (res != null) {
if (res.containsKey("error")) {
if (res["error"] == "invalid_grant") {
return LoginState.invalidGrant;
}
} else {
if (res.containsKey("access_token")) {
try {
Provider.of<KretaClient>(context, listen: false).accessToken = res["access_token"];
Map? studentJson = await Provider.of<KretaClient>(context, listen: false).getAPI(KretaAPI.student(instituteCode));
Student student = Student.fromJson(studentJson!);
var user = User(
username: username,
password: password,
instituteCode: instituteCode,
name: student.name,
student: student,
role: JwtUtils.getRoleFromJWT(res["access_token"])!,
);
if (onLogin != null) onLogin(user);
// Store User in the database
await Provider.of<DatabaseProvider>(context, listen: false).store.storeUser(user);
Provider.of<UserProvider>(context, listen: false).addUser(user);
Provider.of<UserProvider>(context, listen: false).setUser(user.id);
// Get user data
try {
await Future.wait([
Provider.of<GradeProvider>(context, listen: false).fetch(),
Provider.of<TimetableProvider>(context, listen: false).fetch(week: Week.current()),
Provider.of<ExamProvider>(context, listen: false).fetch(),
Provider.of<HomeworkProvider>(context, listen: false).fetch(),
Provider.of<MessageProvider>(context, listen: false).fetchAll(),
Provider.of<NoteProvider>(context, listen: false).fetch(),
Provider.of<EventProvider>(context, listen: false).fetch(),
Provider.of<AbsenceProvider>(context, listen: false).fetch(),
]);
} catch (error) {
print("WARNING: failed to fetch user data: $error");
}
if (onSuccess != null) onSuccess();
return LoginState.success;
} catch (error) {
print("ERROR: loginApi: $error");
// maybe check debug mode
// ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("ERROR: $error")));
return LoginState.failed;
}
}
}
}
return LoginState.failed;
}

View File

@@ -1,25 +1,25 @@
import 'dart:convert';
import 'package:crypto/crypto.dart';
class Nonce {
String nonce;
List<int> key;
String? encoded;
Nonce({required this.nonce, required this.key});
Future encode(String message) async {
List<int> messageBytes = utf8.encode(message);
Hmac hmac = Hmac(sha512, key);
Digest digest = hmac.convert(messageBytes);
encoded = base64.encode(digest.bytes);
}
Map<String, String> header() {
return {
"X-Authorizationpolicy-Nonce": nonce,
"X-Authorizationpolicy-Key": encoded ?? "",
"X-Authorizationpolicy-Version": "v2",
};
}
}
import 'dart:convert';
import 'package:crypto/crypto.dart';
class Nonce {
String nonce;
List<int> key;
String? encoded;
Nonce({required this.nonce, required this.key});
Future encode(String message) async {
List<int> messageBytes = utf8.encode(message);
Hmac hmac = Hmac(sha512, key);
Digest digest = hmac.convert(messageBytes);
encoded = base64.encode(digest.bytes);
}
Map<String, String> header() {
return {
"X-Authorizationpolicy-Nonce": nonce,
"X-Authorizationpolicy-Key": encoded ?? "",
"X-Authorizationpolicy-Version": "v2",
};
}
}

View File

@@ -1,30 +1,30 @@
import 'dart:io';
import 'package:filcnaplo/database/query.dart';
import 'package:filcnaplo/database/store.dart';
import 'package:sqflite/sqflite.dart';
// ignore: depend_on_referenced_packages
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
class DatabaseProvider {
// late Database _database;
late DatabaseQuery query;
late UserDatabaseQuery userQuery;
late DatabaseStore store;
late UserDatabaseStore userStore;
Future<void> init() async {
Database db;
if (Platform.isLinux || Platform.isWindows) {
db = await databaseFactoryFfi.openDatabase("app.db");
} else {
db = await openDatabase("app.db");
}
query = DatabaseQuery(db: db);
store = DatabaseStore(db: db);
userQuery = UserDatabaseQuery(db: db);
userStore = UserDatabaseStore(db: db);
}
}
import 'dart:io';
import 'package:filcnaplo/database/query.dart';
import 'package:filcnaplo/database/store.dart';
import 'package:sqflite/sqflite.dart';
// ignore: depend_on_referenced_packages
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
class DatabaseProvider {
// late Database _database;
late DatabaseQuery query;
late UserDatabaseQuery userQuery;
late DatabaseStore store;
late UserDatabaseStore userStore;
Future<void> init() async {
Database db;
if (Platform.isLinux || Platform.isWindows) {
db = await databaseFactoryFfi.openDatabase("app.db");
} else {
db = await openDatabase("app.db");
}
query = DatabaseQuery(db: db);
store = DatabaseStore(db: db);
userQuery = UserDatabaseQuery(db: db);
userStore = UserDatabaseStore(db: db);
}
}

View File

@@ -1,204 +1,204 @@
// ignore_for_file: no_leading_underscores_for_local_identifiers
import 'dart:async';
import 'dart:io';
import 'package:filcnaplo/helpers/subject.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo_kreta_api/models/lesson.dart';
import 'package:filcnaplo_kreta_api/models/week.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo_kreta_api/providers/timetable_provider.dart';
import 'package:flutter/foundation.dart';
import 'package:live_activities/live_activities.dart';
import 'package:filcnaplo_mobile_ui/pages/home/live_card/live_card.i18n.dart';
enum LiveCardState { empty, duringLesson, duringBreak, morning, afternoon, night }
class LiveCardProvider extends ChangeNotifier {
Lesson? currentLesson;
Lesson? nextLesson;
Lesson? prevLesson;
List<Lesson>? nextLessons;
LiveCardState currentState = LiveCardState.empty;
late Timer _timer;
late final TimetableProvider _timetable;
late final SettingsProvider _settings;
late Duration _delay;
final _liveActivitiesPlugin = LiveActivities();
String? _latestActivityId;
Map<String, String> _lastActivity = {};
LiveCardProvider({
required TimetableProvider timetable,
required SettingsProvider settings,
}) : _timetable = timetable,
_settings = settings {
_liveActivitiesPlugin.init(appGroupId: "group.filcnaplo.livecard");
_liveActivitiesPlugin.getAllActivitiesIds().then((value) {
_latestActivityId = value.isNotEmpty ? value.first : null;
});
_timer = Timer.periodic(const Duration(seconds: 1), (timer) => update());
_delay = settings.bellDelayEnabled ? Duration(seconds: settings.bellDelay) : Duration.zero;
update();
}
@override
void dispose() {
_timer.cancel();
if (_latestActivityId != null && Platform.isIOS) _liveActivitiesPlugin.endActivity(_latestActivityId!);
super.dispose();
}
// Debugging
static DateTime _now() {
return DateTime.now();
}
String getFloorDifference() {
final prevFloor = prevLesson!.getFloor();
final nextFloor = nextLesson!.getFloor();
if (prevFloor == null || nextFloor == null || prevFloor == nextFloor) {
return "to room";
}
if (nextFloor == 0) {
return "ground floor";
}
if (nextFloor > prevFloor) {
return "up floor";
} else {
return "down floor";
}
}
Map<String, String> toMap() {
switch (currentState) {
case LiveCardState.duringLesson:
return {
"icon": currentLesson != null ? SubjectIcon.resolveName(subject: currentLesson?.subject) : "book",
"index": currentLesson != null ? '${currentLesson!.lessonIndex}. ' : "",
"title": currentLesson != null ? ShortSubject.resolve(subject: currentLesson?.subject).capital() : "",
"subtitle": currentLesson?.room.replaceAll("_", " ") ?? "",
"description": currentLesson?.description ?? "",
"startDate": ((currentLesson?.start.millisecondsSinceEpoch ?? 0) - _delay.inMilliseconds).toString(),
"endDate": ((currentLesson?.end.millisecondsSinceEpoch ?? 0) - _delay.inMilliseconds).toString(),
"nextSubject": nextLesson != null ? ShortSubject.resolve(subject: nextLesson?.subject).capital() : "",
"nextRoom": nextLesson?.room.replaceAll("_", " ") ?? "",
};
case LiveCardState.duringBreak:
final iconFloorMap = {
"to room": "chevron.right.2",
"up floor": "arrow.up.right",
"down floor": "arrow.down.left",
"ground floor": "arrow.down.left",
};
final diff = getFloorDifference();
return {
"icon": iconFloorMap[diff] ?? "cup.and.saucer",
"title": "Szünet",
"description": "go $diff".i18n.fill([diff != "to room" ? (nextLesson!.getFloor() ?? 0) : nextLesson!.room]),
"startDate": ((prevLesson?.end.millisecondsSinceEpoch ?? 0) - _delay.inMilliseconds).toString(),
"endDate": ((nextLesson?.start.millisecondsSinceEpoch ?? 0) - _delay.inMilliseconds).toString(),
"nextSubject": (nextLesson != null ? ShortSubject.resolve(subject: nextLesson?.subject) : "").capital(),
"nextRoom": nextLesson?.room.replaceAll("_", " ") ?? "",
"index": "",
"subtitle": "",
};
default:
return {};
}
}
void update() async {
if (Platform.isIOS) {
final cmap = toMap();
if (!mapEquals(cmap, _lastActivity)) {
_lastActivity = cmap;
if (_lastActivity.isNotEmpty) {
if (_latestActivityId == null) {
_liveActivitiesPlugin.createActivity(_lastActivity).then((value) => _latestActivityId = value);
} else {
_liveActivitiesPlugin.updateActivity(_latestActivityId!, _lastActivity);
}
} else {
if (_latestActivityId != null) _liveActivitiesPlugin.endActivity(_latestActivityId!);
}
}
}
List<Lesson> today = _today(_timetable);
if (today.isEmpty) {
await _timetable.fetch(week: Week.current());
today = _today(_timetable);
}
_delay = _settings.bellDelayEnabled ? Duration(seconds: _settings.bellDelay) : Duration.zero;
final now = _now().add(_delay);
// Filter cancelled lessons #20
// Filter label lessons #128
today = today.where((lesson) => lesson.status?.name != "Elmaradt" && lesson.subject.id != '' && !lesson.isEmpty).toList();
if (today.isNotEmpty) {
// sort
today.sort((a, b) => a.start.compareTo(b.start));
final _lesson = today.firstWhere((l) => l.start.isBefore(now) && l.end.isAfter(now), orElse: () => Lesson.fromJson({}));
if (_lesson.start.year != 0) {
currentLesson = _lesson;
} else {
currentLesson = null;
}
final _next = today.firstWhere((l) => l.start.isAfter(now), orElse: () => Lesson.fromJson({}));
nextLessons = today.where((l) => l.start.isAfter(now)).toList();
if (_next.start.year != 0) {
nextLesson = _next;
} else {
nextLesson = null;
}
final _prev = today.lastWhere((l) => l.end.isBefore(now), orElse: () => Lesson.fromJson({}));
if (_prev.start.year != 0) {
prevLesson = _prev;
} else {
prevLesson = null;
}
}
if (currentLesson != null) {
currentState = LiveCardState.duringLesson;
} else if (nextLesson != null && prevLesson != null) {
currentState = LiveCardState.duringBreak;
} else if (now.hour >= 12 && now.hour < 20) {
currentState = LiveCardState.afternoon;
} else if (now.hour >= 20) {
currentState = LiveCardState.night;
} else if (now.hour >= 5 && now.hour <= 10) {
currentState = LiveCardState.morning;
} else {
currentState = LiveCardState.empty;
}
notifyListeners();
}
bool get show => currentState != LiveCardState.empty;
Duration get delay => _delay;
bool _sameDate(DateTime a, DateTime b) => (a.year == b.year && a.month == b.month && a.day == b.day);
List<Lesson> _today(TimetableProvider p) => (p.getWeek(Week.current()) ?? []).where((l) => _sameDate(l.date, _now())).toList();
}
// ignore_for_file: no_leading_underscores_for_local_identifiers
import 'dart:async';
import 'dart:io';
import 'package:filcnaplo/helpers/subject.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo_kreta_api/models/lesson.dart';
import 'package:filcnaplo_kreta_api/models/week.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo_kreta_api/providers/timetable_provider.dart';
import 'package:flutter/foundation.dart';
import 'package:live_activities/live_activities.dart';
import 'package:filcnaplo_mobile_ui/pages/home/live_card/live_card.i18n.dart';
enum LiveCardState { empty, duringLesson, duringBreak, morning, afternoon, night }
class LiveCardProvider extends ChangeNotifier {
Lesson? currentLesson;
Lesson? nextLesson;
Lesson? prevLesson;
List<Lesson>? nextLessons;
LiveCardState currentState = LiveCardState.empty;
late Timer _timer;
late final TimetableProvider _timetable;
late final SettingsProvider _settings;
late Duration _delay;
final _liveActivitiesPlugin = LiveActivities();
String? _latestActivityId;
Map<String, String> _lastActivity = {};
LiveCardProvider({
required TimetableProvider timetable,
required SettingsProvider settings,
}) : _timetable = timetable,
_settings = settings {
_liveActivitiesPlugin.init(appGroupId: "group.filcnaplo.livecard");
_liveActivitiesPlugin.getAllActivitiesIds().then((value) {
_latestActivityId = value.isNotEmpty ? value.first : null;
});
_timer = Timer.periodic(const Duration(seconds: 1), (timer) => update());
_delay = settings.bellDelayEnabled ? Duration(seconds: settings.bellDelay) : Duration.zero;
update();
}
@override
void dispose() {
_timer.cancel();
if (_latestActivityId != null && Platform.isIOS) _liveActivitiesPlugin.endActivity(_latestActivityId!);
super.dispose();
}
// Debugging
static DateTime _now() {
return DateTime.now();
}
String getFloorDifference() {
final prevFloor = prevLesson!.getFloor();
final nextFloor = nextLesson!.getFloor();
if (prevFloor == null || nextFloor == null || prevFloor == nextFloor) {
return "to room";
}
if (nextFloor == 0) {
return "ground floor";
}
if (nextFloor > prevFloor) {
return "up floor";
} else {
return "down floor";
}
}
Map<String, String> toMap() {
switch (currentState) {
case LiveCardState.duringLesson:
return {
"icon": currentLesson != null ? SubjectIcon.resolveName(subject: currentLesson?.subject) : "book",
"index": currentLesson != null ? '${currentLesson!.lessonIndex}. ' : "",
"title": currentLesson != null ? ShortSubject.resolve(subject: currentLesson?.subject).capital() : "",
"subtitle": currentLesson?.room.replaceAll("_", " ") ?? "",
"description": currentLesson?.description ?? "",
"startDate": ((currentLesson?.start.millisecondsSinceEpoch ?? 0) - _delay.inMilliseconds).toString(),
"endDate": ((currentLesson?.end.millisecondsSinceEpoch ?? 0) - _delay.inMilliseconds).toString(),
"nextSubject": nextLesson != null ? ShortSubject.resolve(subject: nextLesson?.subject).capital() : "",
"nextRoom": nextLesson?.room.replaceAll("_", " ") ?? "",
};
case LiveCardState.duringBreak:
final iconFloorMap = {
"to room": "chevron.right.2",
"up floor": "arrow.up.right",
"down floor": "arrow.down.left",
"ground floor": "arrow.down.left",
};
final diff = getFloorDifference();
return {
"icon": iconFloorMap[diff] ?? "cup.and.saucer",
"title": "Szünet",
"description": "go $diff".i18n.fill([diff != "to room" ? (nextLesson!.getFloor() ?? 0) : nextLesson!.room]),
"startDate": ((prevLesson?.end.millisecondsSinceEpoch ?? 0) - _delay.inMilliseconds).toString(),
"endDate": ((nextLesson?.start.millisecondsSinceEpoch ?? 0) - _delay.inMilliseconds).toString(),
"nextSubject": (nextLesson != null ? ShortSubject.resolve(subject: nextLesson?.subject) : "").capital(),
"nextRoom": nextLesson?.room.replaceAll("_", " ") ?? "",
"index": "",
"subtitle": "",
};
default:
return {};
}
}
void update() async {
if (Platform.isIOS) {
final cmap = toMap();
if (!mapEquals(cmap, _lastActivity)) {
_lastActivity = cmap;
if (_lastActivity.isNotEmpty) {
if (_latestActivityId == null) {
_liveActivitiesPlugin.createActivity(_lastActivity).then((value) => _latestActivityId = value);
} else {
_liveActivitiesPlugin.updateActivity(_latestActivityId!, _lastActivity);
}
} else {
if (_latestActivityId != null) _liveActivitiesPlugin.endActivity(_latestActivityId!);
}
}
}
List<Lesson> today = _today(_timetable);
if (today.isEmpty) {
await _timetable.fetch(week: Week.current());
today = _today(_timetable);
}
_delay = _settings.bellDelayEnabled ? Duration(seconds: _settings.bellDelay) : Duration.zero;
final now = _now().add(_delay);
// Filter cancelled lessons #20
// Filter label lessons #128
today = today.where((lesson) => lesson.status?.name != "Elmaradt" && lesson.subject.id != '' && !lesson.isEmpty).toList();
if (today.isNotEmpty) {
// sort
today.sort((a, b) => a.start.compareTo(b.start));
final _lesson = today.firstWhere((l) => l.start.isBefore(now) && l.end.isAfter(now), orElse: () => Lesson.fromJson({}));
if (_lesson.start.year != 0) {
currentLesson = _lesson;
} else {
currentLesson = null;
}
final _next = today.firstWhere((l) => l.start.isAfter(now), orElse: () => Lesson.fromJson({}));
nextLessons = today.where((l) => l.start.isAfter(now)).toList();
if (_next.start.year != 0) {
nextLesson = _next;
} else {
nextLesson = null;
}
final _prev = today.lastWhere((l) => l.end.isBefore(now), orElse: () => Lesson.fromJson({}));
if (_prev.start.year != 0) {
prevLesson = _prev;
} else {
prevLesson = null;
}
}
if (currentLesson != null) {
currentState = LiveCardState.duringLesson;
} else if (nextLesson != null && prevLesson != null) {
currentState = LiveCardState.duringBreak;
} else if (now.hour >= 12 && now.hour < 20) {
currentState = LiveCardState.afternoon;
} else if (now.hour >= 20) {
currentState = LiveCardState.night;
} else if (now.hour >= 5 && now.hour <= 10) {
currentState = LiveCardState.morning;
} else {
currentState = LiveCardState.empty;
}
notifyListeners();
}
bool get show => currentState != LiveCardState.empty;
Duration get delay => _delay;
bool _sameDate(DateTime a, DateTime b) => (a.year == b.year && a.month == b.month && a.day == b.day);
List<Lesson> _today(TimetableProvider p) => (p.getWeek(Week.current()) ?? []).where((l) => _sameDate(l.date, _now())).toList();
}

View File

@@ -1,85 +1,85 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:math';
import 'package:filcnaplo/api/client.dart';
import 'package:filcnaplo/models/news.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class NewsProvider extends ChangeNotifier {
// Private
late List<News> _news;
late int _state;
late int _fresh;
bool show = false;
late BuildContext _context;
// Public
List<News> get news => _news;
int get state => _fresh - 1;
NewsProvider({
List<News> initialNews = const [],
required BuildContext context,
}) {
_news = List.castFrom(initialNews);
_context = context;
}
Future<void> restore() async {
// Load news state from the database
var state_ = Provider.of<SettingsProvider>(_context, listen: false).newsState;
if (state_ == -1) {
var news_ = await FilcAPI.getNews();
if (news_ != null) {
state_ = news_.length;
_news = news_;
}
}
_state = state_;
Provider.of<SettingsProvider>(_context, listen: false).update(newsState: _state);
}
Future<void> fetch() async {
var news_ = await FilcAPI.getNews();
if (news_ == null) return;
_news = news_;
_fresh = news_.length - _state;
if (_fresh < 0) {
_state = news_.length;
Provider.of<SettingsProvider>(_context, listen: false).update(newsState: _state);
}
_fresh = max(_fresh, 0);
if (_fresh > 0) {
show = true;
notifyListeners();
}
}
void lock() => show = false;
void release() {
if (_fresh == 0) return;
_fresh--;
_state++;
Provider.of<SettingsProvider>(_context, listen: false).update(newsState: _state);
if (_fresh > 0) {
show = true;
} else {
show = false;
}
notifyListeners();
}
}
// ignore_for_file: use_build_context_synchronously
import 'dart:math';
import 'package:filcnaplo/api/client.dart';
import 'package:filcnaplo/models/news.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class NewsProvider extends ChangeNotifier {
// Private
late List<News> _news;
late int _state;
late int _fresh;
bool show = false;
late BuildContext _context;
// Public
List<News> get news => _news;
int get state => _fresh - 1;
NewsProvider({
List<News> initialNews = const [],
required BuildContext context,
}) {
_news = List.castFrom(initialNews);
_context = context;
}
Future<void> restore() async {
// Load news state from the database
var state_ = Provider.of<SettingsProvider>(_context, listen: false).newsState;
if (state_ == -1) {
var news_ = await FilcAPI.getNews();
if (news_ != null) {
state_ = news_.length;
_news = news_;
}
}
_state = state_;
Provider.of<SettingsProvider>(_context, listen: false).update(newsState: _state);
}
Future<void> fetch() async {
var news_ = await FilcAPI.getNews();
if (news_ == null) return;
_news = news_;
_fresh = news_.length - _state;
if (_fresh < 0) {
_state = news_.length;
Provider.of<SettingsProvider>(_context, listen: false).update(newsState: _state);
}
_fresh = max(_fresh, 0);
if (_fresh > 0) {
show = true;
notifyListeners();
}
}
void lock() => show = false;
void release() {
if (_fresh == 0) return;
_fresh--;
_state++;
Provider.of<SettingsProvider>(_context, listen: false).update(newsState: _state);
if (_fresh > 0) {
show = true;
} else {
show = false;
}
notifyListeners();
}
}

View File

@@ -1,79 +1,79 @@
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/widgets.dart';
import 'package:http/http.dart' as http;
enum Status { network, maintenance, syncing }
class StatusProvider extends ChangeNotifier {
final List<Status> _stack = [];
double _progress = 0.0;
ConnectivityResult _networkType = ConnectivityResult.none;
ConnectivityResult get networkType => _networkType;
StatusProvider() {
_handleNetworkChanges();
Connectivity().checkConnectivity().then((value) => _networkType = value);
}
Status? getStatus() => _stack.isNotEmpty ? _stack[0] : null;
// Status progress from 0.0 to 1.0
double get progress => _progress;
void _handleNetworkChanges() {
Connectivity().onConnectivityChanged.listen((event) {
_networkType = event;
if (event == ConnectivityResult.none) {
if (!_stack.contains(Status.network)) {
_stack.insert(0, Status.network);
notifyListeners();
}
} else {
if (_stack.contains(Status.network)) {
_stack.remove(Status.network);
notifyListeners();
}
}
});
}
void triggerRequest(http.Response res) {
if (res.headers.containsKey("x-maintenance-mode") || res.statusCode == 503) {
if (!_stack.contains(Status.maintenance)) {
_stack.insert(0, Status.maintenance);
notifyListeners();
}
} else {
if (_stack.contains(Status.maintenance)) {
_stack.remove(Status.maintenance);
notifyListeners();
}
}
}
void triggerSync({required int current, required int max}) {
double prev = _progress;
if (!_stack.contains(Status.syncing)) {
_stack.add(Status.syncing);
_progress = 0.0;
notifyListeners();
}
if (max == 0) {
_progress = 0.0;
} else {
_progress = current / max;
}
if (_progress == 1.0) {
notifyListeners();
// Wait for animation
Future.delayed(const Duration(milliseconds: 250), () {
_stack.remove(Status.syncing);
notifyListeners();
});
} else if (progress != prev) {
notifyListeners();
}
}
}
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/widgets.dart';
import 'package:http/http.dart' as http;
enum Status { network, maintenance, syncing }
class StatusProvider extends ChangeNotifier {
final List<Status> _stack = [];
double _progress = 0.0;
ConnectivityResult _networkType = ConnectivityResult.none;
ConnectivityResult get networkType => _networkType;
StatusProvider() {
_handleNetworkChanges();
Connectivity().checkConnectivity().then((value) => _networkType = value);
}
Status? getStatus() => _stack.isNotEmpty ? _stack[0] : null;
// Status progress from 0.0 to 1.0
double get progress => _progress;
void _handleNetworkChanges() {
Connectivity().onConnectivityChanged.listen((event) {
_networkType = event;
if (event == ConnectivityResult.none) {
if (!_stack.contains(Status.network)) {
_stack.insert(0, Status.network);
notifyListeners();
}
} else {
if (_stack.contains(Status.network)) {
_stack.remove(Status.network);
notifyListeners();
}
}
});
}
void triggerRequest(http.Response res) {
if (res.headers.containsKey("x-maintenance-mode") || res.statusCode == 503) {
if (!_stack.contains(Status.maintenance)) {
_stack.insert(0, Status.maintenance);
notifyListeners();
}
} else {
if (_stack.contains(Status.maintenance)) {
_stack.remove(Status.maintenance);
notifyListeners();
}
}
}
void triggerSync({required int current, required int max}) {
double prev = _progress;
if (!_stack.contains(Status.syncing)) {
_stack.add(Status.syncing);
_progress = 0.0;
notifyListeners();
}
if (max == 0) {
_progress = 0.0;
} else {
_progress = current / max;
}
if (_progress == 1.0) {
notifyListeners();
// Wait for animation
Future.delayed(const Duration(milliseconds: 250), () {
_stack.remove(Status.syncing);
notifyListeners();
});
} else if (progress != prev) {
notifyListeners();
}
}
}

View File

@@ -1,88 +1,88 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/api/providers/status_provider.dart';
import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo_kreta_api/client/api.dart';
import 'package:filcnaplo_kreta_api/client/client.dart';
import 'package:filcnaplo_kreta_api/models/student.dart';
import 'package:filcnaplo_kreta_api/models/week.dart';
import 'package:filcnaplo_kreta_api/providers/absence_provider.dart';
import 'package:filcnaplo_kreta_api/providers/event_provider.dart';
import 'package:filcnaplo_kreta_api/providers/exam_provider.dart';
import 'package:filcnaplo_kreta_api/providers/grade_provider.dart';
import 'package:filcnaplo_kreta_api/providers/homework_provider.dart';
import 'package:filcnaplo_kreta_api/providers/message_provider.dart';
import 'package:filcnaplo_kreta_api/providers/note_provider.dart';
import 'package:filcnaplo_kreta_api/providers/timetable_provider.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
import 'package:home_widget/home_widget.dart';
// Mutex
bool lock = false;
Future<void> syncAll(BuildContext context) {
if (lock) return Future.value();
// Lock
lock = true;
// ignore: avoid_print
print("INFO Syncing all");
UserProvider user = Provider.of<UserProvider>(context, listen: false);
StatusProvider statusProvider = Provider.of<StatusProvider>(context, listen: false);
List<Future<void>> tasks = [];
int taski = 0;
Future<void> syncStatus(Future<void> future) async {
await future.onError((error, stackTrace) => null);
taski++;
statusProvider.triggerSync(current: taski, max: tasks.length);
}
tasks = [
syncStatus(Provider.of<GradeProvider>(context, listen: false).fetch()),
syncStatus(Provider.of<TimetableProvider>(context, listen: false).fetch(week: Week.current())),
syncStatus(Provider.of<ExamProvider>(context, listen: false).fetch()),
syncStatus(Provider.of<HomeworkProvider>(context, listen: false).fetch(from: DateTime.now().subtract(const Duration(days: 30)))),
syncStatus(Provider.of<MessageProvider>(context, listen: false).fetchAll()),
syncStatus(Provider.of<NoteProvider>(context, listen: false).fetch()),
syncStatus(Provider.of<EventProvider>(context, listen: false).fetch()),
syncStatus(Provider.of<AbsenceProvider>(context, listen: false).fetch()),
// Sync student
syncStatus(() async {
if (user.user == null) return;
Map? studentJson = await Provider.of<KretaClient>(context, listen: false).getAPI(KretaAPI.student(user.instituteCode!));
if (studentJson == null) return;
Student student = Student.fromJson(studentJson);
user.user?.name = student.name;
// Store user
await Provider.of<DatabaseProvider>(context, listen: false).store.storeUser(user.user!);
}()),
];
Future<bool?> updateWidget() async {
try {
return HomeWidget.updateWidget(name: 'widget_timetable.WidgetTimetable');
} on PlatformException catch (exception) {
debugPrint('Error Updating Widget. $exception');
}
return false;
}
return Future.wait(tasks).then((value) {
// Unlock
lock = false;
// Update Widget
if (Platform.isAndroid) updateWidget();
});
}
// ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/api/providers/status_provider.dart';
import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo_kreta_api/client/api.dart';
import 'package:filcnaplo_kreta_api/client/client.dart';
import 'package:filcnaplo_kreta_api/models/student.dart';
import 'package:filcnaplo_kreta_api/models/week.dart';
import 'package:filcnaplo_kreta_api/providers/absence_provider.dart';
import 'package:filcnaplo_kreta_api/providers/event_provider.dart';
import 'package:filcnaplo_kreta_api/providers/exam_provider.dart';
import 'package:filcnaplo_kreta_api/providers/grade_provider.dart';
import 'package:filcnaplo_kreta_api/providers/homework_provider.dart';
import 'package:filcnaplo_kreta_api/providers/message_provider.dart';
import 'package:filcnaplo_kreta_api/providers/note_provider.dart';
import 'package:filcnaplo_kreta_api/providers/timetable_provider.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
import 'package:home_widget/home_widget.dart';
// Mutex
bool lock = false;
Future<void> syncAll(BuildContext context) {
if (lock) return Future.value();
// Lock
lock = true;
// ignore: avoid_print
print("INFO Syncing all");
UserProvider user = Provider.of<UserProvider>(context, listen: false);
StatusProvider statusProvider = Provider.of<StatusProvider>(context, listen: false);
List<Future<void>> tasks = [];
int taski = 0;
Future<void> syncStatus(Future<void> future) async {
await future.onError((error, stackTrace) => null);
taski++;
statusProvider.triggerSync(current: taski, max: tasks.length);
}
tasks = [
syncStatus(Provider.of<GradeProvider>(context, listen: false).fetch()),
syncStatus(Provider.of<TimetableProvider>(context, listen: false).fetch(week: Week.current())),
syncStatus(Provider.of<ExamProvider>(context, listen: false).fetch()),
syncStatus(Provider.of<HomeworkProvider>(context, listen: false).fetch(from: DateTime.now().subtract(const Duration(days: 30)))),
syncStatus(Provider.of<MessageProvider>(context, listen: false).fetchAll()),
syncStatus(Provider.of<NoteProvider>(context, listen: false).fetch()),
syncStatus(Provider.of<EventProvider>(context, listen: false).fetch()),
syncStatus(Provider.of<AbsenceProvider>(context, listen: false).fetch()),
// Sync student
syncStatus(() async {
if (user.user == null) return;
Map? studentJson = await Provider.of<KretaClient>(context, listen: false).getAPI(KretaAPI.student(user.instituteCode!));
if (studentJson == null) return;
Student student = Student.fromJson(studentJson);
user.user?.name = student.name;
// Store user
await Provider.of<DatabaseProvider>(context, listen: false).store.storeUser(user.user!);
}()),
];
Future<bool?> updateWidget() async {
try {
return HomeWidget.updateWidget(name: 'widget_timetable.WidgetTimetable');
} on PlatformException catch (exception) {
debugPrint('Error Updating Widget. $exception');
}
return false;
}
return Future.wait(tasks).then((value) {
// Unlock
lock = false;
// Update Widget
if (Platform.isAndroid) updateWidget();
});
}

View File

@@ -1,39 +1,39 @@
import 'dart:io';
import 'package:filcnaplo/api/client.dart';
import 'package:filcnaplo/models/release.dart';
import 'package:flutter/material.dart';
class UpdateProvider extends ChangeNotifier {
// Private
late List<Release> _releases;
bool _available = false;
bool get available => _available && _releases.isNotEmpty;
// Public
List<Release> get releases => _releases;
UpdateProvider({
List<Release> initialReleases = const [],
required BuildContext context,
}) {
_releases = List.castFrom(initialReleases);
}
static const currentVersion = String.fromEnvironment("APPVER", defaultValue: "1.0");
Future<void> fetch() async {
if (!Platform.isAndroid) return;
_releases = await FilcAPI.getReleases() ?? [];
_releases.sort((a, b) => -a.version.compareTo(b.version));
// Check for new releases
if (_releases.isNotEmpty) {
_available = _releases.first.version.compareTo(Version.fromString(currentVersion)) == 1;
// ignore: avoid_print
if (_available) print("INFO: New update: ${releases.first.version}");
notifyListeners();
}
}
}
import 'dart:io';
import 'package:filcnaplo/api/client.dart';
import 'package:filcnaplo/models/release.dart';
import 'package:flutter/material.dart';
class UpdateProvider extends ChangeNotifier {
// Private
late List<Release> _releases;
bool _available = false;
bool get available => _available && _releases.isNotEmpty;
// Public
List<Release> get releases => _releases;
UpdateProvider({
List<Release> initialReleases = const [],
required BuildContext context,
}) {
_releases = List.castFrom(initialReleases);
}
static const currentVersion = String.fromEnvironment("APPVER", defaultValue: "1.0");
Future<void> fetch() async {
if (!Platform.isAndroid) return;
_releases = await FilcAPI.getReleases() ?? [];
_releases.sort((a, b) => -a.version.compareTo(b.version));
// Check for new releases
if (_releases.isNotEmpty) {
_available = _releases.first.version.compareTo(Version.fromString(currentVersion)) == 1;
// ignore: avoid_print
if (_available) print("INFO: New update: ${releases.first.version}");
notifyListeners();
}
}
}

View File

@@ -1,78 +1,78 @@
import 'dart:io';
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/models/user.dart';
import 'package:filcnaplo_kreta_api/models/student.dart';
import 'package:flutter/foundation.dart';
import 'package:home_widget/home_widget.dart';
import 'package:flutter/services.dart';
class UserProvider with ChangeNotifier {
final Map<String, User> _users = {};
String? _selectedUserId;
User? get user => _users[_selectedUserId];
// _user properties
String? get instituteCode => user?.instituteCode;
String? get id => user?.id;
String? get name => user?.name;
String? get username => user?.username;
String? get password => user?.password;
Role? get role => user?.role;
Student? get student => user?.student;
String? get nickname => user?.nickname;
String get picture => user?.picture ?? "";
String? get displayName => user?.displayName;
final SettingsProvider _settings;
UserProvider({required SettingsProvider settings}) : _settings = settings;
void setUser(String userId) async {
_selectedUserId = userId;
await _settings.update(lastAccountId: userId);
if (Platform.isAndroid) updateWidget();
notifyListeners();
}
Future<bool?> updateWidget() async {
try {
return HomeWidget.updateWidget(name: 'widget_timetable.WidgetTimetable');
} on PlatformException catch (exception) {
if (kDebugMode) {
print('Error Updating Widget After setUser. $exception');
}
}
return false;
}
void addUser(User user) {
_users[user.id] = user;
if (kDebugMode) {
print("DEBUG: Added User: ${user.id}");
}
}
void removeUser(String userId) async {
_users.removeWhere((key, value) => key == userId);
if (_users.isNotEmpty) {
setUser(_users.keys.first);
} else {
await _settings.update(lastAccountId: "");
}
if (Platform.isAndroid) updateWidget();
notifyListeners();
}
User getUser(String userId) {
return _users[userId]!;
}
List<User> getUsers() {
return _users.values.toList();
}
void refresh() {
notifyListeners();
}
}
import 'dart:io';
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/models/user.dart';
import 'package:filcnaplo_kreta_api/models/student.dart';
import 'package:flutter/foundation.dart';
import 'package:home_widget/home_widget.dart';
import 'package:flutter/services.dart';
class UserProvider with ChangeNotifier {
final Map<String, User> _users = {};
String? _selectedUserId;
User? get user => _users[_selectedUserId];
// _user properties
String? get instituteCode => user?.instituteCode;
String? get id => user?.id;
String? get name => user?.name;
String? get username => user?.username;
String? get password => user?.password;
Role? get role => user?.role;
Student? get student => user?.student;
String? get nickname => user?.nickname;
String get picture => user?.picture ?? "";
String? get displayName => user?.displayName;
final SettingsProvider _settings;
UserProvider({required SettingsProvider settings}) : _settings = settings;
void setUser(String userId) async {
_selectedUserId = userId;
await _settings.update(lastAccountId: userId);
if (Platform.isAndroid) updateWidget();
notifyListeners();
}
Future<bool?> updateWidget() async {
try {
return HomeWidget.updateWidget(name: 'widget_timetable.WidgetTimetable');
} on PlatformException catch (exception) {
if (kDebugMode) {
print('Error Updating Widget After setUser. $exception');
}
}
return false;
}
void addUser(User user) {
_users[user.id] = user;
if (kDebugMode) {
print("DEBUG: Added User: ${user.id}");
}
}
void removeUser(String userId) async {
_users.removeWhere((key, value) => key == userId);
if (_users.isNotEmpty) {
setUser(_users.keys.first);
} else {
await _settings.update(lastAccountId: "");
}
if (Platform.isAndroid) updateWidget();
notifyListeners();
}
User getUser(String userId) {
return _users[userId]!;
}
List<User> getUsers() {
return _users.values.toList();
}
void refresh() {
notifyListeners();
}
}

View File

@@ -1,193 +1,193 @@
import 'dart:io';
import 'dart:math';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:filcnaplo/api/client.dart';
import 'package:filcnaplo/api/providers/live_card_provider.dart';
import 'package:filcnaplo/api/providers/news_provider.dart';
import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/api/providers/status_provider.dart';
import 'package:filcnaplo/models/config.dart';
import 'package:filcnaplo/theme/observer.dart';
import 'package:filcnaplo/theme/theme.dart';
import 'package:filcnaplo_kreta_api/client/client.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:i18n_extension/i18n_widget.dart';
import 'package:material_color_utilities/palettes/core_palette.dart';
import 'package:provider/provider.dart';
// Mobile UI
import 'package:filcnaplo_mobile_ui/common/system_chrome.dart' as mobile;
import 'package:filcnaplo_mobile_ui/screens/login/login_route.dart' as mobile;
import 'package:filcnaplo_mobile_ui/screens/login/login_screen.dart' as mobile;
import 'package:filcnaplo_mobile_ui/screens/navigation/navigation_screen.dart' as mobile;
import 'package:filcnaplo_mobile_ui/screens/settings/settings_route.dart' as mobile;
import 'package:filcnaplo_mobile_ui/screens/settings/settings_screen.dart' as mobile;
// Desktop UI
import 'package:filcnaplo_desktop_ui/screens/navigation/navigation_screen.dart' as desktop;
import 'package:filcnaplo_desktop_ui/screens/login/login_screen.dart' as desktop;
import 'package:filcnaplo_desktop_ui/screens/login/login_route.dart' as desktop;
// Providers
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo_kreta_api/providers/absence_provider.dart';
import 'package:filcnaplo_kreta_api/providers/event_provider.dart';
import 'package:filcnaplo_kreta_api/providers/exam_provider.dart';
import 'package:filcnaplo_kreta_api/providers/grade_provider.dart';
import 'package:filcnaplo_kreta_api/providers/homework_provider.dart';
import 'package:filcnaplo_kreta_api/providers/message_provider.dart';
import 'package:filcnaplo_kreta_api/providers/note_provider.dart';
import 'package:filcnaplo_kreta_api/providers/timetable_provider.dart';
import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo/api/providers/update_provider.dart';
import 'package:filcnaplo_mobile_ui/pages/grades/calculator/grade_calculator_provider.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:filcnaplo_premium/providers/premium_provider.dart';
class App extends StatelessWidget {
final SettingsProvider settings;
final UserProvider user;
final DatabaseProvider database;
const App({Key? key, required this.database, required this.settings, required this.user}) : super(key: key);
@override
Widget build(BuildContext context) {
mobile.setSystemChrome(context);
// Set high refresh mode #28
if (Platform.isAndroid) FlutterDisplayMode.setHighRefreshRate();
CorePalette? corePalette;
final status = StatusProvider();
final kreta = KretaClient(user: user, settings: settings, status: status);
final timetable = TimetableProvider(user: user, database: database, kreta: kreta);
final premium = PremiumProvider(settings: settings);
WidgetsBinding.instance.addPostFrameCallback((_) {
FilcAPI.getConfig(settings).then((Config? config) {
if (config != null) settings.update(config: config);
});
premium.activate();
});
return MultiProvider(
providers: [
ChangeNotifierProvider<PremiumProvider>(create: (_) => premium),
ChangeNotifierProvider<SettingsProvider>(create: (_) => settings),
ChangeNotifierProvider<UserProvider>(create: (_) => user),
ChangeNotifierProvider<StatusProvider>(create: (_) => status),
Provider<KretaClient>(create: (_) => kreta),
Provider<DatabaseProvider>(create: (context) => database),
ChangeNotifierProvider<ThemeModeObserver>(create: (context) => ThemeModeObserver(initialTheme: settings.theme)),
ChangeNotifierProvider<NewsProvider>(create: (context) => NewsProvider(context: context)),
ChangeNotifierProvider<UpdateProvider>(create: (context) => UpdateProvider(context: context)),
// User data providers
ChangeNotifierProvider<GradeProvider>(create: (_) => GradeProvider(settings: settings, user: user, database: database, kreta: kreta)),
ChangeNotifierProvider<TimetableProvider>(create: (_) => timetable),
ChangeNotifierProvider<ExamProvider>(create: (context) => ExamProvider(context: context)),
ChangeNotifierProvider<HomeworkProvider>(create: (context) => HomeworkProvider(context: context)),
ChangeNotifierProvider<MessageProvider>(create: (context) => MessageProvider(context: context)),
ChangeNotifierProvider<NoteProvider>(create: (context) => NoteProvider(context: context)),
ChangeNotifierProvider<EventProvider>(create: (context) => EventProvider(context: context)),
ChangeNotifierProvider<AbsenceProvider>(create: (context) => AbsenceProvider(context: context)),
ChangeNotifierProvider<GradeCalculatorProvider>(
create: (_) => GradeCalculatorProvider(settings: settings, user: user, database: database, kreta: kreta)),
ChangeNotifierProvider<LiveCardProvider>(create: (context) => LiveCardProvider(timetable: timetable, settings: settings))
],
child: Consumer<ThemeModeObserver>(
builder: (context, themeMode, child) {
return FutureBuilder<CorePalette?>(
future: DynamicColorPlugin.getCorePalette(),
builder: (context, snapshot) {
corePalette = snapshot.data;
return MaterialApp(
builder: (context, child) {
// Limit font size scaling to 1.0
double textScaleFactor = min(MediaQuery.of(context).textScaleFactor, 1.0);
return I18n(
initialLocale: Locale(settings.language, settings.language.toUpperCase()),
child: MediaQuery(
data: MediaQuery.of(context).copyWith(textScaleFactor: textScaleFactor),
child: child ?? Container(),
),
);
},
title: "Filc Napló",
debugShowCheckedModeBanner: false,
theme: AppTheme.lightTheme(context, palette: corePalette),
darkTheme: AppTheme.darkTheme(context, palette: corePalette),
themeMode: themeMode.themeMode,
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('en', 'EN'),
Locale('hu', 'HU'),
Locale('de', 'DE'),
],
localeListResolutionCallback: (locales, supported) {
Locale locale = const Locale('hu', 'HU');
for (var loc in locales ?? []) {
if (supported.contains(loc)) {
locale = loc;
break;
}
}
return locale;
},
onGenerateRoute: (settings) => rootNavigator(settings),
initialRoute: user.getUsers().isNotEmpty ? "navigation" : "login",
);
},
);
},
),
);
}
Route? rootNavigator(RouteSettings route) {
// if platform == android || platform == ios
if (Platform.isAndroid || Platform.isIOS) {
switch (route.name) {
case "login_back":
return CupertinoPageRoute(builder: (context) => const mobile.LoginScreen(back: true));
case "login":
return _rootRoute(const mobile.LoginScreen());
case "navigation":
return _rootRoute(const mobile.NavigationScreen());
case "login_to_navigation":
return mobile.loginRoute(const mobile.NavigationScreen());
case "settings":
return mobile.settingsRoute(const mobile.SettingsScreen());
}
} else if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
switch (route.name) {
case "login_back":
return CupertinoPageRoute(builder: (context) => const desktop.LoginScreen(back: true));
case "login":
return _rootRoute(const desktop.LoginScreen());
case "navigation":
return _rootRoute(const desktop.NavigationScreen());
case "login_to_navigation":
return desktop.loginRoute(const desktop.NavigationScreen());
}
}
return null;
}
Route _rootRoute(Widget widget) {
return PageRouteBuilder(pageBuilder: (context, _, __) => widget);
}
}
import 'dart:io';
import 'dart:math';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:filcnaplo/api/client.dart';
import 'package:filcnaplo/api/providers/live_card_provider.dart';
import 'package:filcnaplo/api/providers/news_provider.dart';
import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/api/providers/status_provider.dart';
import 'package:filcnaplo/models/config.dart';
import 'package:filcnaplo/theme/observer.dart';
import 'package:filcnaplo/theme/theme.dart';
import 'package:filcnaplo_kreta_api/client/client.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:i18n_extension/i18n_widget.dart';
import 'package:material_color_utilities/palettes/core_palette.dart';
import 'package:provider/provider.dart';
// Mobile UI
import 'package:filcnaplo_mobile_ui/common/system_chrome.dart' as mobile;
import 'package:filcnaplo_mobile_ui/screens/login/login_route.dart' as mobile;
import 'package:filcnaplo_mobile_ui/screens/login/login_screen.dart' as mobile;
import 'package:filcnaplo_mobile_ui/screens/navigation/navigation_screen.dart' as mobile;
import 'package:filcnaplo_mobile_ui/screens/settings/settings_route.dart' as mobile;
import 'package:filcnaplo_mobile_ui/screens/settings/settings_screen.dart' as mobile;
// Desktop UI
import 'package:filcnaplo_desktop_ui/screens/navigation/navigation_screen.dart' as desktop;
import 'package:filcnaplo_desktop_ui/screens/login/login_screen.dart' as desktop;
import 'package:filcnaplo_desktop_ui/screens/login/login_route.dart' as desktop;
// Providers
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo_kreta_api/providers/absence_provider.dart';
import 'package:filcnaplo_kreta_api/providers/event_provider.dart';
import 'package:filcnaplo_kreta_api/providers/exam_provider.dart';
import 'package:filcnaplo_kreta_api/providers/grade_provider.dart';
import 'package:filcnaplo_kreta_api/providers/homework_provider.dart';
import 'package:filcnaplo_kreta_api/providers/message_provider.dart';
import 'package:filcnaplo_kreta_api/providers/note_provider.dart';
import 'package:filcnaplo_kreta_api/providers/timetable_provider.dart';
import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo/api/providers/update_provider.dart';
import 'package:filcnaplo_mobile_ui/pages/grades/calculator/grade_calculator_provider.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:filcnaplo_premium/providers/premium_provider.dart';
class App extends StatelessWidget {
final SettingsProvider settings;
final UserProvider user;
final DatabaseProvider database;
const App({Key? key, required this.database, required this.settings, required this.user}) : super(key: key);
@override
Widget build(BuildContext context) {
mobile.setSystemChrome(context);
// Set high refresh mode #28
if (Platform.isAndroid) FlutterDisplayMode.setHighRefreshRate();
CorePalette? corePalette;
final status = StatusProvider();
final kreta = KretaClient(user: user, settings: settings, status: status);
final timetable = TimetableProvider(user: user, database: database, kreta: kreta);
final premium = PremiumProvider(settings: settings);
WidgetsBinding.instance.addPostFrameCallback((_) {
FilcAPI.getConfig(settings).then((Config? config) {
if (config != null) settings.update(config: config);
});
premium.activate();
});
return MultiProvider(
providers: [
ChangeNotifierProvider<PremiumProvider>(create: (_) => premium),
ChangeNotifierProvider<SettingsProvider>(create: (_) => settings),
ChangeNotifierProvider<UserProvider>(create: (_) => user),
ChangeNotifierProvider<StatusProvider>(create: (_) => status),
Provider<KretaClient>(create: (_) => kreta),
Provider<DatabaseProvider>(create: (context) => database),
ChangeNotifierProvider<ThemeModeObserver>(create: (context) => ThemeModeObserver(initialTheme: settings.theme)),
ChangeNotifierProvider<NewsProvider>(create: (context) => NewsProvider(context: context)),
ChangeNotifierProvider<UpdateProvider>(create: (context) => UpdateProvider(context: context)),
// User data providers
ChangeNotifierProvider<GradeProvider>(create: (_) => GradeProvider(settings: settings, user: user, database: database, kreta: kreta)),
ChangeNotifierProvider<TimetableProvider>(create: (_) => timetable),
ChangeNotifierProvider<ExamProvider>(create: (context) => ExamProvider(context: context)),
ChangeNotifierProvider<HomeworkProvider>(create: (context) => HomeworkProvider(context: context)),
ChangeNotifierProvider<MessageProvider>(create: (context) => MessageProvider(context: context)),
ChangeNotifierProvider<NoteProvider>(create: (context) => NoteProvider(context: context)),
ChangeNotifierProvider<EventProvider>(create: (context) => EventProvider(context: context)),
ChangeNotifierProvider<AbsenceProvider>(create: (context) => AbsenceProvider(context: context)),
ChangeNotifierProvider<GradeCalculatorProvider>(
create: (_) => GradeCalculatorProvider(settings: settings, user: user, database: database, kreta: kreta)),
ChangeNotifierProvider<LiveCardProvider>(create: (context) => LiveCardProvider(timetable: timetable, settings: settings))
],
child: Consumer<ThemeModeObserver>(
builder: (context, themeMode, child) {
return FutureBuilder<CorePalette?>(
future: DynamicColorPlugin.getCorePalette(),
builder: (context, snapshot) {
corePalette = snapshot.data;
return MaterialApp(
builder: (context, child) {
// Limit font size scaling to 1.0
double textScaleFactor = min(MediaQuery.of(context).textScaleFactor, 1.0);
return I18n(
initialLocale: Locale(settings.language, settings.language.toUpperCase()),
child: MediaQuery(
data: MediaQuery.of(context).copyWith(textScaleFactor: textScaleFactor),
child: child ?? Container(),
),
);
},
title: "Filc Napló",
debugShowCheckedModeBanner: false,
theme: AppTheme.lightTheme(context, palette: corePalette),
darkTheme: AppTheme.darkTheme(context, palette: corePalette),
themeMode: themeMode.themeMode,
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('en', 'EN'),
Locale('hu', 'HU'),
Locale('de', 'DE'),
],
localeListResolutionCallback: (locales, supported) {
Locale locale = const Locale('hu', 'HU');
for (var loc in locales ?? []) {
if (supported.contains(loc)) {
locale = loc;
break;
}
}
return locale;
},
onGenerateRoute: (settings) => rootNavigator(settings),
initialRoute: user.getUsers().isNotEmpty ? "navigation" : "login",
);
},
);
},
),
);
}
Route? rootNavigator(RouteSettings route) {
// if platform == android || platform == ios
if (Platform.isAndroid || Platform.isIOS) {
switch (route.name) {
case "login_back":
return CupertinoPageRoute(builder: (context) => const mobile.LoginScreen(back: true));
case "login":
return _rootRoute(const mobile.LoginScreen());
case "navigation":
return _rootRoute(const mobile.NavigationScreen());
case "login_to_navigation":
return mobile.loginRoute(const mobile.NavigationScreen());
case "settings":
return mobile.settingsRoute(const mobile.SettingsScreen());
}
} else if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
switch (route.name) {
case "login_back":
return CupertinoPageRoute(builder: (context) => const desktop.LoginScreen(back: true));
case "login":
return _rootRoute(const desktop.LoginScreen());
case "navigation":
return _rootRoute(const desktop.NavigationScreen());
case "login_to_navigation":
return desktop.loginRoute(const desktop.NavigationScreen());
}
}
return null;
}
Route _rootRoute(Widget widget) {
return PageRouteBuilder(pageBuilder: (context, _, __) => widget);
}
}

View File

@@ -1,141 +1,141 @@
// ignore_for_file: avoid_print
import 'dart:io';
import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/database/struct.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:sqflite/sqflite.dart';
// ignore: depend_on_referenced_packages
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
const settingsDB = DatabaseStruct("settings", {
"language": String, "start_page": int, "rounding": int, "theme": int, "accent_color": int, "news": int, "news_state": int, "developer_mode": int,
"update_channel": int, "config": String, "custom_accent_color": int, "custom_background_color": int, "custom_highlight_color": int, // general
"grade_color1": int, "grade_color2": int, "grade_color3": int, "grade_color4": int, "grade_color5": int, // grade colors
"vibration_strength": int, "ab_weeks": int, "swap_ab_weeks": int,
"notifications": int, "notifications_bitfield": int, "notification_poll_interval": int, // notifications
"x_filc_id": String, "graph_class_avg": int, "presentation_mode": int, "bell_delay": int, "bell_delay_enabled": int,
"grade_opening_fun": int, "icon_pack": String, "premium_scopes": String, "premium_token": String, "premium_login": String,
"last_account_id": String, "renamed_subjects_enabled": int,
});
// DON'T FORGET TO UPDATE DEFAULT VALUES IN `initDB` MIGRATION OR ELSE PARENTS WILL COMPLAIN ABOUT THEIR CHILDREN MISSING
// YOU'VE BEEN WARNED!!!
const usersDB = DatabaseStruct("users", {
"id": String, "name": String, "username": String, "password": String, "institute_code": String, "student": String, "role": int,
"nickname": String, "picture": String // premium only
});
const userDataDB = DatabaseStruct("user_data", {
"id": String, "grades": String, "timetable": String, "exams": String, "homework": String, "messages": String, "notes": String,
"events": String, "absences": String, "group_averages": String,
// renamed subjects // non kreta data
"renamed_subjects": String,
// "subject_lesson_count": String, // non kreta data
"last_seen_grade": int,
});
Future<void> createTable(Database db, DatabaseStruct struct) => db.execute("CREATE TABLE IF NOT EXISTS ${struct.table} ($struct)");
Future<Database> initDB(DatabaseProvider database) async {
Database db;
if (Platform.isLinux || Platform.isWindows) {
sqfliteFfiInit();
db = await databaseFactoryFfi.openDatabase("app.db");
} else {
db = await openDatabase("app.db");
}
await createTable(db, settingsDB);
await createTable(db, usersDB);
await createTable(db, userDataDB);
if ((await db.rawQuery("SELECT COUNT(*) FROM settings"))[0].values.first == 0) {
// Set default values for table Settings
await db.insert("settings", SettingsProvider.defaultSettings(database: database).toMap());
}
// Migrate Databases
try {
await migrateDB(
db,
struct: settingsDB,
defaultValues: SettingsProvider.defaultSettings(database: database).toMap(),
);
await migrateDB(
db,
struct: usersDB,
defaultValues: {"role": 0, "nickname": "", "picture": ""},
);
await migrateDB(db, struct: userDataDB, defaultValues: {
"grades": "[]", "timetable": "[]", "exams": "[]", "homework": "[]", "messages": "[]", "notes": "[]", "events": "[]", "absences": "[]",
"group_averages": "[]",
// renamed subjects // non kreta data
"renamed_subjects": "{}",
// "subject_lesson_count": "{}", // non kreta data
"last_seen_grade": 0,
});
} catch (error) {
print("ERROR: migrateDB: $error");
}
return db;
}
Future<void> migrateDB(
Database db, {
required DatabaseStruct struct,
required Map<String, Object?> defaultValues,
}) async {
var originalRows = await db.query(struct.table);
if (originalRows.isEmpty) {
await db.execute("drop table ${struct.table}");
await createTable(db, struct);
return;
}
List<Map<String, dynamic>> migrated = [];
// go through each row and add missing keys or delete non existing keys
await Future.forEach<Map<String, Object?>>(originalRows, (original) async {
bool migrationRequired = struct.struct.keys.any((key) => !original.containsKey(key) || original[key] == null) ||
original.keys.any((key) => !struct.struct.containsKey(key));
if (migrationRequired) {
print("INFO: Migrating ${struct.table}");
var copy = Map<String, Object?>.from(original);
// Fill missing columns
for (var key in struct.struct.keys) {
if (!original.containsKey(key) || original[key] == null) {
print("DEBUG: migrating $key");
copy[key] = defaultValues[key];
}
}
for (var key in original.keys) {
if (!struct.struct.keys.contains(key)) {
print("DEBUG: dropping $key");
copy.remove(key);
}
}
migrated.add(copy);
}
});
// replace the old table with the migrated one
if (migrated.isNotEmpty) {
// Delete table
await db.execute("drop table ${struct.table}");
// Recreate table
await createTable(db, struct);
await Future.forEach(migrated, (Map<String, Object?> copy) async {
await db.insert(struct.table, copy);
});
print("INFO: Database migrated");
}
}
// ignore_for_file: avoid_print
import 'dart:io';
import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/database/struct.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:sqflite/sqflite.dart';
// ignore: depend_on_referenced_packages
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
const settingsDB = DatabaseStruct("settings", {
"language": String, "start_page": int, "rounding": int, "theme": int, "accent_color": int, "news": int, "news_state": int, "developer_mode": int,
"update_channel": int, "config": String, "custom_accent_color": int, "custom_background_color": int, "custom_highlight_color": int, // general
"grade_color1": int, "grade_color2": int, "grade_color3": int, "grade_color4": int, "grade_color5": int, // grade colors
"vibration_strength": int, "ab_weeks": int, "swap_ab_weeks": int,
"notifications": int, "notifications_bitfield": int, "notification_poll_interval": int, // notifications
"x_filc_id": String, "graph_class_avg": int, "presentation_mode": int, "bell_delay": int, "bell_delay_enabled": int,
"grade_opening_fun": int, "icon_pack": String, "premium_scopes": String, "premium_token": String, "premium_login": String,
"last_account_id": String, "renamed_subjects_enabled": int,
});
// DON'T FORGET TO UPDATE DEFAULT VALUES IN `initDB` MIGRATION OR ELSE PARENTS WILL COMPLAIN ABOUT THEIR CHILDREN MISSING
// YOU'VE BEEN WARNED!!!
const usersDB = DatabaseStruct("users", {
"id": String, "name": String, "username": String, "password": String, "institute_code": String, "student": String, "role": int,
"nickname": String, "picture": String // premium only
});
const userDataDB = DatabaseStruct("user_data", {
"id": String, "grades": String, "timetable": String, "exams": String, "homework": String, "messages": String, "notes": String,
"events": String, "absences": String, "group_averages": String,
// renamed subjects // non kreta data
"renamed_subjects": String,
// "subject_lesson_count": String, // non kreta data
"last_seen_grade": int,
});
Future<void> createTable(Database db, DatabaseStruct struct) => db.execute("CREATE TABLE IF NOT EXISTS ${struct.table} ($struct)");
Future<Database> initDB(DatabaseProvider database) async {
Database db;
if (Platform.isLinux || Platform.isWindows) {
sqfliteFfiInit();
db = await databaseFactoryFfi.openDatabase("app.db");
} else {
db = await openDatabase("app.db");
}
await createTable(db, settingsDB);
await createTable(db, usersDB);
await createTable(db, userDataDB);
if ((await db.rawQuery("SELECT COUNT(*) FROM settings"))[0].values.first == 0) {
// Set default values for table Settings
await db.insert("settings", SettingsProvider.defaultSettings(database: database).toMap());
}
// Migrate Databases
try {
await migrateDB(
db,
struct: settingsDB,
defaultValues: SettingsProvider.defaultSettings(database: database).toMap(),
);
await migrateDB(
db,
struct: usersDB,
defaultValues: {"role": 0, "nickname": "", "picture": ""},
);
await migrateDB(db, struct: userDataDB, defaultValues: {
"grades": "[]", "timetable": "[]", "exams": "[]", "homework": "[]", "messages": "[]", "notes": "[]", "events": "[]", "absences": "[]",
"group_averages": "[]",
// renamed subjects // non kreta data
"renamed_subjects": "{}",
// "subject_lesson_count": "{}", // non kreta data
"last_seen_grade": 0,
});
} catch (error) {
print("ERROR: migrateDB: $error");
}
return db;
}
Future<void> migrateDB(
Database db, {
required DatabaseStruct struct,
required Map<String, Object?> defaultValues,
}) async {
var originalRows = await db.query(struct.table);
if (originalRows.isEmpty) {
await db.execute("drop table ${struct.table}");
await createTable(db, struct);
return;
}
List<Map<String, dynamic>> migrated = [];
// go through each row and add missing keys or delete non existing keys
await Future.forEach<Map<String, Object?>>(originalRows, (original) async {
bool migrationRequired = struct.struct.keys.any((key) => !original.containsKey(key) || original[key] == null) ||
original.keys.any((key) => !struct.struct.containsKey(key));
if (migrationRequired) {
print("INFO: Migrating ${struct.table}");
var copy = Map<String, Object?>.from(original);
// Fill missing columns
for (var key in struct.struct.keys) {
if (!original.containsKey(key) || original[key] == null) {
print("DEBUG: migrating $key");
copy[key] = defaultValues[key];
}
}
for (var key in original.keys) {
if (!struct.struct.keys.contains(key)) {
print("DEBUG: dropping $key");
copy.remove(key);
}
}
migrated.add(copy);
}
});
// replace the old table with the migrated one
if (migrated.isNotEmpty) {
// Delete table
await db.execute("drop table ${struct.table}");
// Recreate table
await createTable(db, struct);
await Future.forEach(migrated, (Map<String, Object?> copy) async {
await db.insert(struct.table, copy);
});
print("INFO: Database migrated");
}
}

View File

@@ -1,165 +1,165 @@
import 'dart:convert';
import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/models/subject_lesson_count.dart';
import 'package:filcnaplo/models/user.dart';
import 'package:filcnaplo_kreta_api/models/week.dart';
// ignore: depend_on_referenced_packages
import 'package:sqflite_common/sqlite_api.dart';
// Models
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo_kreta_api/models/lesson.dart';
import 'package:filcnaplo_kreta_api/models/exam.dart';
import 'package:filcnaplo_kreta_api/models/homework.dart';
import 'package:filcnaplo_kreta_api/models/message.dart';
import 'package:filcnaplo_kreta_api/models/note.dart';
import 'package:filcnaplo_kreta_api/models/event.dart';
import 'package:filcnaplo_kreta_api/models/absence.dart';
import 'package:filcnaplo_kreta_api/models/group_average.dart';
class DatabaseQuery {
DatabaseQuery({required this.db});
final Database db;
Future<SettingsProvider> getSettings(DatabaseProvider database) async {
Map settingsMap = (await db.query("settings")).elementAt(0);
SettingsProvider settings = SettingsProvider.fromMap(settingsMap, database: database);
return settings;
}
Future<UserProvider> getUsers(SettingsProvider settings) async {
var userProvider = UserProvider(settings: settings);
List<Map> usersMap = await db.query("users");
for (var user in usersMap) {
userProvider.addUser(User.fromMap(user));
}
if (userProvider.getUsers().map((e) => e.id).contains(settings.lastAccountId)) {
userProvider.setUser(settings.lastAccountId);
} else {
if (usersMap.isNotEmpty) {
userProvider.setUser(userProvider.getUsers().first.id);
settings.update(lastAccountId: userProvider.id);
}
}
return userProvider;
}
}
class UserDatabaseQuery {
UserDatabaseQuery({required this.db});
final Database db;
Future<List<Grade>> getGrades({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return [];
String? gradesJson = userData.elementAt(0)["grades"] as String?;
if (gradesJson == null) return [];
List<Grade> grades = (jsonDecode(gradesJson) as List).map((e) => Grade.fromJson(e)).toList();
return grades;
}
Future<Map<Week, List<Lesson>>> getLessons({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return {};
String? lessonsJson = userData.elementAt(0)["timetable"] as String?;
if (lessonsJson == null) return {};
if (jsonDecode(lessonsJson) is List) return {};
Map<Week, List<Lesson>> lessons = (jsonDecode(lessonsJson) as Map).cast<String, List>().map((key, value) {
return MapEntry(Week.fromId(int.parse(key)), value.cast<Map<String, Object?>>().map((e) => Lesson.fromJson(e)).toList());
}).cast();
return lessons;
}
Future<List<Exam>> getExams({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return [];
String? examsJson = userData.elementAt(0)["exams"] as String?;
if (examsJson == null) return [];
List<Exam> exams = (jsonDecode(examsJson) as List).map((e) => Exam.fromJson(e)).toList();
return exams;
}
Future<List<Homework>> getHomework({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return [];
String? homeworkJson = userData.elementAt(0)["homework"] as String?;
if (homeworkJson == null) return [];
List<Homework> homework = (jsonDecode(homeworkJson) as List).map((e) => Homework.fromJson(e)).toList();
return homework;
}
Future<List<Message>> getMessages({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return [];
String? messagesJson = userData.elementAt(0)["messages"] as String?;
if (messagesJson == null) return [];
List<Message> messages = (jsonDecode(messagesJson) as List).map((e) => Message.fromJson(e)).toList();
return messages;
}
Future<List<Note>> getNotes({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return [];
String? notesJson = userData.elementAt(0)["notes"] as String?;
if (notesJson == null) return [];
List<Note> notes = (jsonDecode(notesJson) as List).map((e) => Note.fromJson(e)).toList();
return notes;
}
Future<List<Event>> getEvents({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return [];
String? eventsJson = userData.elementAt(0)["events"] as String?;
if (eventsJson == null) return [];
List<Event> events = (jsonDecode(eventsJson) as List).map((e) => Event.fromJson(e)).toList();
return events;
}
Future<List<Absence>> getAbsences({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return [];
String? absencesJson = userData.elementAt(0)["absences"] as String?;
if (absencesJson == null) return [];
List<Absence> absences = (jsonDecode(absencesJson) as List).map((e) => Absence.fromJson(e)).toList();
return absences;
}
Future<List<GroupAverage>> getGroupAverages({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return [];
String? groupAveragesJson = userData.elementAt(0)["group_averages"] as String?;
if (groupAveragesJson == null) return [];
List<GroupAverage> groupAverages = (jsonDecode(groupAveragesJson) as List).map((e) => GroupAverage.fromJson(e)).toList();
return groupAverages;
}
Future<SubjectLessonCount> getSubjectLessonCount({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return SubjectLessonCount.fromMap({});
String? lessonCountJson = userData.elementAt(0)["subject_lesson_count"] as String?;
if (lessonCountJson == null) return SubjectLessonCount.fromMap({});
SubjectLessonCount lessonCount = SubjectLessonCount.fromMap(jsonDecode(lessonCountJson) as Map);
return lessonCount;
}
Future<DateTime> lastSeenGrade({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return DateTime(0);
int? lastSeenDate = userData.elementAt(0)["last_seen_grade"] as int?;
if (lastSeenDate == null) return DateTime(0);
DateTime lastSeen = DateTime.fromMillisecondsSinceEpoch(lastSeenDate);
return lastSeen;
}
Future<Map<String, String>> renamedSubjects({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return {};
String? renamedSubjectsJson = userData.elementAt(0)["renamed_subjects"] as String?;
if (renamedSubjectsJson == null) return {};
return (jsonDecode(renamedSubjectsJson) as Map).map((key, value) => MapEntry(key.toString(), value.toString()));
}
}
import 'dart:convert';
import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/models/subject_lesson_count.dart';
import 'package:filcnaplo/models/user.dart';
import 'package:filcnaplo_kreta_api/models/week.dart';
// ignore: depend_on_referenced_packages
import 'package:sqflite_common/sqlite_api.dart';
// Models
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo_kreta_api/models/lesson.dart';
import 'package:filcnaplo_kreta_api/models/exam.dart';
import 'package:filcnaplo_kreta_api/models/homework.dart';
import 'package:filcnaplo_kreta_api/models/message.dart';
import 'package:filcnaplo_kreta_api/models/note.dart';
import 'package:filcnaplo_kreta_api/models/event.dart';
import 'package:filcnaplo_kreta_api/models/absence.dart';
import 'package:filcnaplo_kreta_api/models/group_average.dart';
class DatabaseQuery {
DatabaseQuery({required this.db});
final Database db;
Future<SettingsProvider> getSettings(DatabaseProvider database) async {
Map settingsMap = (await db.query("settings")).elementAt(0);
SettingsProvider settings = SettingsProvider.fromMap(settingsMap, database: database);
return settings;
}
Future<UserProvider> getUsers(SettingsProvider settings) async {
var userProvider = UserProvider(settings: settings);
List<Map> usersMap = await db.query("users");
for (var user in usersMap) {
userProvider.addUser(User.fromMap(user));
}
if (userProvider.getUsers().map((e) => e.id).contains(settings.lastAccountId)) {
userProvider.setUser(settings.lastAccountId);
} else {
if (usersMap.isNotEmpty) {
userProvider.setUser(userProvider.getUsers().first.id);
settings.update(lastAccountId: userProvider.id);
}
}
return userProvider;
}
}
class UserDatabaseQuery {
UserDatabaseQuery({required this.db});
final Database db;
Future<List<Grade>> getGrades({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return [];
String? gradesJson = userData.elementAt(0)["grades"] as String?;
if (gradesJson == null) return [];
List<Grade> grades = (jsonDecode(gradesJson) as List).map((e) => Grade.fromJson(e)).toList();
return grades;
}
Future<Map<Week, List<Lesson>>> getLessons({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return {};
String? lessonsJson = userData.elementAt(0)["timetable"] as String?;
if (lessonsJson == null) return {};
if (jsonDecode(lessonsJson) is List) return {};
Map<Week, List<Lesson>> lessons = (jsonDecode(lessonsJson) as Map).cast<String, List>().map((key, value) {
return MapEntry(Week.fromId(int.parse(key)), value.cast<Map<String, Object?>>().map((e) => Lesson.fromJson(e)).toList());
}).cast();
return lessons;
}
Future<List<Exam>> getExams({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return [];
String? examsJson = userData.elementAt(0)["exams"] as String?;
if (examsJson == null) return [];
List<Exam> exams = (jsonDecode(examsJson) as List).map((e) => Exam.fromJson(e)).toList();
return exams;
}
Future<List<Homework>> getHomework({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return [];
String? homeworkJson = userData.elementAt(0)["homework"] as String?;
if (homeworkJson == null) return [];
List<Homework> homework = (jsonDecode(homeworkJson) as List).map((e) => Homework.fromJson(e)).toList();
return homework;
}
Future<List<Message>> getMessages({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return [];
String? messagesJson = userData.elementAt(0)["messages"] as String?;
if (messagesJson == null) return [];
List<Message> messages = (jsonDecode(messagesJson) as List).map((e) => Message.fromJson(e)).toList();
return messages;
}
Future<List<Note>> getNotes({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return [];
String? notesJson = userData.elementAt(0)["notes"] as String?;
if (notesJson == null) return [];
List<Note> notes = (jsonDecode(notesJson) as List).map((e) => Note.fromJson(e)).toList();
return notes;
}
Future<List<Event>> getEvents({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return [];
String? eventsJson = userData.elementAt(0)["events"] as String?;
if (eventsJson == null) return [];
List<Event> events = (jsonDecode(eventsJson) as List).map((e) => Event.fromJson(e)).toList();
return events;
}
Future<List<Absence>> getAbsences({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return [];
String? absencesJson = userData.elementAt(0)["absences"] as String?;
if (absencesJson == null) return [];
List<Absence> absences = (jsonDecode(absencesJson) as List).map((e) => Absence.fromJson(e)).toList();
return absences;
}
Future<List<GroupAverage>> getGroupAverages({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return [];
String? groupAveragesJson = userData.elementAt(0)["group_averages"] as String?;
if (groupAveragesJson == null) return [];
List<GroupAverage> groupAverages = (jsonDecode(groupAveragesJson) as List).map((e) => GroupAverage.fromJson(e)).toList();
return groupAverages;
}
Future<SubjectLessonCount> getSubjectLessonCount({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return SubjectLessonCount.fromMap({});
String? lessonCountJson = userData.elementAt(0)["subject_lesson_count"] as String?;
if (lessonCountJson == null) return SubjectLessonCount.fromMap({});
SubjectLessonCount lessonCount = SubjectLessonCount.fromMap(jsonDecode(lessonCountJson) as Map);
return lessonCount;
}
Future<DateTime> lastSeenGrade({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return DateTime(0);
int? lastSeenDate = userData.elementAt(0)["last_seen_grade"] as int?;
if (lastSeenDate == null) return DateTime(0);
DateTime lastSeen = DateTime.fromMillisecondsSinceEpoch(lastSeenDate);
return lastSeen;
}
Future<Map<String, String>> renamedSubjects({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return {};
String? renamedSubjectsJson = userData.elementAt(0)["renamed_subjects"] as String?;
if (renamedSubjectsJson == null) return {};
return (jsonDecode(renamedSubjectsJson) as Map).map((key, value) => MapEntry(key.toString(), value.toString()));
}
}

View File

@@ -1,112 +1,112 @@
import 'dart:convert';
import 'package:filcnaplo/models/subject_lesson_count.dart';
import 'package:filcnaplo_kreta_api/models/week.dart';
// ignore: depend_on_referenced_packages
import 'package:sqflite_common/sqlite_api.dart';
// Models
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/models/user.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo_kreta_api/models/lesson.dart';
import 'package:filcnaplo_kreta_api/models/exam.dart';
import 'package:filcnaplo_kreta_api/models/homework.dart';
import 'package:filcnaplo_kreta_api/models/message.dart';
import 'package:filcnaplo_kreta_api/models/note.dart';
import 'package:filcnaplo_kreta_api/models/event.dart';
import 'package:filcnaplo_kreta_api/models/absence.dart';
import 'package:filcnaplo_kreta_api/models/group_average.dart';
class DatabaseStore {
DatabaseStore({required this.db});
final Database db;
Future<void> storeSettings(SettingsProvider settings) async {
await db.update("settings", settings.toMap());
}
Future<void> storeUser(User user) async {
List userRes = await db.query("users", where: "id = ?", whereArgs: [user.id]);
if (userRes.isNotEmpty) {
await db.update("users", user.toMap(), where: "id = ?", whereArgs: [user.id]);
} else {
await db.insert("users", user.toMap());
await db.insert("user_data", {"id": user.id});
}
}
Future<void> removeUser(String userId) async {
await db.delete("users", where: "id = ?", whereArgs: [userId]);
await db.delete("user_data", where: "id = ?", whereArgs: [userId]);
}
}
class UserDatabaseStore {
UserDatabaseStore({required this.db});
final Database db;
Future<void> storeGrades(List<Grade> grades, {required String userId}) async {
String gradesJson = jsonEncode(grades.map((e) => e.json).toList());
await db.update("user_data", {"grades": gradesJson}, where: "id = ?", whereArgs: [userId]);
}
Future<void> storeLessons(Map<Week, List<Lesson>?> lessons, {required String userId}) async {
final map = lessons.map<String, List<Map<String, Object?>>>(
(k, v) => MapEntry(k.id.toString(), v!.where((e) => e.json != null).map((e) => e.json!).toList().cast()),
);
String lessonsJson = jsonEncode(map);
await db.update("user_data", {"timetable": lessonsJson}, where: "id = ?", whereArgs: [userId]);
}
Future<void> storeExams(List<Exam> exams, {required String userId}) async {
String examsJson = jsonEncode(exams.map((e) => e.json).toList());
await db.update("user_data", {"exams": examsJson}, where: "id = ?", whereArgs: [userId]);
}
Future<void> storeHomework(List<Homework> homework, {required String userId}) async {
String homeworkJson = jsonEncode(homework.map((e) => e.json).toList());
await db.update("user_data", {"homework": homeworkJson}, where: "id = ?", whereArgs: [userId]);
}
Future<void> storeMessages(List<Message> messages, {required String userId}) async {
String messagesJson = jsonEncode(messages.map((e) => e.json).toList());
await db.update("user_data", {"messages": messagesJson}, where: "id = ?", whereArgs: [userId]);
}
Future<void> storeNotes(List<Note> notes, {required String userId}) async {
String notesJson = jsonEncode(notes.map((e) => e.json).toList());
await db.update("user_data", {"notes": notesJson}, where: "id = ?", whereArgs: [userId]);
}
Future<void> storeEvents(List<Event> events, {required String userId}) async {
String eventsJson = jsonEncode(events.map((e) => e.json).toList());
await db.update("user_data", {"events": eventsJson}, where: "id = ?", whereArgs: [userId]);
}
Future<void> storeAbsences(List<Absence> absences, {required String userId}) async {
String absencesJson = jsonEncode(absences.map((e) => e.json).toList());
await db.update("user_data", {"absences": absencesJson}, where: "id = ?", whereArgs: [userId]);
}
Future<void> storeGroupAverages(List<GroupAverage> groupAverages, {required String userId}) async {
String groupAveragesJson = jsonEncode(groupAverages.map((e) => e.json).toList());
await db.update("user_data", {"group_averages": groupAveragesJson}, where: "id = ?", whereArgs: [userId]);
}
Future<void> storeSubjectLessonCount(SubjectLessonCount lessonCount, {required String userId}) async {
String lessonCountJson = jsonEncode(lessonCount.toMap());
await db.update("user_data", {"subject_lesson_count": lessonCountJson}, where: "id = ?", whereArgs: [userId]);
}
Future<void> storeLastSeenGrade(DateTime date, {required String userId}) async {
int lastSeenDate = date.millisecondsSinceEpoch;
await db.update("user_data", {"last_seen_grade": lastSeenDate}, where: "id = ?", whereArgs: [userId]);
}
Future<void> storeRenamedSubjects(Map<String, String> subjects, {required String userId}) async {
String renamedSubjectsJson = jsonEncode(subjects);
await db.update("user_data", {"renamed_subjects": renamedSubjectsJson}, where: "id = ?", whereArgs: [userId]);
}
}
import 'dart:convert';
import 'package:filcnaplo/models/subject_lesson_count.dart';
import 'package:filcnaplo_kreta_api/models/week.dart';
// ignore: depend_on_referenced_packages
import 'package:sqflite_common/sqlite_api.dart';
// Models
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/models/user.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo_kreta_api/models/lesson.dart';
import 'package:filcnaplo_kreta_api/models/exam.dart';
import 'package:filcnaplo_kreta_api/models/homework.dart';
import 'package:filcnaplo_kreta_api/models/message.dart';
import 'package:filcnaplo_kreta_api/models/note.dart';
import 'package:filcnaplo_kreta_api/models/event.dart';
import 'package:filcnaplo_kreta_api/models/absence.dart';
import 'package:filcnaplo_kreta_api/models/group_average.dart';
class DatabaseStore {
DatabaseStore({required this.db});
final Database db;
Future<void> storeSettings(SettingsProvider settings) async {
await db.update("settings", settings.toMap());
}
Future<void> storeUser(User user) async {
List userRes = await db.query("users", where: "id = ?", whereArgs: [user.id]);
if (userRes.isNotEmpty) {
await db.update("users", user.toMap(), where: "id = ?", whereArgs: [user.id]);
} else {
await db.insert("users", user.toMap());
await db.insert("user_data", {"id": user.id});
}
}
Future<void> removeUser(String userId) async {
await db.delete("users", where: "id = ?", whereArgs: [userId]);
await db.delete("user_data", where: "id = ?", whereArgs: [userId]);
}
}
class UserDatabaseStore {
UserDatabaseStore({required this.db});
final Database db;
Future<void> storeGrades(List<Grade> grades, {required String userId}) async {
String gradesJson = jsonEncode(grades.map((e) => e.json).toList());
await db.update("user_data", {"grades": gradesJson}, where: "id = ?", whereArgs: [userId]);
}
Future<void> storeLessons(Map<Week, List<Lesson>?> lessons, {required String userId}) async {
final map = lessons.map<String, List<Map<String, Object?>>>(
(k, v) => MapEntry(k.id.toString(), v!.where((e) => e.json != null).map((e) => e.json!).toList().cast()),
);
String lessonsJson = jsonEncode(map);
await db.update("user_data", {"timetable": lessonsJson}, where: "id = ?", whereArgs: [userId]);
}
Future<void> storeExams(List<Exam> exams, {required String userId}) async {
String examsJson = jsonEncode(exams.map((e) => e.json).toList());
await db.update("user_data", {"exams": examsJson}, where: "id = ?", whereArgs: [userId]);
}
Future<void> storeHomework(List<Homework> homework, {required String userId}) async {
String homeworkJson = jsonEncode(homework.map((e) => e.json).toList());
await db.update("user_data", {"homework": homeworkJson}, where: "id = ?", whereArgs: [userId]);
}
Future<void> storeMessages(List<Message> messages, {required String userId}) async {
String messagesJson = jsonEncode(messages.map((e) => e.json).toList());
await db.update("user_data", {"messages": messagesJson}, where: "id = ?", whereArgs: [userId]);
}
Future<void> storeNotes(List<Note> notes, {required String userId}) async {
String notesJson = jsonEncode(notes.map((e) => e.json).toList());
await db.update("user_data", {"notes": notesJson}, where: "id = ?", whereArgs: [userId]);
}
Future<void> storeEvents(List<Event> events, {required String userId}) async {
String eventsJson = jsonEncode(events.map((e) => e.json).toList());
await db.update("user_data", {"events": eventsJson}, where: "id = ?", whereArgs: [userId]);
}
Future<void> storeAbsences(List<Absence> absences, {required String userId}) async {
String absencesJson = jsonEncode(absences.map((e) => e.json).toList());
await db.update("user_data", {"absences": absencesJson}, where: "id = ?", whereArgs: [userId]);
}
Future<void> storeGroupAverages(List<GroupAverage> groupAverages, {required String userId}) async {
String groupAveragesJson = jsonEncode(groupAverages.map((e) => e.json).toList());
await db.update("user_data", {"group_averages": groupAveragesJson}, where: "id = ?", whereArgs: [userId]);
}
Future<void> storeSubjectLessonCount(SubjectLessonCount lessonCount, {required String userId}) async {
String lessonCountJson = jsonEncode(lessonCount.toMap());
await db.update("user_data", {"subject_lesson_count": lessonCountJson}, where: "id = ?", whereArgs: [userId]);
}
Future<void> storeLastSeenGrade(DateTime date, {required String userId}) async {
int lastSeenDate = date.millisecondsSinceEpoch;
await db.update("user_data", {"last_seen_grade": lastSeenDate}, where: "id = ?", whereArgs: [userId]);
}
Future<void> storeRenamedSubjects(Map<String, String> subjects, {required String userId}) async {
String renamedSubjectsJson = jsonEncode(subjects);
await db.update("user_data", {"renamed_subjects": renamedSubjectsJson}, where: "id = ?", whereArgs: [userId]);
}
}

View File

@@ -1,30 +1,30 @@
class DatabaseStruct {
final String table;
final Map<String, dynamic> struct;
const DatabaseStruct(this.table, this.struct);
String _toDBfield(String name, dynamic type) {
String typeName = "";
switch (type.runtimeType) {
case int:
typeName = "integer";
break;
case String:
typeName = "text";
break;
}
return "$name ${typeName.toUpperCase()} ${name == 'id' ? 'NOT NULL' : ''}";
}
@override
String toString() {
List<String> columns = [];
struct.forEach((key, value) {
columns.add(_toDBfield(key, value));
});
return columns.join(",");
}
}
class DatabaseStruct {
final String table;
final Map<String, dynamic> struct;
const DatabaseStruct(this.table, this.struct);
String _toDBfield(String name, dynamic type) {
String typeName = "";
switch (type.runtimeType) {
case int:
typeName = "integer";
break;
case String:
typeName = "text";
break;
}
return "$name ${typeName.toUpperCase()} ${name == 'id' ? 'NOT NULL' : ''}";
}
@override
String toString() {
List<String> columns = [];
struct.forEach((key, value) {
columns.add(_toDBfield(key, value));
});
return columns.join(",");
}
}

View File

@@ -1,56 +1,56 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'dart:typed_data';
import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo/helpers/storage_helper.dart';
import 'package:filcnaplo_kreta_api/client/client.dart';
import 'package:filcnaplo_kreta_api/models/attachment.dart';
import 'package:filcnaplo_kreta_api/models/homework.dart';
import 'package:flutter/widgets.dart';
import 'package:open_file/open_file.dart';
import 'package:provider/provider.dart';
extension AttachmentHelper on Attachment {
Future<String> download(BuildContext context, {bool overwrite = false}) async {
String downloads = await StorageHelper.downloadsPath();
if (!overwrite && await File("$downloads/$name").exists()) return "$downloads/$name";
Uint8List data = await Provider.of<KretaClient>(context, listen: false).getAPI(downloadUrl, rawResponse: true);
if (!await StorageHelper.write("$downloads/$name", data)) return "";
return "$downloads/$name";
}
Future<bool> open(BuildContext context) async {
String downloads = await StorageHelper.downloadsPath();
if (!await File("$downloads/$name").exists()) await download(context);
var result = await OpenFile.open("$downloads/$name");
return result.type == ResultType.done;
}
}
extension HomeworkAttachmentHelper on HomeworkAttachment {
Future<String> download(BuildContext context, {bool overwrite = false}) async {
String downloads = await StorageHelper.downloadsPath();
if (!overwrite && await File("$downloads/$name").exists()) return "$downloads/$name";
String url = downloadUrl(Provider.of<UserProvider>(context, listen: false).instituteCode ?? "");
Uint8List data = await Provider.of<KretaClient>(context, listen: false).getAPI(url, rawResponse: true);
if (!await StorageHelper.write("$downloads/$name", data)) return "";
return "$downloads/$name";
}
Future<bool> open(BuildContext context) async {
String downloads = await StorageHelper.downloadsPath();
if (!await File("$downloads/$name").exists()) await download(context);
var result = await OpenFile.open("$downloads/$name");
return result.type == ResultType.done;
}
}
// ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'dart:typed_data';
import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo/helpers/storage_helper.dart';
import 'package:filcnaplo_kreta_api/client/client.dart';
import 'package:filcnaplo_kreta_api/models/attachment.dart';
import 'package:filcnaplo_kreta_api/models/homework.dart';
import 'package:flutter/widgets.dart';
import 'package:open_file/open_file.dart';
import 'package:provider/provider.dart';
extension AttachmentHelper on Attachment {
Future<String> download(BuildContext context, {bool overwrite = false}) async {
String downloads = await StorageHelper.downloadsPath();
if (!overwrite && await File("$downloads/$name").exists()) return "$downloads/$name";
Uint8List data = await Provider.of<KretaClient>(context, listen: false).getAPI(downloadUrl, rawResponse: true);
if (!await StorageHelper.write("$downloads/$name", data)) return "";
return "$downloads/$name";
}
Future<bool> open(BuildContext context) async {
String downloads = await StorageHelper.downloadsPath();
if (!await File("$downloads/$name").exists()) await download(context);
var result = await OpenFile.open("$downloads/$name");
return result.type == ResultType.done;
}
}
extension HomeworkAttachmentHelper on HomeworkAttachment {
Future<String> download(BuildContext context, {bool overwrite = false}) async {
String downloads = await StorageHelper.downloadsPath();
if (!overwrite && await File("$downloads/$name").exists()) return "$downloads/$name";
String url = downloadUrl(Provider.of<UserProvider>(context, listen: false).instituteCode ?? "");
Uint8List data = await Provider.of<KretaClient>(context, listen: false).getAPI(url, rawResponse: true);
if (!await StorageHelper.write("$downloads/$name", data)) return "";
return "$downloads/$name";
}
Future<bool> open(BuildContext context) async {
String downloads = await StorageHelper.downloadsPath();
if (!await File("$downloads/$name").exists()) await download(context);
var result = await OpenFile.open("$downloads/$name");
return result.type == ResultType.done;
}
}

View File

@@ -1,21 +1,21 @@
import 'package:filcnaplo_kreta_api/models/grade.dart';
class AverageHelper {
static double averageEvals(List<Grade> grades, {bool finalAvg = false}) {
double average = 0.0;
List<String> ignoreInFinal = ["5,SzorgalomErtek", "4,MagatartasErtek"];
if (finalAvg) {
grades.removeWhere((e) => (e.value.value == 0) || (ignoreInFinal.contains(e.gradeType?.id)));
}
for (var e in grades) {
average += e.value.value * ((finalAvg ? 100 : e.value.weight) / 100);
}
average = average / grades.map((e) => (finalAvg ? 100 : e.value.weight) / 100).fold(0.0, (a, b) => a + b);
return average.isNaN ? 0.0 : average;
}
}
import 'package:filcnaplo_kreta_api/models/grade.dart';
class AverageHelper {
static double averageEvals(List<Grade> grades, {bool finalAvg = false}) {
double average = 0.0;
List<String> ignoreInFinal = ["5,SzorgalomErtek", "4,MagatartasErtek"];
if (finalAvg) {
grades.removeWhere((e) => (e.value.value == 0) || (ignoreInFinal.contains(e.gradeType?.id)));
}
for (var e in grades) {
average += e.value.value * ((finalAvg ? 100 : e.value.weight) / 100);
}
average = average / grades.map((e) => (finalAvg ? 100 : e.value.weight) / 100).fold(0.0, (a, b) => a + b);
return average.isNaN ? 0.0 : average;
}
}

View File

@@ -1,36 +1,36 @@
import 'package:flutter/cupertino.dart';
import 'package:quick_actions/quick_actions.dart';
import 'package:filcnaplo_mobile_ui/common/screens.i18n.dart';
const QuickActions quickActions = QuickActions();
void setupQuickActions() {
quickActions.setShortcutItems(<ShortcutItem>[
ShortcutItem(type: 'action_grades', localizedTitle: 'grades'.i18n, icon: 'ic_grades'),
ShortcutItem(type: 'action_timetable', localizedTitle: 'timetable'.i18n, icon: 'ic_timetable'),
ShortcutItem(type: 'action_messages', localizedTitle: 'messages'.i18n, icon: 'ic_messages'),
ShortcutItem(type: 'action_absences', localizedTitle: 'absences'.i18n, icon: 'ic_absences')
]);
}
void handleQuickActions(BuildContext context, void Function(String) callback) {
quickActions.initialize((shortcutType) {
switch (shortcutType) {
case 'action_home':
callback("home");
break;
case 'action_grades':
callback("grades");
break;
case 'action_timetable':
callback("timetable");
break;
case 'action_messages':
callback("messages");
break;
case 'action_absences':
callback("absences");
break;
}
});
}
import 'package:flutter/cupertino.dart';
import 'package:quick_actions/quick_actions.dart';
import 'package:filcnaplo_mobile_ui/common/screens.i18n.dart';
const QuickActions quickActions = QuickActions();
void setupQuickActions() {
quickActions.setShortcutItems(<ShortcutItem>[
ShortcutItem(type: 'action_grades', localizedTitle: 'grades'.i18n, icon: 'ic_grades'),
ShortcutItem(type: 'action_timetable', localizedTitle: 'timetable'.i18n, icon: 'ic_timetable'),
ShortcutItem(type: 'action_messages', localizedTitle: 'messages'.i18n, icon: 'ic_messages'),
ShortcutItem(type: 'action_absences', localizedTitle: 'absences'.i18n, icon: 'ic_absences')
]);
}
void handleQuickActions(BuildContext context, void Function(String) callback) {
quickActions.initialize((shortcutType) {
switch (shortcutType) {
case 'action_home':
callback("home");
break;
case 'action_grades':
callback("grades");
break;
case 'action_timetable':
callback("timetable");
break;
case 'action_messages':
callback("messages");
break;
case 'action_absences':
callback("absences");
break;
}
});
}

View File

@@ -1,15 +1,15 @@
import 'package:filcnaplo/helpers/attachment_helper.dart';
import 'package:filcnaplo_kreta_api/models/attachment.dart';
import 'package:flutter/widgets.dart';
import 'package:share_plus/share_plus.dart';
class ShareHelper {
static Future<void> shareText(String text, {String? subject}) => Share.share(text, subject: subject);
// ignore: deprecated_member_use
static Future<void> shareFile(String path, {String? text, String? subject}) => Share.shareFiles([path], text: text, subject: subject);
static Future<void> shareAttachment(Attachment attachment, {required BuildContext context}) async {
String path = await attachment.download(context);
await shareFile(path);
}
}
import 'package:filcnaplo/helpers/attachment_helper.dart';
import 'package:filcnaplo_kreta_api/models/attachment.dart';
import 'package:flutter/widgets.dart';
import 'package:share_plus/share_plus.dart';
class ShareHelper {
static Future<void> shareText(String text, {String? subject}) => Share.share(text, subject: subject);
// ignore: deprecated_member_use
static Future<void> shareFile(String path, {String? text, String? subject}) => Share.shareFiles([path], text: text, subject: subject);
static Future<void> shareAttachment(Attachment attachment, {required BuildContext context}) async {
String path = await attachment.download(context);
await shareFile(path);
}
}

View File

@@ -1,38 +1,38 @@
// ignore_for_file: avoid_print
import 'dart:io';
import 'dart:typed_data';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
class StorageHelper {
static Future<bool> write(String path, Uint8List data) async {
try {
if (await Permission.storage.request().isGranted) {
await File(path).writeAsBytes(data);
return true;
} else {
if (await Permission.storage.isPermanentlyDenied) {
openAppSettings();
}
return false;
}
} catch (error) {
print("ERROR: StorageHelper.write: $error");
return false;
}
}
static Future<String> downloadsPath() async {
String downloads;
if (Platform.isAndroid) {
downloads = "/storage/self/primary/Download";
} else {
downloads = (await getTemporaryDirectory()).path;
}
return downloads;
}
}
// ignore_for_file: avoid_print
import 'dart:io';
import 'dart:typed_data';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
class StorageHelper {
static Future<bool> write(String path, Uint8List data) async {
try {
if (await Permission.storage.request().isGranted) {
await File(path).writeAsBytes(data);
return true;
} else {
if (await Permission.storage.isPermanentlyDenied) {
openAppSettings();
}
return false;
}
} catch (error) {
print("ERROR: StorageHelper.write: $error");
return false;
}
}
static Future<String> downloadsPath() async {
String downloads;
if (Platform.isAndroid) {
downloads = "/storage/self/primary/Download";
} else {
downloads = (await getTemporaryDirectory()).path;
}
return downloads;
}
}

View File

@@ -1,144 +1,144 @@
import 'package:filcnaplo/icons/filc_icons.dart';
import 'package:filcnaplo/models/icon_pack.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo_kreta_api/models/subject.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
typedef SubjectIconVariants = Map<IconPack, IconData>;
class SubjectIconData {
final SubjectIconVariants data;
final String name; // for iOS live activities compatibilty
SubjectIconData({
this.data = const {
IconPack.material: Icons.widgets_outlined,
IconPack.cupertino: CupertinoIcons.rectangle_grid_2x2,
},
this.name = "square.grid.2x2",
});
}
SubjectIconVariants createIcon({required IconData material, required IconData cupertino}) {
return {
IconPack.material: material,
IconPack.cupertino: cupertino,
};
}
class SubjectIcon {
static String resolveName({Subject? subject, String? subjectName}) => _resolve(subject: subject, subjectName: subjectName).name;
static IconData resolveVariant({Subject? subject, String? subjectName, required BuildContext context}) =>
_resolve(subject: subject, subjectName: subjectName).data[Provider.of<SettingsProvider>(context, listen: false).iconPack]!;
static SubjectIconData _resolve({Subject? subject, String? subjectName}) {
assert(!(subject == null && subjectName == null));
String name = (subject?.name ?? subjectName ?? "").toLowerCase().specialChars().trim();
String category = subject?.category.description.toLowerCase().specialChars() ?? "";
// todo: check for categories
if (RegExp("mate(k|matika)").hasMatch(name) || category == "matematika") {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.function, material: Icons.calculate_outlined), name: "function");
} else if (RegExp("magyar nyelv|nyelvtan").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.textformat_alt, material: Icons.spellcheck_outlined), name: "textformat.alt");
} else if (RegExp("irodalom").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.book, material: Icons.menu_book_outlined), name: "book");
} else if (RegExp("tor(i|tenelem)").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.compass, material: Icons.hourglass_empty_outlined), name: "safari");
} else if (RegExp("foldrajz").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.map, material: Icons.public_outlined), name: "map");
} else if (RegExp("rajz|muvtori|muveszet|vizualis").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.paintbrush, material: Icons.palette_outlined), name: "paintbrush");
} else if (RegExp("fizika").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.lightbulb, material: Icons.emoji_objects_outlined), name: "lightbulb");
} else if (RegExp("^enek|zene|szolfezs|zongora|korus").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.music_note, material: Icons.music_note_outlined), name: "music.note");
} else if (RegExp("^tes(i|tneveles)|sport").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.sportscourt, material: Icons.sports_soccer_outlined), name: "sportscourt");
} else if (RegExp("kemia").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.lab_flask, material: Icons.science_outlined), name: "testtube.2");
} else if (RegExp("biologia").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.paw, material: Icons.pets_outlined), name: "pawprint");
} else if (RegExp("kornyezet|termeszet ?(tudomany|ismeret)|hon( es nep)?ismeret").hasMatch(name)) {
return SubjectIconData(
data: createIcon(cupertino: CupertinoIcons.arrow_3_trianglepath, material: Icons.eco_outlined), name: "arrow.3.trianglepath");
} else if (RegExp("(hit|erkolcs)tan|vallas|etika").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.heart, material: Icons.favorite_border_outlined), name: "heart");
} else if (RegExp("penzugy").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.money_dollar, material: Icons.savings_outlined), name: "dollarsign");
} else if (RegExp("informatika|szoftver|iroda|digitalis").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.device_laptop, material: Icons.computer_outlined), name: "laptopcomputer");
} else if (RegExp("prog").hasMatch(name)) {
return SubjectIconData(
data: createIcon(cupertino: CupertinoIcons.chevron_left_slash_chevron_right, material: Icons.code_outlined),
name: "chevron.left.forwardslash.chevron.right");
} else if (RegExp("halozat").hasMatch(name)) {
return SubjectIconData(
data: createIcon(cupertino: CupertinoIcons.antenna_radiowaves_left_right, material: Icons.wifi_tethering_outlined),
name: "antenna.radiowaves.left.and.right");
} else if (RegExp("szinhaz").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.hifispeaker, material: Icons.theater_comedy_outlined), name: "hifispeaker");
} else if (RegExp("film|media").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.film, material: Icons.theaters_outlined), name: "film");
} else if (RegExp("elektro(tech)?nika").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.bolt, material: Icons.electrical_services_outlined), name: "bolt");
} else if (RegExp("gepesz|mernok|ipar").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.wrench, material: Icons.precision_manufacturing_outlined), name: "wrench");
} else if (RegExp("technika").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.hammer, material: Icons.build_outlined), name: "hammer");
} else if (RegExp("tanc").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.music_mic, material: Icons.speaker_outlined), name: "music.mic");
} else if (RegExp("filozofia").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.bubble_left, material: Icons.psychology_outlined), name: "bubble.left");
} else if (RegExp("osztaly(fonoki|kozosseg)").hasMatch(name) || name == "ofo") {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.group, material: Icons.groups_outlined), name: "person.3");
} else if (RegExp("gazdasag").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.chart_pie, material: Icons.account_balance_outlined), name: "chart.pie");
} else if (RegExp("szorgalom").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.checkmark_seal, material: Icons.verified_outlined), name: "checkmark.seal");
} else if (RegExp("magatartas").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.smiley, material: Icons.emoji_people_outlined), name: "face.smiling");
} else if (RegExp("angol|nemet|francia|olasz|orosz|spanyol|latin|kinai|nyelv").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.globe, material: Icons.translate_outlined), name: "globe");
} else if (RegExp("linux").hasMatch(name)) {
return SubjectIconData(data: createIcon(material: FilcIcons.linux, cupertino: FilcIcons.linux));
}
return SubjectIconData();
}
}
class ShortSubject {
static String resolve({Subject? subject, String? subjectName}) {
assert(!(subject == null && subjectName == null));
String name = (subject?.name ?? subjectName ?? "").toLowerCase().specialChars().trim();
// String category = subject?.category.description.toLowerCase().specialChars() ?? "";
if (RegExp("magyar irodalom").hasMatch(name)) {
return "Irodalom";
} else if (RegExp("magyar nyelv").hasMatch(name)) {
return "Nyelvtan";
} else if (RegExp("matematika").hasMatch(name)) {
return "Matek";
} else if (RegExp("digitalis kultura").hasMatch(name)) {
return "Dig. kult.";
} else if (RegExp("testneveles").hasMatch(name)) {
return "Tesi";
} else if (RegExp("tortenelem").hasMatch(name)) {
return "Töri";
} else if (RegExp("(angol|nemet|francia|olasz|orosz|spanyol|latin|kinai) nyelv").hasMatch(name)) {
return (subject?.name ?? subjectName ?? "?").replaceAll(" nyelv", "");
} else if (RegExp("informatika").hasMatch(name)) {
return "Infó";
} else if (RegExp("osztalyfonoki").hasMatch(name)) {
return "Ofő";
}
return subject?.name.capital() ?? subjectName?.capital() ?? "?";
}
}
import 'package:filcnaplo/icons/filc_icons.dart';
import 'package:filcnaplo/models/icon_pack.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo_kreta_api/models/subject.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
typedef SubjectIconVariants = Map<IconPack, IconData>;
class SubjectIconData {
final SubjectIconVariants data;
final String name; // for iOS live activities compatibilty
SubjectIconData({
this.data = const {
IconPack.material: Icons.widgets_outlined,
IconPack.cupertino: CupertinoIcons.rectangle_grid_2x2,
},
this.name = "square.grid.2x2",
});
}
SubjectIconVariants createIcon({required IconData material, required IconData cupertino}) {
return {
IconPack.material: material,
IconPack.cupertino: cupertino,
};
}
class SubjectIcon {
static String resolveName({Subject? subject, String? subjectName}) => _resolve(subject: subject, subjectName: subjectName).name;
static IconData resolveVariant({Subject? subject, String? subjectName, required BuildContext context}) =>
_resolve(subject: subject, subjectName: subjectName).data[Provider.of<SettingsProvider>(context, listen: false).iconPack]!;
static SubjectIconData _resolve({Subject? subject, String? subjectName}) {
assert(!(subject == null && subjectName == null));
String name = (subject?.name ?? subjectName ?? "").toLowerCase().specialChars().trim();
String category = subject?.category.description.toLowerCase().specialChars() ?? "";
// todo: check for categories
if (RegExp("mate(k|matika)").hasMatch(name) || category == "matematika") {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.function, material: Icons.calculate_outlined), name: "function");
} else if (RegExp("magyar nyelv|nyelvtan").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.textformat_alt, material: Icons.spellcheck_outlined), name: "textformat.alt");
} else if (RegExp("irodalom").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.book, material: Icons.menu_book_outlined), name: "book");
} else if (RegExp("tor(i|tenelem)").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.compass, material: Icons.hourglass_empty_outlined), name: "safari");
} else if (RegExp("foldrajz").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.map, material: Icons.public_outlined), name: "map");
} else if (RegExp("rajz|muvtori|muveszet|vizualis").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.paintbrush, material: Icons.palette_outlined), name: "paintbrush");
} else if (RegExp("fizika").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.lightbulb, material: Icons.emoji_objects_outlined), name: "lightbulb");
} else if (RegExp("^enek|zene|szolfezs|zongora|korus").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.music_note, material: Icons.music_note_outlined), name: "music.note");
} else if (RegExp("^tes(i|tneveles)|sport").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.sportscourt, material: Icons.sports_soccer_outlined), name: "sportscourt");
} else if (RegExp("kemia").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.lab_flask, material: Icons.science_outlined), name: "testtube.2");
} else if (RegExp("biologia").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.paw, material: Icons.pets_outlined), name: "pawprint");
} else if (RegExp("kornyezet|termeszet ?(tudomany|ismeret)|hon( es nep)?ismeret").hasMatch(name)) {
return SubjectIconData(
data: createIcon(cupertino: CupertinoIcons.arrow_3_trianglepath, material: Icons.eco_outlined), name: "arrow.3.trianglepath");
} else if (RegExp("(hit|erkolcs)tan|vallas|etika").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.heart, material: Icons.favorite_border_outlined), name: "heart");
} else if (RegExp("penzugy").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.money_dollar, material: Icons.savings_outlined), name: "dollarsign");
} else if (RegExp("informatika|szoftver|iroda|digitalis").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.device_laptop, material: Icons.computer_outlined), name: "laptopcomputer");
} else if (RegExp("prog").hasMatch(name)) {
return SubjectIconData(
data: createIcon(cupertino: CupertinoIcons.chevron_left_slash_chevron_right, material: Icons.code_outlined),
name: "chevron.left.forwardslash.chevron.right");
} else if (RegExp("halozat").hasMatch(name)) {
return SubjectIconData(
data: createIcon(cupertino: CupertinoIcons.antenna_radiowaves_left_right, material: Icons.wifi_tethering_outlined),
name: "antenna.radiowaves.left.and.right");
} else if (RegExp("szinhaz").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.hifispeaker, material: Icons.theater_comedy_outlined), name: "hifispeaker");
} else if (RegExp("film|media").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.film, material: Icons.theaters_outlined), name: "film");
} else if (RegExp("elektro(tech)?nika").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.bolt, material: Icons.electrical_services_outlined), name: "bolt");
} else if (RegExp("gepesz|mernok|ipar").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.wrench, material: Icons.precision_manufacturing_outlined), name: "wrench");
} else if (RegExp("technika").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.hammer, material: Icons.build_outlined), name: "hammer");
} else if (RegExp("tanc").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.music_mic, material: Icons.speaker_outlined), name: "music.mic");
} else if (RegExp("filozofia").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.bubble_left, material: Icons.psychology_outlined), name: "bubble.left");
} else if (RegExp("osztaly(fonoki|kozosseg)").hasMatch(name) || name == "ofo") {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.group, material: Icons.groups_outlined), name: "person.3");
} else if (RegExp("gazdasag").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.chart_pie, material: Icons.account_balance_outlined), name: "chart.pie");
} else if (RegExp("szorgalom").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.checkmark_seal, material: Icons.verified_outlined), name: "checkmark.seal");
} else if (RegExp("magatartas").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.smiley, material: Icons.emoji_people_outlined), name: "face.smiling");
} else if (RegExp("angol|nemet|francia|olasz|orosz|spanyol|latin|kinai|nyelv").hasMatch(name)) {
return SubjectIconData(data: createIcon(cupertino: CupertinoIcons.globe, material: Icons.translate_outlined), name: "globe");
} else if (RegExp("linux").hasMatch(name)) {
return SubjectIconData(data: createIcon(material: FilcIcons.linux, cupertino: FilcIcons.linux));
}
return SubjectIconData();
}
}
class ShortSubject {
static String resolve({Subject? subject, String? subjectName}) {
assert(!(subject == null && subjectName == null));
String name = (subject?.name ?? subjectName ?? "").toLowerCase().specialChars().trim();
// String category = subject?.category.description.toLowerCase().specialChars() ?? "";
if (RegExp("magyar irodalom").hasMatch(name)) {
return "Irodalom";
} else if (RegExp("magyar nyelv").hasMatch(name)) {
return "Nyelvtan";
} else if (RegExp("matematika").hasMatch(name)) {
return "Matek";
} else if (RegExp("digitalis kultura").hasMatch(name)) {
return "Dig. kult.";
} else if (RegExp("testneveles").hasMatch(name)) {
return "Tesi";
} else if (RegExp("tortenelem").hasMatch(name)) {
return "Töri";
} else if (RegExp("(angol|nemet|francia|olasz|orosz|spanyol|latin|kinai) nyelv").hasMatch(name)) {
return (subject?.name ?? subjectName ?? "?").replaceAll(" nyelv", "");
} else if (RegExp("informatika").hasMatch(name)) {
return "Infó";
} else if (RegExp("osztalyfonoki").hasMatch(name)) {
return "Ofő";
}
return subject?.name.capital() ?? subjectName?.capital() ?? "?";
}
}

View File

@@ -1,71 +1,71 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:filcnaplo/api/client.dart';
import 'package:filcnaplo/helpers/storage_helper.dart';
import 'package:filcnaplo/models/release.dart';
import 'package:open_file/open_file.dart';
enum UpdateState { none, preparing, downloading, installing }
typedef UpdateCallback = Function(double progress, UpdateState state);
// ignore: todo
// TODO: cleanup old apk files
extension UpdateHelper on Release {
Future<void> install({UpdateCallback? updateCallback}) async {
updateCallback!(-1, UpdateState.preparing);
String downloads = await StorageHelper.downloadsPath();
File apk = File("$downloads/filcnaplo-$version.apk");
if (!await apk.exists()) {
updateCallback(-1, UpdateState.downloading);
var bytes = await download(updateCallback: updateCallback);
if (!await StorageHelper.write(apk.path, bytes)) throw "failed to write apk: permission denied";
}
updateCallback(-1, UpdateState.installing);
var result = await OpenFile.open(apk.path);
if (result.type != ResultType.done) {
// ignore: avoid_print
print("ERROR: installUpdate.openFile: ${result.message}");
throw result.message;
}
updateCallback(-1, UpdateState.none);
}
Future<Uint8List> download({UpdateCallback? updateCallback}) async {
var response = await FilcAPI.downloadRelease(downloads.first);
List<List<int>> chunks = [];
int downloaded = 0;
var completer = Completer<Uint8List>();
response?.stream.listen((List<int> chunk) {
updateCallback!(downloaded / (response.contentLength ?? 0), UpdateState.downloading);
chunks.add(chunk);
downloaded += chunk.length;
}, onDone: () {
// Save the file
final Uint8List bytes = Uint8List(response.contentLength ?? 0);
int offset = 0;
for (List<int> chunk in chunks) {
bytes.setRange(offset, offset + chunk.length, chunk);
offset += chunk.length;
}
completer.complete(bytes);
});
return completer.future;
}
}
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:filcnaplo/api/client.dart';
import 'package:filcnaplo/helpers/storage_helper.dart';
import 'package:filcnaplo/models/release.dart';
import 'package:open_file/open_file.dart';
enum UpdateState { none, preparing, downloading, installing }
typedef UpdateCallback = Function(double progress, UpdateState state);
// ignore: todo
// TODO: cleanup old apk files
extension UpdateHelper on Release {
Future<void> install({UpdateCallback? updateCallback}) async {
updateCallback!(-1, UpdateState.preparing);
String downloads = await StorageHelper.downloadsPath();
File apk = File("$downloads/filcnaplo-$version.apk");
if (!await apk.exists()) {
updateCallback(-1, UpdateState.downloading);
var bytes = await download(updateCallback: updateCallback);
if (!await StorageHelper.write(apk.path, bytes)) throw "failed to write apk: permission denied";
}
updateCallback(-1, UpdateState.installing);
var result = await OpenFile.open(apk.path);
if (result.type != ResultType.done) {
// ignore: avoid_print
print("ERROR: installUpdate.openFile: ${result.message}");
throw result.message;
}
updateCallback(-1, UpdateState.none);
}
Future<Uint8List> download({UpdateCallback? updateCallback}) async {
var response = await FilcAPI.downloadRelease(downloads.first);
List<List<int>> chunks = [];
int downloaded = 0;
var completer = Completer<Uint8List>();
response?.stream.listen((List<int> chunk) {
updateCallback!(downloaded / (response.contentLength ?? 0), UpdateState.downloading);
chunks.add(chunk);
downloaded += chunk.length;
}, onDone: () {
// Save the file
final Uint8List bytes = Uint8List(response.contentLength ?? 0);
int offset = 0;
for (List<int> chunk in chunks) {
bytes.setRange(offset, offset + chunk.length, chunk);
offset += chunk.length;
}
completer.complete(bytes);
});
return completer.future;
}
}

View File

@@ -1,43 +1,43 @@
import 'package:flutter/widgets.dart';
class FilcIcons {
FilcIcons._();
static const iconFontFamily = 'FilcIcons';
/// home
static const IconData home = IconData(0x21, fontFamily: iconFontFamily);
/// linux
static const IconData linux = IconData(0x22, fontFamily: iconFontFamily);
/// upstairs
static const IconData upstairs = IconData(0x23, fontFamily: iconFontFamily);
/// downstairs
static const IconData downstairs = IconData(0x24, fontFamily: iconFontFamily);
/// premium
static const IconData premium = IconData(0x25, fontFamily: iconFontFamily);
/// tinta
static const IconData tinta = IconData(0x26, fontFamily: iconFontFamily);
/// kupak
static const IconData kupak = IconData(0x27, fontFamily: iconFontFamily);
/// homefill
static const IconData homefill = IconData(0x28, fontFamily: iconFontFamily);
/// gradesfill
static const IconData gradesfill = IconData(0x29, fontFamily: iconFontFamily);
/// timetablefill
static const IconData timetablefill = IconData(0x2a, fontFamily: iconFontFamily);
/// messagesfill
static const IconData messagesfill = IconData(0x2b, fontFamily: iconFontFamily);
/// absencesfill
static const IconData absencesfill = IconData(0x2c, fontFamily: iconFontFamily);
}
import 'package:flutter/widgets.dart';
class FilcIcons {
FilcIcons._();
static const iconFontFamily = 'FilcIcons';
/// home
static const IconData home = IconData(0x21, fontFamily: iconFontFamily);
/// linux
static const IconData linux = IconData(0x22, fontFamily: iconFontFamily);
/// upstairs
static const IconData upstairs = IconData(0x23, fontFamily: iconFontFamily);
/// downstairs
static const IconData downstairs = IconData(0x24, fontFamily: iconFontFamily);
/// premium
static const IconData premium = IconData(0x25, fontFamily: iconFontFamily);
/// tinta
static const IconData tinta = IconData(0x26, fontFamily: iconFontFamily);
/// kupak
static const IconData kupak = IconData(0x27, fontFamily: iconFontFamily);
/// homefill
static const IconData homefill = IconData(0x28, fontFamily: iconFontFamily);
/// gradesfill
static const IconData gradesfill = IconData(0x29, fontFamily: iconFontFamily);
/// timetablefill
static const IconData timetablefill = IconData(0x2a, fontFamily: iconFontFamily);
/// messagesfill
static const IconData messagesfill = IconData(0x2b, fontFamily: iconFontFamily);
/// absencesfill
static const IconData absencesfill = IconData(0x2c, fontFamily: iconFontFamily);
}

View File

@@ -1,76 +1,76 @@
import 'package:background_fetch/background_fetch.dart';
import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/database/init.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:filcnaplo/app.dart';
import 'package:flutter/services.dart';
import 'package:filcnaplo_mobile_ui/screens/error_screen.dart';
import 'package:filcnaplo_mobile_ui/screens/error_report_screen.dart';
void main() async {
// Initalize
WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized();
binding.renderView.automaticSystemUiAdjustment = false;
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
// Startup
Startup startup = Startup();
await startup.start();
// Custom error page
ErrorWidget.builder = errorBuilder;
BackgroundFetch.registerHeadlessTask(backgroundHeadlessTask);
// Run App
runApp(App(database: startup.database, settings: startup.settings, user: startup.user));
}
class Startup {
late SettingsProvider settings;
late UserProvider user;
late DatabaseProvider database;
Future<void> start() async {
database = DatabaseProvider();
var db = await initDB(database);
await db.close();
await database.init();
settings = await database.query.getSettings(database);
user = await database.query.getUsers(settings);
}
}
bool errorShown = false;
String lastException = '';
Widget errorBuilder(FlutterErrorDetails details) {
return Builder(builder: (context) {
if (Navigator.of(context).canPop()) Navigator.pop(context);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!errorShown && details.exceptionAsString() != lastException) {
errorShown = true;
lastException = details.exceptionAsString();
Navigator.of(context, rootNavigator: true).push(MaterialPageRoute(builder: (context) {
if (kReleaseMode) {
return ErrorReportScreen(details);
} else {
return ErrorScreen(details);
}
})).then((_) => errorShown = false);
}
});
return Container();
});
}
@pragma('vm:entry-point')
void backgroundHeadlessTask(HeadlessTask task) {
print('[BackgroundFetch] Headless event received.');
BackgroundFetch.finish(task.taskId);
}
import 'package:background_fetch/background_fetch.dart';
import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/database/init.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:filcnaplo/app.dart';
import 'package:flutter/services.dart';
import 'package:filcnaplo_mobile_ui/screens/error_screen.dart';
import 'package:filcnaplo_mobile_ui/screens/error_report_screen.dart';
void main() async {
// Initalize
WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized();
binding.renderView.automaticSystemUiAdjustment = false;
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
// Startup
Startup startup = Startup();
await startup.start();
// Custom error page
ErrorWidget.builder = errorBuilder;
BackgroundFetch.registerHeadlessTask(backgroundHeadlessTask);
// Run App
runApp(App(database: startup.database, settings: startup.settings, user: startup.user));
}
class Startup {
late SettingsProvider settings;
late UserProvider user;
late DatabaseProvider database;
Future<void> start() async {
database = DatabaseProvider();
var db = await initDB(database);
await db.close();
await database.init();
settings = await database.query.getSettings(database);
user = await database.query.getUsers(settings);
}
}
bool errorShown = false;
String lastException = '';
Widget errorBuilder(FlutterErrorDetails details) {
return Builder(builder: (context) {
if (Navigator.of(context).canPop()) Navigator.pop(context);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!errorShown && details.exceptionAsString() != lastException) {
errorShown = true;
lastException = details.exceptionAsString();
Navigator.of(context, rootNavigator: true).push(MaterialPageRoute(builder: (context) {
if (kReleaseMode) {
return ErrorReportScreen(details);
} else {
return ErrorScreen(details);
}
})).then((_) => errorShown = false);
}
});
return Container();
});
}
@pragma('vm:entry-point')
void backgroundHeadlessTask(HeadlessTask task) {
print('[BackgroundFetch] Headless event received.');
BackgroundFetch.finish(task.taskId);
}

View File

@@ -1,37 +1,37 @@
import 'dart:io';
class Config {
String _userAgent;
Map? json;
static const String _version = String.fromEnvironment("APPVER", defaultValue: "2.2.0");
Config({required String userAgent, this.json}) : _userAgent = userAgent;
factory Config.fromJson(Map json) {
return Config(
userAgent: json["user_agent"] ?? "hu.filc.naplo/\$0/\$1/\$2",
json: json,
);
}
String get userAgent => _userAgent.replaceAll("\$0", _version).replaceAll("\$1", platform).replaceAll("\$2", "0");
static String get platform {
if (Platform.isAndroid) {
return "Android";
} else if (Platform.isIOS) {
return "iOS";
} else if (Platform.isLinux) {
return "Linux";
} else if (Platform.isWindows) {
return "Windows";
} else if (Platform.isMacOS) {
return "MacOS";
} else {
return "Unknown";
}
}
@override
String toString() => json.toString();
}
import 'dart:io';
class Config {
String _userAgent;
Map? json;
static const String _version = String.fromEnvironment("APPVER", defaultValue: "2.2.0");
Config({required String userAgent, this.json}) : _userAgent = userAgent;
factory Config.fromJson(Map json) {
return Config(
userAgent: json["user_agent"] ?? "hu.filc.naplo/\$0/\$1/\$2",
json: json,
);
}
String get userAgent => _userAgent.replaceAll("\$0", _version).replaceAll("\$1", platform).replaceAll("\$2", "0");
static String get platform {
if (Platform.isAndroid) {
return "Android";
} else if (Platform.isIOS) {
return "iOS";
} else if (Platform.isLinux) {
return "Linux";
} else if (Platform.isWindows) {
return "Windows";
} else if (Platform.isMacOS) {
return "MacOS";
} else {
return "Unknown";
}
}
@override
String toString() => json.toString();
}

View File

@@ -1 +1 @@
enum IconPack { material, cupertino }
enum IconPack { material, cupertino }

View File

@@ -1,31 +1,31 @@
class News {
String title;
String content;
String link;
String openLabel;
String platform;
bool emergency;
Map? json;
News({
required this.title,
required this.content,
required this.link,
required this.openLabel,
required this.platform,
required this.emergency,
this.json,
});
factory News.fromJson(Map json) {
return News(
title: json["title"] ?? "",
content: json["content"] ?? "",
link: json["link"] ?? "",
openLabel: json["open_label"] ?? "",
platform: json["platform"] ?? "",
emergency: json["emergency"] ?? false,
json: json,
);
}
}
class News {
String title;
String content;
String link;
String openLabel;
String platform;
bool emergency;
Map? json;
News({
required this.title,
required this.content,
required this.link,
required this.openLabel,
required this.platform,
required this.emergency,
this.json,
});
factory News.fromJson(Map json) {
return News(
title: json["title"] ?? "",
content: json["content"] ?? "",
link: json["link"] ?? "",
openLabel: json["open_label"] ?? "",
platform: json["platform"] ?? "",
emergency: json["emergency"] ?? false,
json: json,
);
}
}

View File

@@ -1,151 +1,151 @@
class ReleaseDownload {
String url;
int size;
ReleaseDownload({
required this.url,
required this.size,
});
factory ReleaseDownload.fromJson(Map json) {
return ReleaseDownload(
url: json["browser_download_url"] ?? "",
size: json["size"] ?? 0,
);
}
}
class Release {
String tag;
Version version;
String author;
String body;
List<ReleaseDownload> downloads;
bool prerelease;
Release({
required this.tag,
required this.author,
required this.body,
required this.downloads,
required this.prerelease,
required this.version,
});
factory Release.fromJson(Map json) {
return Release(
tag: json["tag_name"] ?? Version.zero.toString(),
author: json["author"] != null ? json["author"]["login"] ?? "" : "",
body: json["body"] ?? "",
downloads: json["assets"] != null ? json["assets"].map((a) => ReleaseDownload.fromJson(a)).toList().cast<ReleaseDownload>() : [],
prerelease: json["prerelease"] ?? false,
version: Version.fromString(json["tag_name"] ?? ""),
);
}
}
class Version {
final int major;
final int minor;
final int patch;
final String prerelease;
final int prever;
const Version(this.major, this.minor, this.patch, {this.prerelease = "", this.prever = 0});
factory Version.fromString(String o) {
String string = o;
int x = 0, y = 0, z = 0; // major, minor, patch (1.1.1)
String pre = ""; // prerelease string (-beta)
int prev = 0; // prerelease version (.1)
try {
// cut build
string = string.split("+")[0];
// cut prerelease
var p = string.split("-");
string = p[0];
if (p.length > 1) pre = p[1];
// prerelease
p = pre.split(".");
if (p.length > 1) prev = int.tryParse(p[1]) ?? 0;
// check for valid prerelease name
if (p[0] != "") {
if (prereleases.contains(p[0].toLowerCase().trim())) {
pre = p[0];
} else {
throw "invalid prerelease name: ${p[0]}";
}
}
// core
p = string.split(".");
if (p.length != 3) throw "invalid core length: ${p.length}";
x = int.tryParse(p[0]) ?? 0;
y = int.tryParse(p[1]) ?? 0;
z = int.tryParse(p[2]) ?? 0;
return Version(x, y, z, prerelease: pre, prever: prev);
} catch (error) {
// ignore: avoid_print
print("WARNING: Failed to parse version ($o): $error");
return Version.zero;
}
}
@override
bool operator ==(other) {
if (other is! Version) return false;
return other.major == major && other.minor == minor && other.patch == patch && other.prei == prei && other.prever == prever;
}
int compareTo(Version other) {
if (other == this) return 0;
if (major > other.major) {
return 1;
} else if (major == other.major) {
if (minor > other.minor) {
return 1;
} else if (minor == other.minor) {
if (patch > other.patch) {
return 1;
} else if (patch == other.patch) {
if (prei > other.prei) {
return 1;
} else if (other.prei == prei) {
if (prever > other.prever) {
return 1;
}
}
}
}
}
return -1;
}
@override
String toString() {
String str = "$major.$minor.$patch";
if (prerelease != "") str += "-$prerelease";
if (prever != 0) str += ".$prever";
return str;
}
int get prei {
if (prerelease != "") return prereleases.indexOf(prerelease);
return prereleases.length;
}
static const zero = Version(0, 0, 0);
static const List<String> prereleases = ["dev", "pre", "alpha", "beta", "rc", "nightly", "test"];
@override
int get hashCode => toString().hashCode;
}
class ReleaseDownload {
String url;
int size;
ReleaseDownload({
required this.url,
required this.size,
});
factory ReleaseDownload.fromJson(Map json) {
return ReleaseDownload(
url: json["browser_download_url"] ?? "",
size: json["size"] ?? 0,
);
}
}
class Release {
String tag;
Version version;
String author;
String body;
List<ReleaseDownload> downloads;
bool prerelease;
Release({
required this.tag,
required this.author,
required this.body,
required this.downloads,
required this.prerelease,
required this.version,
});
factory Release.fromJson(Map json) {
return Release(
tag: json["tag_name"] ?? Version.zero.toString(),
author: json["author"] != null ? json["author"]["login"] ?? "" : "",
body: json["body"] ?? "",
downloads: json["assets"] != null ? json["assets"].map((a) => ReleaseDownload.fromJson(a)).toList().cast<ReleaseDownload>() : [],
prerelease: json["prerelease"] ?? false,
version: Version.fromString(json["tag_name"] ?? ""),
);
}
}
class Version {
final int major;
final int minor;
final int patch;
final String prerelease;
final int prever;
const Version(this.major, this.minor, this.patch, {this.prerelease = "", this.prever = 0});
factory Version.fromString(String o) {
String string = o;
int x = 0, y = 0, z = 0; // major, minor, patch (1.1.1)
String pre = ""; // prerelease string (-beta)
int prev = 0; // prerelease version (.1)
try {
// cut build
string = string.split("+")[0];
// cut prerelease
var p = string.split("-");
string = p[0];
if (p.length > 1) pre = p[1];
// prerelease
p = pre.split(".");
if (p.length > 1) prev = int.tryParse(p[1]) ?? 0;
// check for valid prerelease name
if (p[0] != "") {
if (prereleases.contains(p[0].toLowerCase().trim())) {
pre = p[0];
} else {
throw "invalid prerelease name: ${p[0]}";
}
}
// core
p = string.split(".");
if (p.length != 3) throw "invalid core length: ${p.length}";
x = int.tryParse(p[0]) ?? 0;
y = int.tryParse(p[1]) ?? 0;
z = int.tryParse(p[2]) ?? 0;
return Version(x, y, z, prerelease: pre, prever: prev);
} catch (error) {
// ignore: avoid_print
print("WARNING: Failed to parse version ($o): $error");
return Version.zero;
}
}
@override
bool operator ==(other) {
if (other is! Version) return false;
return other.major == major && other.minor == minor && other.patch == patch && other.prei == prei && other.prever == prever;
}
int compareTo(Version other) {
if (other == this) return 0;
if (major > other.major) {
return 1;
} else if (major == other.major) {
if (minor > other.minor) {
return 1;
} else if (minor == other.minor) {
if (patch > other.patch) {
return 1;
} else if (patch == other.patch) {
if (prei > other.prei) {
return 1;
} else if (other.prei == prei) {
if (prever > other.prever) {
return 1;
}
}
}
}
}
return -1;
}
@override
String toString() {
String str = "$major.$minor.$patch";
if (prerelease != "") str += "-$prerelease";
if (prever != 0) str += ".$prever";
return str;
}
int get prei {
if (prerelease != "") return prereleases.indexOf(prerelease);
return prereleases.length;
}
static const zero = Version(0, 0, 0);
static const List<String> prereleases = ["dev", "pre", "alpha", "beta", "rc", "nightly", "test"];
@override
int get hashCode => toString().hashCode;
}

View File

@@ -1,390 +1,390 @@
import 'dart:convert';
import 'dart:developer';
import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/models/config.dart';
import 'package:filcnaplo/models/icon_pack.dart';
import 'package:filcnaplo/theme/colors/accent.dart';
import 'package:filcnaplo/theme/colors/dark_mobile.dart';
import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
enum Pages { home, grades, timetable, messages, absences }
enum UpdateChannel { stable, beta, dev }
enum VibrationStrength { off, light, medium, strong }
class SettingsProvider extends ChangeNotifier {
final DatabaseProvider? _database;
// en_en, hu_hu, de_de
String _language;
Pages _startPage;
// divide by 10
int _rounding;
ThemeMode _theme;
AccentColor _accentColor;
// zero is one, ...
List<Color> _gradeColors;
bool _newsEnabled;
int _newsState;
bool _notificationsEnabled;
/*
notificationsBitfield values:
1 << 0 current lesson
1 << 1 newsletter
1 << 2 grades
1 << 3 notes and events
1 << 4 inbox messages
1 << 5 substituted lessons and cancelled lessons
1 << 6 absences and misses
1 << 7 exams and homework
*/
int _notificationsBitfield;
// minutes: times 15
int _notificationPollInterval;
bool _developerMode;
VibrationStrength _vibrate;
bool _abWeeks;
bool _swapABweeks;
UpdateChannel _updateChannel;
Config _config;
String _xFilcId;
bool _graphClassAvg;
bool _goodStudent;
bool _presentationMode;
bool _bellDelayEnabled;
int _bellDelay;
bool _gradeOpeningFun;
IconPack _iconPack;
Color _customAccentColor;
Color _customBackgroundColor;
Color _customHighlightColor;
List<String> _premiumScopes;
String _premiumAccessToken;
String _premiumLogin;
String _lastAccountId;
bool _renamedSubjectsEnabled;
SettingsProvider({
DatabaseProvider? database,
required String language,
required Pages startPage,
required int rounding,
required ThemeMode theme,
required AccentColor accentColor,
required List<Color> gradeColors,
required bool newsEnabled,
required int newsState,
required bool notificationsEnabled,
required int notificationsBitfield,
required bool developerMode,
required int notificationPollInterval,
required VibrationStrength vibrate,
required bool abWeeks,
required bool swapABweeks,
required UpdateChannel updateChannel,
required Config config,
required String xFilcId,
required bool graphClassAvg,
required bool goodStudent,
required bool presentationMode,
required bool bellDelayEnabled,
required int bellDelay,
required bool gradeOpeningFun,
required IconPack iconPack,
required Color customAccentColor,
required Color customBackgroundColor,
required Color customHighlightColor,
required List<String> premiumScopes,
required String premiumAccessToken,
required String premiumLogin,
required String lastAccountId,
required bool renameSubjectsEnabled,
}) : _database = database,
_language = language,
_startPage = startPage,
_rounding = rounding,
_theme = theme,
_accentColor = accentColor,
_gradeColors = gradeColors,
_newsEnabled = newsEnabled,
_newsState = newsState,
_notificationsEnabled = notificationsEnabled,
_notificationsBitfield = notificationsBitfield,
_developerMode = developerMode,
_notificationPollInterval = notificationPollInterval,
_vibrate = vibrate,
_abWeeks = abWeeks,
_swapABweeks = swapABweeks,
_updateChannel = updateChannel,
_config = config,
_xFilcId = xFilcId,
_graphClassAvg = graphClassAvg,
_goodStudent = goodStudent,
_presentationMode = presentationMode,
_bellDelayEnabled = bellDelayEnabled,
_bellDelay = bellDelay,
_gradeOpeningFun = gradeOpeningFun,
_iconPack = iconPack,
_customAccentColor = customAccentColor,
_customBackgroundColor = customBackgroundColor,
_customHighlightColor = customHighlightColor,
_premiumScopes = premiumScopes,
_premiumAccessToken = premiumAccessToken,
_premiumLogin = premiumLogin,
_lastAccountId = lastAccountId,
_renamedSubjectsEnabled = renameSubjectsEnabled;
factory SettingsProvider.fromMap(Map map, {required DatabaseProvider database}) {
Map<String, Object?>? configMap;
try {
configMap = jsonDecode(map["config"] ?? "{}");
} catch (e) {
log("[ERROR] SettingsProvider.fromMap: $e");
}
return SettingsProvider(
database: database,
language: map["language"],
startPage: Pages.values[map["start_page"]],
rounding: map["rounding"],
theme: ThemeMode.values[map["theme"]],
accentColor: AccentColor.values[map["accent_color"]],
gradeColors: [
Color(map["grade_color1"]),
Color(map["grade_color2"]),
Color(map["grade_color3"]),
Color(map["grade_color4"]),
Color(map["grade_color5"]),
],
newsEnabled: map["news"] == 1,
newsState: map["news_state"],
notificationsEnabled: map["notifications"] == 1,
notificationsBitfield: map["notifications_bitfield"],
notificationPollInterval: map["notification_poll_interval"],
developerMode: map["developer_mode"] == 1,
vibrate: VibrationStrength.values[map["vibration_strength"]],
abWeeks: map["ab_weeks"] == 1,
swapABweeks: map["swap_ab_weeks"] == 1,
updateChannel: UpdateChannel.values[map["update_channel"]],
config: Config.fromJson(configMap ?? {}),
xFilcId: map["x_filc_id"],
graphClassAvg: map["graph_class_avg"] == 1,
goodStudent: false,
presentationMode: map["presentation_mode"] == 1,
bellDelayEnabled: map["bell_delay_enabled"] == 1,
bellDelay: map["bell_delay"],
gradeOpeningFun: map["grade_opening_fun"] == 1,
iconPack: Map.fromEntries(IconPack.values.map((e) => MapEntry(e.name, e)))[map["icon_pack"]]!,
customAccentColor: Color(map["custom_accent_color"]),
customBackgroundColor: Color(map["custom_background_color"]),
customHighlightColor: Color(map["custom_highlight_color"]),
premiumScopes: jsonDecode(map["premium_scopes"]).cast<String>(),
premiumAccessToken: map["premium_token"],
premiumLogin: map["premium_login"],
lastAccountId: map["last_account_id"],
renameSubjectsEnabled: map["renamed_subjects_enabled"] == 1,
);
}
Map<String, Object?> toMap() {
return {
"language": _language,
"start_page": _startPage.index,
"rounding": _rounding,
"theme": _theme.index,
"accent_color": _accentColor.index,
"news": _newsEnabled ? 1 : 0,
"news_state": _newsState,
"notifications": _notificationsEnabled ? 1 : 0,
"notifications_bitfield": _notificationsBitfield,
"developer_mode": _developerMode ? 1 : 0,
"grade_color1": _gradeColors[0].value,
"grade_color2": _gradeColors[1].value,
"grade_color3": _gradeColors[2].value,
"grade_color4": _gradeColors[3].value,
"grade_color5": _gradeColors[4].value,
"update_channel": _updateChannel.index,
"vibration_strength": _vibrate.index,
"ab_weeks": _abWeeks ? 1 : 0,
"swap_ab_weeks": _swapABweeks ? 1 : 0,
"notification_poll_interval": _notificationPollInterval,
"config": jsonEncode(config.json),
"x_filc_id": _xFilcId,
"graph_class_avg": _graphClassAvg ? 1 : 0,
"presentation_mode": _presentationMode ? 1 : 0,
"bell_delay_enabled": _bellDelayEnabled ? 1 : 0,
"bell_delay": _bellDelay,
"grade_opening_fun": _gradeOpeningFun ? 1 : 0,
"icon_pack": _iconPack.name,
"custom_accent_color": _customAccentColor.value,
"custom_background_color": _customBackgroundColor.value,
"custom_highlight_color": _customHighlightColor.value,
"premium_scopes": jsonEncode(_premiumScopes),
"premium_token": _premiumAccessToken,
"premium_login": _premiumLogin,
"last_account_id": _lastAccountId,
"renamed_subjects_enabled": _renamedSubjectsEnabled ? 1 : 0
};
}
factory SettingsProvider.defaultSettings({DatabaseProvider? database}) {
return SettingsProvider(
database: database,
language: "hu",
startPage: Pages.home,
rounding: 5,
theme: ThemeMode.system,
accentColor: AccentColor.filc,
gradeColors: [
DarkMobileAppColors().red,
DarkMobileAppColors().orange,
DarkMobileAppColors().yellow,
DarkMobileAppColors().green,
DarkMobileAppColors().filc,
],
newsEnabled: true,
newsState: -1,
notificationsEnabled: true,
notificationsBitfield: 255,
developerMode: false,
notificationPollInterval: 1,
vibrate: VibrationStrength.medium,
abWeeks: false,
swapABweeks: false,
updateChannel: UpdateChannel.stable,
config: Config.fromJson({}),
xFilcId: const Uuid().v4(),
graphClassAvg: false,
goodStudent: false,
presentationMode: false,
bellDelayEnabled: false,
bellDelay: 0,
gradeOpeningFun: true,
iconPack: IconPack.cupertino,
customAccentColor: const Color(0xff20AC9B),
customBackgroundColor: const Color(0xff000000),
customHighlightColor: const Color(0xff222222),
premiumScopes: [],
premiumAccessToken: "",
premiumLogin: "",
lastAccountId: "",
renameSubjectsEnabled: false,
);
}
// Getters
String get language => _language;
Pages get startPage => _startPage;
int get rounding => _rounding;
ThemeMode get theme => _theme;
AccentColor get accentColor => _accentColor;
List<Color> get gradeColors => _gradeColors;
bool get newsEnabled => _newsEnabled;
int get newsState => _newsState;
bool get notificationsEnabled => _notificationsEnabled;
int get notificationsBitfield => _notificationsBitfield;
bool get developerMode => _developerMode;
int get notificationPollInterval => _notificationPollInterval;
VibrationStrength get vibrate => _vibrate;
bool get abWeeks => _abWeeks;
bool get swapABweeks => _swapABweeks;
UpdateChannel get updateChannel => _updateChannel;
Config get config => _config;
String get xFilcId => _xFilcId;
bool get graphClassAvg => _graphClassAvg;
bool get goodStudent => _goodStudent;
bool get presentationMode => _presentationMode;
bool get bellDelayEnabled => _bellDelayEnabled;
int get bellDelay => _bellDelay;
bool get gradeOpeningFun => _gradeOpeningFun;
IconPack get iconPack => _iconPack;
Color? get customAccentColor => _customAccentColor == accentColorMap[AccentColor.custom] ? null : _customAccentColor;
Color? get customBackgroundColor => _customBackgroundColor;
Color? get customHighlightColor => _customHighlightColor;
List<String> get premiumScopes => _premiumScopes;
String get premiumAccessToken => _premiumAccessToken;
String get premiumLogin => _premiumLogin;
String get lastAccountId => _lastAccountId;
bool get renamedSubjectsEnabled => _renamedSubjectsEnabled;
Future<void> update({
bool store = true,
String? language,
Pages? startPage,
int? rounding,
ThemeMode? theme,
AccentColor? accentColor,
List<Color>? gradeColors,
bool? newsEnabled,
int? newsState,
bool? notificationsEnabled,
int? notificationsBitfield,
bool? developerMode,
int? notificationPollInterval,
VibrationStrength? vibrate,
bool? abWeeks,
bool? swapABweeks,
UpdateChannel? updateChannel,
Config? config,
String? xFilcId,
bool? graphClassAvg,
bool? goodStudent,
bool? presentationMode,
bool? bellDelayEnabled,
int? bellDelay,
bool? gradeOpeningFun,
IconPack? iconPack,
Color? customAccentColor,
Color? customBackgroundColor,
Color? customHighlightColor,
List<String>? premiumScopes,
String? premiumAccessToken,
String? premiumLogin,
String? lastAccountId,
bool? renamedSubjectsEnabled,
}) async {
if (language != null && language != _language) _language = language;
if (startPage != null && startPage != _startPage) _startPage = startPage;
if (rounding != null && rounding != _rounding) _rounding = rounding;
if (theme != null && theme != _theme) _theme = theme;
if (accentColor != null && accentColor != _accentColor) _accentColor = accentColor;
if (gradeColors != null && gradeColors != _gradeColors) _gradeColors = gradeColors;
if (newsEnabled != null && newsEnabled != _newsEnabled) _newsEnabled = newsEnabled;
if (newsState != null && newsState != _newsState) _newsState = newsState;
if (notificationsEnabled != null && notificationsEnabled != _notificationsEnabled) _notificationsEnabled = notificationsEnabled;
if (notificationsBitfield != null && notificationsBitfield != _notificationsBitfield) _notificationsBitfield = notificationsBitfield;
if (developerMode != null && developerMode != _developerMode) _developerMode = developerMode;
if (notificationPollInterval != null && notificationPollInterval != _notificationPollInterval) {
_notificationPollInterval = notificationPollInterval;
}
if (vibrate != null && vibrate != _vibrate) _vibrate = vibrate;
if (abWeeks != null && abWeeks != _abWeeks) _abWeeks = abWeeks;
if (swapABweeks != null && swapABweeks != _swapABweeks) _swapABweeks = swapABweeks;
if (updateChannel != null && updateChannel != _updateChannel) _updateChannel = updateChannel;
if (config != null && config != _config) _config = config;
if (xFilcId != null && xFilcId != _xFilcId) _xFilcId = xFilcId;
if (graphClassAvg != null && graphClassAvg != _graphClassAvg) _graphClassAvg = graphClassAvg;
if (goodStudent != null) _goodStudent = goodStudent;
if (presentationMode != null && presentationMode != _presentationMode) _presentationMode = presentationMode;
if (bellDelay != null && bellDelay != _bellDelay) _bellDelay = bellDelay;
if (bellDelayEnabled != null && bellDelayEnabled != _bellDelayEnabled) _bellDelayEnabled = bellDelayEnabled;
if (gradeOpeningFun != null && gradeOpeningFun != _gradeOpeningFun) _gradeOpeningFun = gradeOpeningFun;
if (iconPack != null && iconPack != _iconPack) _iconPack = iconPack;
if (customAccentColor != null && customAccentColor != _customAccentColor) _customAccentColor = customAccentColor;
if (customBackgroundColor != null && customBackgroundColor != _customBackgroundColor) _customBackgroundColor = customBackgroundColor;
if (customHighlightColor != null && customHighlightColor != _customHighlightColor) _customHighlightColor = customHighlightColor;
if (premiumScopes != null && premiumScopes != _premiumScopes) _premiumScopes = premiumScopes;
if (premiumAccessToken != null && premiumAccessToken != _premiumAccessToken) _premiumAccessToken = premiumAccessToken;
if (premiumLogin != null && premiumLogin != _premiumLogin) _premiumLogin = premiumLogin;
if (lastAccountId != null && lastAccountId != _lastAccountId) _lastAccountId = lastAccountId;
if (renamedSubjectsEnabled != null && renamedSubjectsEnabled != _renamedSubjectsEnabled) _renamedSubjectsEnabled = renamedSubjectsEnabled;
if (store) await _database?.store.storeSettings(this);
notifyListeners();
}
}
import 'dart:convert';
import 'dart:developer';
import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/models/config.dart';
import 'package:filcnaplo/models/icon_pack.dart';
import 'package:filcnaplo/theme/colors/accent.dart';
import 'package:filcnaplo/theme/colors/dark_mobile.dart';
import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
enum Pages { home, grades, timetable, messages, absences }
enum UpdateChannel { stable, beta, dev }
enum VibrationStrength { off, light, medium, strong }
class SettingsProvider extends ChangeNotifier {
final DatabaseProvider? _database;
// en_en, hu_hu, de_de
String _language;
Pages _startPage;
// divide by 10
int _rounding;
ThemeMode _theme;
AccentColor _accentColor;
// zero is one, ...
List<Color> _gradeColors;
bool _newsEnabled;
int _newsState;
bool _notificationsEnabled;
/*
notificationsBitfield values:
1 << 0 current lesson
1 << 1 newsletter
1 << 2 grades
1 << 3 notes and events
1 << 4 inbox messages
1 << 5 substituted lessons and cancelled lessons
1 << 6 absences and misses
1 << 7 exams and homework
*/
int _notificationsBitfield;
// minutes: times 15
int _notificationPollInterval;
bool _developerMode;
VibrationStrength _vibrate;
bool _abWeeks;
bool _swapABweeks;
UpdateChannel _updateChannel;
Config _config;
String _xFilcId;
bool _graphClassAvg;
bool _goodStudent;
bool _presentationMode;
bool _bellDelayEnabled;
int _bellDelay;
bool _gradeOpeningFun;
IconPack _iconPack;
Color _customAccentColor;
Color _customBackgroundColor;
Color _customHighlightColor;
List<String> _premiumScopes;
String _premiumAccessToken;
String _premiumLogin;
String _lastAccountId;
bool _renamedSubjectsEnabled;
SettingsProvider({
DatabaseProvider? database,
required String language,
required Pages startPage,
required int rounding,
required ThemeMode theme,
required AccentColor accentColor,
required List<Color> gradeColors,
required bool newsEnabled,
required int newsState,
required bool notificationsEnabled,
required int notificationsBitfield,
required bool developerMode,
required int notificationPollInterval,
required VibrationStrength vibrate,
required bool abWeeks,
required bool swapABweeks,
required UpdateChannel updateChannel,
required Config config,
required String xFilcId,
required bool graphClassAvg,
required bool goodStudent,
required bool presentationMode,
required bool bellDelayEnabled,
required int bellDelay,
required bool gradeOpeningFun,
required IconPack iconPack,
required Color customAccentColor,
required Color customBackgroundColor,
required Color customHighlightColor,
required List<String> premiumScopes,
required String premiumAccessToken,
required String premiumLogin,
required String lastAccountId,
required bool renameSubjectsEnabled,
}) : _database = database,
_language = language,
_startPage = startPage,
_rounding = rounding,
_theme = theme,
_accentColor = accentColor,
_gradeColors = gradeColors,
_newsEnabled = newsEnabled,
_newsState = newsState,
_notificationsEnabled = notificationsEnabled,
_notificationsBitfield = notificationsBitfield,
_developerMode = developerMode,
_notificationPollInterval = notificationPollInterval,
_vibrate = vibrate,
_abWeeks = abWeeks,
_swapABweeks = swapABweeks,
_updateChannel = updateChannel,
_config = config,
_xFilcId = xFilcId,
_graphClassAvg = graphClassAvg,
_goodStudent = goodStudent,
_presentationMode = presentationMode,
_bellDelayEnabled = bellDelayEnabled,
_bellDelay = bellDelay,
_gradeOpeningFun = gradeOpeningFun,
_iconPack = iconPack,
_customAccentColor = customAccentColor,
_customBackgroundColor = customBackgroundColor,
_customHighlightColor = customHighlightColor,
_premiumScopes = premiumScopes,
_premiumAccessToken = premiumAccessToken,
_premiumLogin = premiumLogin,
_lastAccountId = lastAccountId,
_renamedSubjectsEnabled = renameSubjectsEnabled;
factory SettingsProvider.fromMap(Map map, {required DatabaseProvider database}) {
Map<String, Object?>? configMap;
try {
configMap = jsonDecode(map["config"] ?? "{}");
} catch (e) {
log("[ERROR] SettingsProvider.fromMap: $e");
}
return SettingsProvider(
database: database,
language: map["language"],
startPage: Pages.values[map["start_page"]],
rounding: map["rounding"],
theme: ThemeMode.values[map["theme"]],
accentColor: AccentColor.values[map["accent_color"]],
gradeColors: [
Color(map["grade_color1"]),
Color(map["grade_color2"]),
Color(map["grade_color3"]),
Color(map["grade_color4"]),
Color(map["grade_color5"]),
],
newsEnabled: map["news"] == 1,
newsState: map["news_state"],
notificationsEnabled: map["notifications"] == 1,
notificationsBitfield: map["notifications_bitfield"],
notificationPollInterval: map["notification_poll_interval"],
developerMode: map["developer_mode"] == 1,
vibrate: VibrationStrength.values[map["vibration_strength"]],
abWeeks: map["ab_weeks"] == 1,
swapABweeks: map["swap_ab_weeks"] == 1,
updateChannel: UpdateChannel.values[map["update_channel"]],
config: Config.fromJson(configMap ?? {}),
xFilcId: map["x_filc_id"],
graphClassAvg: map["graph_class_avg"] == 1,
goodStudent: false,
presentationMode: map["presentation_mode"] == 1,
bellDelayEnabled: map["bell_delay_enabled"] == 1,
bellDelay: map["bell_delay"],
gradeOpeningFun: map["grade_opening_fun"] == 1,
iconPack: Map.fromEntries(IconPack.values.map((e) => MapEntry(e.name, e)))[map["icon_pack"]]!,
customAccentColor: Color(map["custom_accent_color"]),
customBackgroundColor: Color(map["custom_background_color"]),
customHighlightColor: Color(map["custom_highlight_color"]),
premiumScopes: jsonDecode(map["premium_scopes"]).cast<String>(),
premiumAccessToken: map["premium_token"],
premiumLogin: map["premium_login"],
lastAccountId: map["last_account_id"],
renameSubjectsEnabled: map["renamed_subjects_enabled"] == 1,
);
}
Map<String, Object?> toMap() {
return {
"language": _language,
"start_page": _startPage.index,
"rounding": _rounding,
"theme": _theme.index,
"accent_color": _accentColor.index,
"news": _newsEnabled ? 1 : 0,
"news_state": _newsState,
"notifications": _notificationsEnabled ? 1 : 0,
"notifications_bitfield": _notificationsBitfield,
"developer_mode": _developerMode ? 1 : 0,
"grade_color1": _gradeColors[0].value,
"grade_color2": _gradeColors[1].value,
"grade_color3": _gradeColors[2].value,
"grade_color4": _gradeColors[3].value,
"grade_color5": _gradeColors[4].value,
"update_channel": _updateChannel.index,
"vibration_strength": _vibrate.index,
"ab_weeks": _abWeeks ? 1 : 0,
"swap_ab_weeks": _swapABweeks ? 1 : 0,
"notification_poll_interval": _notificationPollInterval,
"config": jsonEncode(config.json),
"x_filc_id": _xFilcId,
"graph_class_avg": _graphClassAvg ? 1 : 0,
"presentation_mode": _presentationMode ? 1 : 0,
"bell_delay_enabled": _bellDelayEnabled ? 1 : 0,
"bell_delay": _bellDelay,
"grade_opening_fun": _gradeOpeningFun ? 1 : 0,
"icon_pack": _iconPack.name,
"custom_accent_color": _customAccentColor.value,
"custom_background_color": _customBackgroundColor.value,
"custom_highlight_color": _customHighlightColor.value,
"premium_scopes": jsonEncode(_premiumScopes),
"premium_token": _premiumAccessToken,
"premium_login": _premiumLogin,
"last_account_id": _lastAccountId,
"renamed_subjects_enabled": _renamedSubjectsEnabled ? 1 : 0
};
}
factory SettingsProvider.defaultSettings({DatabaseProvider? database}) {
return SettingsProvider(
database: database,
language: "hu",
startPage: Pages.home,
rounding: 5,
theme: ThemeMode.system,
accentColor: AccentColor.filc,
gradeColors: [
DarkMobileAppColors().red,
DarkMobileAppColors().orange,
DarkMobileAppColors().yellow,
DarkMobileAppColors().green,
DarkMobileAppColors().filc,
],
newsEnabled: true,
newsState: -1,
notificationsEnabled: true,
notificationsBitfield: 255,
developerMode: false,
notificationPollInterval: 1,
vibrate: VibrationStrength.medium,
abWeeks: false,
swapABweeks: false,
updateChannel: UpdateChannel.stable,
config: Config.fromJson({}),
xFilcId: const Uuid().v4(),
graphClassAvg: false,
goodStudent: false,
presentationMode: false,
bellDelayEnabled: false,
bellDelay: 0,
gradeOpeningFun: true,
iconPack: IconPack.cupertino,
customAccentColor: const Color(0xff20AC9B),
customBackgroundColor: const Color(0xff000000),
customHighlightColor: const Color(0xff222222),
premiumScopes: [],
premiumAccessToken: "",
premiumLogin: "",
lastAccountId: "",
renameSubjectsEnabled: false,
);
}
// Getters
String get language => _language;
Pages get startPage => _startPage;
int get rounding => _rounding;
ThemeMode get theme => _theme;
AccentColor get accentColor => _accentColor;
List<Color> get gradeColors => _gradeColors;
bool get newsEnabled => _newsEnabled;
int get newsState => _newsState;
bool get notificationsEnabled => _notificationsEnabled;
int get notificationsBitfield => _notificationsBitfield;
bool get developerMode => _developerMode;
int get notificationPollInterval => _notificationPollInterval;
VibrationStrength get vibrate => _vibrate;
bool get abWeeks => _abWeeks;
bool get swapABweeks => _swapABweeks;
UpdateChannel get updateChannel => _updateChannel;
Config get config => _config;
String get xFilcId => _xFilcId;
bool get graphClassAvg => _graphClassAvg;
bool get goodStudent => _goodStudent;
bool get presentationMode => _presentationMode;
bool get bellDelayEnabled => _bellDelayEnabled;
int get bellDelay => _bellDelay;
bool get gradeOpeningFun => _gradeOpeningFun;
IconPack get iconPack => _iconPack;
Color? get customAccentColor => _customAccentColor == accentColorMap[AccentColor.custom] ? null : _customAccentColor;
Color? get customBackgroundColor => _customBackgroundColor;
Color? get customHighlightColor => _customHighlightColor;
List<String> get premiumScopes => _premiumScopes;
String get premiumAccessToken => _premiumAccessToken;
String get premiumLogin => _premiumLogin;
String get lastAccountId => _lastAccountId;
bool get renamedSubjectsEnabled => _renamedSubjectsEnabled;
Future<void> update({
bool store = true,
String? language,
Pages? startPage,
int? rounding,
ThemeMode? theme,
AccentColor? accentColor,
List<Color>? gradeColors,
bool? newsEnabled,
int? newsState,
bool? notificationsEnabled,
int? notificationsBitfield,
bool? developerMode,
int? notificationPollInterval,
VibrationStrength? vibrate,
bool? abWeeks,
bool? swapABweeks,
UpdateChannel? updateChannel,
Config? config,
String? xFilcId,
bool? graphClassAvg,
bool? goodStudent,
bool? presentationMode,
bool? bellDelayEnabled,
int? bellDelay,
bool? gradeOpeningFun,
IconPack? iconPack,
Color? customAccentColor,
Color? customBackgroundColor,
Color? customHighlightColor,
List<String>? premiumScopes,
String? premiumAccessToken,
String? premiumLogin,
String? lastAccountId,
bool? renamedSubjectsEnabled,
}) async {
if (language != null && language != _language) _language = language;
if (startPage != null && startPage != _startPage) _startPage = startPage;
if (rounding != null && rounding != _rounding) _rounding = rounding;
if (theme != null && theme != _theme) _theme = theme;
if (accentColor != null && accentColor != _accentColor) _accentColor = accentColor;
if (gradeColors != null && gradeColors != _gradeColors) _gradeColors = gradeColors;
if (newsEnabled != null && newsEnabled != _newsEnabled) _newsEnabled = newsEnabled;
if (newsState != null && newsState != _newsState) _newsState = newsState;
if (notificationsEnabled != null && notificationsEnabled != _notificationsEnabled) _notificationsEnabled = notificationsEnabled;
if (notificationsBitfield != null && notificationsBitfield != _notificationsBitfield) _notificationsBitfield = notificationsBitfield;
if (developerMode != null && developerMode != _developerMode) _developerMode = developerMode;
if (notificationPollInterval != null && notificationPollInterval != _notificationPollInterval) {
_notificationPollInterval = notificationPollInterval;
}
if (vibrate != null && vibrate != _vibrate) _vibrate = vibrate;
if (abWeeks != null && abWeeks != _abWeeks) _abWeeks = abWeeks;
if (swapABweeks != null && swapABweeks != _swapABweeks) _swapABweeks = swapABweeks;
if (updateChannel != null && updateChannel != _updateChannel) _updateChannel = updateChannel;
if (config != null && config != _config) _config = config;
if (xFilcId != null && xFilcId != _xFilcId) _xFilcId = xFilcId;
if (graphClassAvg != null && graphClassAvg != _graphClassAvg) _graphClassAvg = graphClassAvg;
if (goodStudent != null) _goodStudent = goodStudent;
if (presentationMode != null && presentationMode != _presentationMode) _presentationMode = presentationMode;
if (bellDelay != null && bellDelay != _bellDelay) _bellDelay = bellDelay;
if (bellDelayEnabled != null && bellDelayEnabled != _bellDelayEnabled) _bellDelayEnabled = bellDelayEnabled;
if (gradeOpeningFun != null && gradeOpeningFun != _gradeOpeningFun) _gradeOpeningFun = gradeOpeningFun;
if (iconPack != null && iconPack != _iconPack) _iconPack = iconPack;
if (customAccentColor != null && customAccentColor != _customAccentColor) _customAccentColor = customAccentColor;
if (customBackgroundColor != null && customBackgroundColor != _customBackgroundColor) _customBackgroundColor = customBackgroundColor;
if (customHighlightColor != null && customHighlightColor != _customHighlightColor) _customHighlightColor = customHighlightColor;
if (premiumScopes != null && premiumScopes != _premiumScopes) _premiumScopes = premiumScopes;
if (premiumAccessToken != null && premiumAccessToken != _premiumAccessToken) _premiumAccessToken = premiumAccessToken;
if (premiumLogin != null && premiumLogin != _premiumLogin) _premiumLogin = premiumLogin;
if (lastAccountId != null && lastAccountId != _lastAccountId) _lastAccountId = lastAccountId;
if (renamedSubjectsEnabled != null && renamedSubjectsEnabled != _renamedSubjectsEnabled) _renamedSubjectsEnabled = renamedSubjectsEnabled;
if (store) await _database?.store.storeSettings(this);
notifyListeners();
}
}

View File

@@ -1,31 +1,31 @@
import 'package:filcnaplo_kreta_api/models/category.dart';
import 'package:filcnaplo_kreta_api/models/subject.dart';
enum SubjectLessonCountUpdateState { ready, updating }
class SubjectLessonCount {
DateTime lastUpdated;
Map<Subject, int> subjects;
SubjectLessonCountUpdateState state;
SubjectLessonCount({required this.lastUpdated, required this.subjects, this.state = SubjectLessonCountUpdateState.ready});
factory SubjectLessonCount.fromMap(Map json) {
return SubjectLessonCount(
lastUpdated: DateTime.fromMillisecondsSinceEpoch(json["last_updated"] ?? 0),
subjects: ((json["subjects"] as Map?) ?? {}).map(
(key, value) => MapEntry(
Subject(id: key, name: "", category: Category.fromJson({})),
value,
),
),
);
}
Map toMap() {
return {
"last_updated": lastUpdated.millisecondsSinceEpoch,
"subjects": subjects.map((key, value) => MapEntry(key.id, value)),
};
}
}
import 'package:filcnaplo_kreta_api/models/category.dart';
import 'package:filcnaplo_kreta_api/models/subject.dart';
enum SubjectLessonCountUpdateState { ready, updating }
class SubjectLessonCount {
DateTime lastUpdated;
Map<Subject, int> subjects;
SubjectLessonCountUpdateState state;
SubjectLessonCount({required this.lastUpdated, required this.subjects, this.state = SubjectLessonCountUpdateState.ready});
factory SubjectLessonCount.fromMap(Map json) {
return SubjectLessonCount(
lastUpdated: DateTime.fromMillisecondsSinceEpoch(json["last_updated"] ?? 0),
subjects: ((json["subjects"] as Map?) ?? {}).map(
(key, value) => MapEntry(
Subject(id: key, name: "", category: Category.fromJson({})),
value,
),
),
);
}
Map toMap() {
return {
"last_updated": lastUpdated.millisecondsSinceEpoch,
"subjects": subjects.map((key, value) => MapEntry(key.id, value)),
};
}
}

View File

@@ -1,50 +1,50 @@
enum DonationType { once, monthly }
class Supporter {
final String avatar;
final String name;
final String comment;
final int price;
final DonationType type;
const Supporter({required this.avatar, required this.name, this.comment = "", this.price = 0, this.type = DonationType.once});
factory Supporter.fromJson(Map json, {String? avatarPattern}) {
return Supporter(
avatar: json["avatar"] ?? avatarPattern != null ? avatarPattern!.replaceFirst("\$", json["name"]) : "",
name: json["name"] ?? "Unknown",
comment: json["comment"] ?? "",
price: json["price"].toInt() ?? 0,
type: DonationType.values.asNameMap()[json["type"] ?? "once"] ?? DonationType.once,
);
}
}
class Supporters {
final double progress;
final double max;
final String description;
final List<Supporter> github;
final List<Supporter> patreon;
Supporters({
required this.progress,
required this.max,
required this.description,
required this.github,
required this.patreon,
});
factory Supporters.fromJson(Map json) {
return Supporters(
progress: json["percentage"].toDouble() ?? 100.0,
max: json["target"].toDouble() ?? 1.0,
description: json["description"] ?? "",
github: json["sponsors"]["github"]
.map((e) => Supporter.fromJson(e, avatarPattern: "https://github.com/\$.png?size=200"))
.cast<Supporter>()
.toList(),
patreon: json["sponsors"]["patreon"].map((e) => Supporter.fromJson(e)).cast<Supporter>().toList(),
);
}
}
enum DonationType { once, monthly }
class Supporter {
final String avatar;
final String name;
final String comment;
final int price;
final DonationType type;
const Supporter({required this.avatar, required this.name, this.comment = "", this.price = 0, this.type = DonationType.once});
factory Supporter.fromJson(Map json, {String? avatarPattern}) {
return Supporter(
avatar: json["avatar"] ?? avatarPattern != null ? avatarPattern!.replaceFirst("\$", json["name"]) : "",
name: json["name"] ?? "Unknown",
comment: json["comment"] ?? "",
price: json["price"].toInt() ?? 0,
type: DonationType.values.asNameMap()[json["type"] ?? "once"] ?? DonationType.once,
);
}
}
class Supporters {
final double progress;
final double max;
final String description;
final List<Supporter> github;
final List<Supporter> patreon;
Supporters({
required this.progress,
required this.max,
required this.description,
required this.github,
required this.patreon,
});
factory Supporters.fromJson(Map json) {
return Supporters(
progress: json["percentage"].toDouble() ?? 100.0,
max: json["target"].toDouble() ?? 1.0,
description: json["description"] ?? "",
github: json["sponsors"]["github"]
.map((e) => Supporter.fromJson(e, avatarPattern: "https://github.com/\$.png?size=200"))
.cast<Supporter>()
.toList(),
patreon: json["sponsors"]["patreon"].map((e) => Supporter.fromJson(e)).cast<Supporter>().toList(),
);
}
}

View File

@@ -1,96 +1,96 @@
import 'dart:convert';
import 'package:filcnaplo_kreta_api/client/api.dart';
import 'package:filcnaplo_kreta_api/models/student.dart';
import 'package:uuid/uuid.dart';
enum Role { student, parent }
class User {
late String id;
String username;
String password;
String instituteCode;
String name;
Student student;
Role role;
String nickname;
String picture;
String get displayName => nickname != '' ? nickname : name;
User({
String? id,
required this.name,
required this.username,
required this.password,
required this.instituteCode,
required this.student,
required this.role,
this.nickname = "",
this.picture = "",
}) {
if (id != null) {
this.id = id;
} else {
this.id = const Uuid().v4();
}
}
factory User.fromMap(Map map) {
return User(
id: map["id"],
instituteCode: map["institute_code"],
username: map["username"],
password: map["password"],
name: map["name"].trim(),
student: Student.fromJson(jsonDecode(map["student"])),
role: Role.values[map["role"] ?? 0],
nickname: map["nickname"] ?? "",
picture: map["picture"] ?? "",
);
}
Map<String, Object?> toMap() {
return {
"id": id,
"username": username,
"password": password,
"institute_code": instituteCode,
"name": name,
"student": jsonEncode(student.json),
"role": role.index,
"nickname": nickname,
"picture": picture,
};
}
@override
String toString() => jsonEncode(toMap());
static Map<String, Object?> loginBody({
required String username,
required String password,
required String instituteCode,
}) {
return {
"userName": username,
"password": password,
"institute_code": instituteCode,
"grant_type": "password",
"client_id": KretaAPI.clientId,
};
}
static Map<String, Object?> refreshBody({
required String refreshToken,
required String instituteCode,
}) {
return {
"refresh_token": refreshToken,
"institute_code": instituteCode,
"client_id": KretaAPI.clientId,
"grant_type": "refresh_token",
"refresh_user_data": "false",
};
}
}
import 'dart:convert';
import 'package:filcnaplo_kreta_api/client/api.dart';
import 'package:filcnaplo_kreta_api/models/student.dart';
import 'package:uuid/uuid.dart';
enum Role { student, parent }
class User {
late String id;
String username;
String password;
String instituteCode;
String name;
Student student;
Role role;
String nickname;
String picture;
String get displayName => nickname != '' ? nickname : name;
User({
String? id,
required this.name,
required this.username,
required this.password,
required this.instituteCode,
required this.student,
required this.role,
this.nickname = "",
this.picture = "",
}) {
if (id != null) {
this.id = id;
} else {
this.id = const Uuid().v4();
}
}
factory User.fromMap(Map map) {
return User(
id: map["id"],
instituteCode: map["institute_code"],
username: map["username"],
password: map["password"],
name: map["name"].trim(),
student: Student.fromJson(jsonDecode(map["student"])),
role: Role.values[map["role"] ?? 0],
nickname: map["nickname"] ?? "",
picture: map["picture"] ?? "",
);
}
Map<String, Object?> toMap() {
return {
"id": id,
"username": username,
"password": password,
"institute_code": instituteCode,
"name": name,
"student": jsonEncode(student.json),
"role": role.index,
"nickname": nickname,
"picture": picture,
};
}
@override
String toString() => jsonEncode(toMap());
static Map<String, Object?> loginBody({
required String username,
required String password,
required String instituteCode,
}) {
return {
"userName": username,
"password": password,
"institute_code": instituteCode,
"grant_type": "password",
"client_id": KretaAPI.clientId,
};
}
static Map<String, Object?> refreshBody({
required String refreshToken,
required String instituteCode,
}) {
return {
"refresh_token": refreshToken,
"institute_code": instituteCode,
"client_id": KretaAPI.clientId,
"grant_type": "refresh_token",
"refresh_user_data": "false",
};
}
}

View File

@@ -1,17 +1,29 @@
import 'package:flutter/material.dart';
enum AccentColor { filc, blue, green, lime, yellow, orange, red, pink, purple, adaptive, custom }
Map<AccentColor, Color> accentColorMap = {
AccentColor.filc: const Color(0xff20AC9B),
AccentColor.blue: Colors.blue.shade300,
AccentColor.green: Colors.green.shade400,
AccentColor.lime: Colors.lightGreen.shade400,
AccentColor.yellow: Colors.orange.shade300,
AccentColor.orange: Colors.deepOrange.shade300,
AccentColor.red: Colors.red.shade300,
AccentColor.pink: Colors.pink.shade300,
AccentColor.purple: Colors.purple.shade300,
AccentColor.adaptive: const Color(0xff20AC9B),
AccentColor.custom: const Color(0xff20AC9B),
};
import 'package:flutter/material.dart';
enum AccentColor {
filc,
blue,
green,
lime,
yellow,
orange,
red,
pink,
purple,
adaptive,
custom
}
Map<AccentColor, Color> accentColorMap = {
AccentColor.filc: Color.fromARGB(255, 61, 123, 244),
AccentColor.blue: Colors.blue.shade300,
AccentColor.green: Colors.green.shade400,
AccentColor.lime: Colors.lightGreen.shade400,
AccentColor.yellow: Colors.orange.shade300,
AccentColor.orange: Colors.deepOrange.shade300,
AccentColor.red: Colors.red.shade300,
AccentColor.pink: Colors.pink.shade300,
AccentColor.purple: Colors.purple.shade300,
AccentColor.adaptive: const Color(0x003d7bf4),
AccentColor.custom: const Color(0x003d7bf4),
};

View File

@@ -1,46 +1,47 @@
import 'dart:io';
import 'package:filcnaplo/theme/colors/dark_desktop.dart';
import 'package:filcnaplo/theme/colors/dark_mobile.dart';
import 'package:filcnaplo/theme/colors/light_desktop.dart';
import 'package:filcnaplo/theme/colors/light_mobile.dart';
import 'package:flutter/material.dart';
class AppColors {
static ThemeAppColors of(BuildContext context) => fromBrightness(Theme.of(context).brightness);
static ThemeAppColors fromBrightness(Brightness brightness) {
if (Platform.isAndroid || Platform.isIOS) {
switch (brightness) {
case Brightness.light:
return LightMobileAppColors();
case Brightness.dark:
return DarkMobileAppColors();
}
} else {
switch (brightness) {
case Brightness.light:
return LightDesktopAppColors();
case Brightness.dark:
return DarkDesktopAppColors();
}
}
}
}
abstract class ThemeAppColors {
final Color shadow = const Color(0x00000000);
final Color text = const Color(0x00000000);
final Color background = const Color(0x00000000);
final Color highlight = const Color(0x00000000);
final Color red = const Color(0x00000000);
final Color orange = const Color(0x00000000);
final Color yellow = const Color(0x00000000);
final Color green = const Color(0x00000000);
final Color filc = const Color(0x00000000);
final Color teal = const Color(0x00000000);
final Color blue = const Color(0x00000000);
final Color indigo = const Color(0x00000000);
final Color purple = const Color(0x00000000);
final Color pink = const Color(0x00000000);
}
import 'dart:io';
import 'package:filcnaplo/theme/colors/dark_desktop.dart';
import 'package:filcnaplo/theme/colors/dark_mobile.dart';
import 'package:filcnaplo/theme/colors/light_desktop.dart';
import 'package:filcnaplo/theme/colors/light_mobile.dart';
import 'package:flutter/material.dart';
class AppColors {
static ThemeAppColors of(BuildContext context) =>
fromBrightness(Theme.of(context).brightness);
static ThemeAppColors fromBrightness(Brightness brightness) {
if (Platform.isAndroid || Platform.isIOS) {
switch (brightness) {
case Brightness.light:
return LightMobileAppColors();
case Brightness.dark:
return DarkMobileAppColors();
}
} else {
switch (brightness) {
case Brightness.light:
return LightDesktopAppColors();
case Brightness.dark:
return DarkDesktopAppColors();
}
}
}
}
abstract class ThemeAppColors {
final Color shadow = const Color(0x00000000);
final Color text = const Color(0x00000000);
final Color background = const Color(0x00000000);
final Color highlight = const Color(0x00000000);
final Color red = const Color(0x00000000);
final Color orange = const Color(0x00000000);
final Color yellow = const Color(0x00000000);
final Color green = const Color(0x00000000);
final Color filc = const Color(0x00000000);
final Color teal = const Color(0x00000000);
final Color blue = const Color(0x00000000);
final Color indigo = const Color(0x00000000);
final Color purple = const Color(0x00000000);
final Color pink = const Color(0x00000000);
}

View File

@@ -1,33 +1,33 @@
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:flutter/material.dart';
class DarkDesktopAppColors implements ThemeAppColors {
@override
final shadow = const Color(0x00000000);
@override
final text = Colors.white;
@override
final background = const Color.fromARGB(255, 42, 42, 42);
@override
final highlight = const Color.fromARGB(255, 46, 48, 50);
@override
final red = const Color(0xffFF453A);
@override
final orange = const Color(0xffFF9F0A);
@override
final yellow = const Color(0xffFFD60A);
@override
final green = const Color(0xff32D74B);
@override
final filc = const Color(0xff29826F);
@override
final teal = const Color(0xff64D2FF);
@override
final blue = const Color(0xff0A84FF);
@override
final indigo = const Color(0xff5E5CE6);
@override
final purple = const Color(0xffBF5AF2);
@override
final pink = const Color(0xffFF375F);
}
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:flutter/material.dart';
class DarkDesktopAppColors implements ThemeAppColors {
@override
final shadow = const Color(0x00000000);
@override
final text = Colors.white;
@override
final background = const Color.fromARGB(255, 42, 42, 42);
@override
final highlight = const Color.fromARGB(255, 46, 48, 50);
@override
final red = const Color(0xffFF453A);
@override
final orange = const Color(0xffFF9F0A);
@override
final yellow = const Color(0xffFFD60A);
@override
final green = const Color(0xff32D74B);
@override
final filc = const Color(0xff29826F);
@override
final teal = const Color(0xff64D2FF);
@override
final blue = const Color(0xff0A84FF);
@override
final indigo = const Color(0xff5E5CE6);
@override
final purple = const Color(0xffBF5AF2);
@override
final pink = const Color(0xffFF375F);
}

View File

@@ -1,33 +1,33 @@
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:flutter/material.dart';
class DarkMobileAppColors implements ThemeAppColors {
@override
final shadow = const Color(0x00000000);
@override
final text = Colors.white;
@override
final background = const Color(0xff000000);
@override
final highlight = const Color(0xff141516);
@override
final red = const Color(0xffFF453A);
@override
final orange = const Color(0xffFF9F0A);
@override
final yellow = const Color(0xffFFD60A);
@override
final green = const Color(0xff32D74B);
@override
final filc = const Color(0xff29826F);
@override
final teal = const Color(0xff64D2FF);
@override
final blue = const Color(0xff0A84FF);
@override
final indigo = const Color(0xff5E5CE6);
@override
final purple = const Color(0xffBF5AF2);
@override
final pink = const Color(0xffFF375F);
}
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:flutter/material.dart';
class DarkMobileAppColors implements ThemeAppColors {
@override
final shadow = const Color(0x00000000);
@override
final text = Colors.white;
@override
final background = const Color(0xff000000);
@override
final highlight = const Color(0xff141516);
@override
final red = const Color(0xffFF453A);
@override
final orange = const Color(0xffFF9F0A);
@override
final yellow = const Color(0xffFFD60A);
@override
final green = const Color(0xff32D74B);
@override
final filc = const Color(0x003d7bf4);
@override
final teal = const Color(0xff64D2FF);
@override
final blue = Color.fromARGB(255, 255, 10, 10);
@override
final indigo = const Color(0xff5E5CE6);
@override
final purple = const Color(0xffBF5AF2);
@override
final pink = const Color(0xffFF375F);
}

View File

@@ -1,33 +1,33 @@
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:flutter/material.dart';
class LightDesktopAppColors implements ThemeAppColors {
@override
final shadow = const Color(0xffE8E8E8);
@override
final text = Colors.black;
@override
final background = const Color(0xffF4F9FF);
@override
final highlight = const Color(0xffFFFFFF);
@override
final red = const Color(0xffFF3B30);
@override
final orange = const Color(0xffFF9500);
@override
final yellow = const Color(0xffFFCC00);
@override
final green = const Color(0xff34C759);
@override
final filc = const Color(0xff247665);
@override
final teal = const Color(0xff5AC8FA);
@override
final blue = const Color(0xff007AFF);
@override
final indigo = const Color(0xff5856D6);
@override
final purple = const Color(0xffAF52DE);
@override
final pink = const Color(0xffFF2D55);
}
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:flutter/material.dart';
class LightDesktopAppColors implements ThemeAppColors {
@override
final shadow = const Color(0xffE8E8E8);
@override
final text = Colors.black;
@override
final background = const Color(0xffF4F9FF);
@override
final highlight = const Color(0xffFFFFFF);
@override
final red = const Color(0xffFF3B30);
@override
final orange = const Color(0xffFF9500);
@override
final yellow = const Color(0xffFFCC00);
@override
final green = const Color(0xff34C759);
@override
final filc = const Color(0xff247665);
@override
final teal = const Color(0xff5AC8FA);
@override
final blue = const Color(0xff007AFF);
@override
final indigo = const Color(0xff5856D6);
@override
final purple = const Color(0xffAF52DE);
@override
final pink = const Color(0xffFF2D55);
}

View File

@@ -1,33 +1,33 @@
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:flutter/material.dart';
class LightMobileAppColors implements ThemeAppColors {
@override
final shadow = const Color(0xffE8E8E8);
@override
final text = Colors.black;
@override
final background = const Color(0xffF4F9FF);
@override
final highlight = const Color(0xffFFFFFF);
@override
final red = const Color(0xffFF3B30);
@override
final orange = const Color(0xffFF9500);
@override
final yellow = const Color(0xffFFCC00);
@override
final green = const Color(0xff34C759);
@override
final filc = const Color(0xff247665);
@override
final teal = const Color(0xff5AC8FA);
@override
final blue = const Color(0xff007AFF);
@override
final indigo = const Color(0xff5856D6);
@override
final purple = const Color(0xffAF52DE);
@override
final pink = const Color(0xffFF2D55);
}
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:flutter/material.dart';
class LightMobileAppColors implements ThemeAppColors {
@override
final shadow = const Color(0xffE8E8E8);
@override
final text = Colors.black;
@override
final background = const Color(0xffF4F9FF);
@override
final highlight = const Color(0xffFFFFFF);
@override
final red = const Color(0xffFF3B30);
@override
final orange = const Color(0xffFF9500);
@override
final yellow = const Color(0xffFFCC00);
@override
final green = const Color(0xff34C759);
@override
final filc = const Color(0x003d7bf4);
@override
final teal = const Color(0xff5AC8FA);
@override
final blue = const Color(0xff007AFF);
@override
final indigo = const Color(0xff5856D6);
@override
final purple = const Color(0xffAF52DE);
@override
final pink = const Color(0xffFF2D55);
}

View File

@@ -1,18 +1,18 @@
import 'package:flutter/material.dart';
class ThemeModeObserver extends ChangeNotifier {
ThemeMode _themeMode;
bool _updateNavbarColor;
ThemeMode get themeMode => _themeMode;
bool get updateNavbarColor => _updateNavbarColor;
ThemeModeObserver({ThemeMode initialTheme = ThemeMode.system, bool updateNavbarColor = true})
: _themeMode = initialTheme,
_updateNavbarColor = updateNavbarColor;
void changeTheme(ThemeMode mode, {bool updateNavbarColor = true}) {
_themeMode = mode;
_updateNavbarColor = updateNavbarColor;
notifyListeners();
}
}
import 'package:flutter/material.dart';
class ThemeModeObserver extends ChangeNotifier {
ThemeMode _themeMode;
bool _updateNavbarColor;
ThemeMode get themeMode => _themeMode;
bool get updateNavbarColor => _updateNavbarColor;
ThemeModeObserver({ThemeMode initialTheme = ThemeMode.system, bool updateNavbarColor = true})
: _themeMode = initialTheme,
_updateNavbarColor = updateNavbarColor;
void changeTheme(ThemeMode mode, {bool updateNavbarColor = true}) {
_themeMode = mode;
_updateNavbarColor = updateNavbarColor;
notifyListeners();
}
}

View File

@@ -1,160 +1,160 @@
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/theme/colors/accent.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo/theme/observer.dart';
import 'package:flutter/material.dart';
import 'package:material_color_utilities/material_color_utilities.dart';
import 'package:provider/provider.dart';
class AppTheme {
// Dev note: All of these could be constant variables, but this is better for
// development (you don't have to hot-restart)
static const String _fontFamily = "Montserrat";
static Color? _paletteAccentLight(CorePalette? palette) => palette != null ? Color(palette.primary.get(70)) : null;
static Color? _paletteHighlightLight(CorePalette? palette) => palette != null ? Color(palette.neutral.get(100)) : null;
static Color? _paletteBackgroundLight(CorePalette? palette) => palette != null ? Color(palette.neutral.get(95)) : null;
static Color? _paletteAccentDark(CorePalette? palette) => palette != null ? Color(palette.primary.get(80)) : null;
static Color? _paletteBackgroundDark(CorePalette? palette) => palette != null ? Color(palette.neutralVariant.get(10)) : null;
static Color? _paletteHighlightDark(CorePalette? palette) => palette != null ? Color(palette.neutralVariant.get(20)) : null;
// Light Theme
static ThemeData lightTheme(BuildContext context, {CorePalette? palette}) {
var lightColors = AppColors.fromBrightness(Brightness.light);
final settings = Provider.of<SettingsProvider>(context, listen: false);
AccentColor accentColor = settings.accentColor;
final customAccentColor = accentColor == AccentColor.custom ? settings.customAccentColor : null;
Color accent = customAccentColor ?? accentColorMap[accentColor] ?? const Color(0x00000000);
if (accentColor == AccentColor.adaptive) {
if (palette != null) accent = _paletteAccentLight(palette)!;
} else {
palette = null;
}
Color backgroundColor =
(accentColor == AccentColor.custom ? settings.customBackgroundColor : _paletteBackgroundLight(palette)) ?? lightColors.background;
Color highlightColor =
(accentColor == AccentColor.custom ? settings.customHighlightColor : _paletteHighlightLight(palette)) ?? lightColors.highlight;
return ThemeData(
brightness: Brightness.light,
useMaterial3: true,
fontFamily: _fontFamily,
scaffoldBackgroundColor: backgroundColor,
primaryColor: lightColors.filc,
dividerColor: const Color(0x00000000),
colorScheme: ColorScheme(
primary: accent,
onPrimary: (accent.computeLuminance() > 0.5 ? Colors.black : Colors.white).withOpacity(.9),
secondary: accent,
onSecondary: (accent.computeLuminance() > 0.5 ? Colors.black : Colors.white).withOpacity(.9),
background: highlightColor,
onBackground: Colors.black.withOpacity(.9),
brightness: Brightness.light,
error: lightColors.red,
onError: Colors.white.withOpacity(.9),
surface: highlightColor,
onSurface: Colors.black.withOpacity(.9),
),
shadowColor: lightColors.shadow.withOpacity(.5),
appBarTheme: AppBarTheme(backgroundColor: backgroundColor),
indicatorColor: accent,
iconTheme: IconThemeData(color: lightColors.text.withOpacity(.75)),
navigationBarTheme: NavigationBarThemeData(
indicatorColor: accent.withOpacity(accentColor == AccentColor.adaptive ? 0.4 : 0.8),
iconTheme: MaterialStateProperty.all(IconThemeData(color: lightColors.text)),
backgroundColor: highlightColor,
labelTextStyle: MaterialStateProperty.all(TextStyle(
fontSize: 13.0,
fontWeight: FontWeight.w500,
color: lightColors.text.withOpacity(0.8),
)),
labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
height: 76.0,
),
sliderTheme: SliderThemeData(
inactiveTrackColor: accent.withOpacity(.3),
),
progressIndicatorTheme: ProgressIndicatorThemeData(color: accent),
expansionTileTheme: ExpansionTileThemeData(iconColor: accent),
cardColor: highlightColor,
bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: Provider.of<ThemeModeObserver>(context, listen: false).updateNavbarColor ? backgroundColor : null,
),
);
}
// Dark Theme
static ThemeData darkTheme(BuildContext context, {CorePalette? palette}) {
var darkColors = AppColors.fromBrightness(Brightness.dark);
final settings = Provider.of<SettingsProvider>(context, listen: false);
AccentColor accentColor = settings.accentColor;
final customAccentColor = accentColor == AccentColor.custom ? settings.customAccentColor : null;
Color accent = customAccentColor ?? accentColorMap[accentColor] ?? const Color(0x00000000);
if (accentColor == AccentColor.adaptive) {
if (palette != null) accent = _paletteAccentDark(palette)!;
} else {
palette = null;
}
Color backgroundColor =
(accentColor == AccentColor.custom ? settings.customBackgroundColor : _paletteBackgroundDark(palette)) ?? darkColors.background;
Color highlightColor =
(accentColor == AccentColor.custom ? settings.customHighlightColor : _paletteHighlightDark(palette)) ?? darkColors.highlight;
return ThemeData(
brightness: Brightness.dark,
useMaterial3: true,
fontFamily: _fontFamily,
scaffoldBackgroundColor: backgroundColor,
primaryColor: darkColors.filc,
dividerColor: const Color(0x00000000),
colorScheme: ColorScheme(
primary: accent,
onPrimary: (accent.computeLuminance() > 0.5 ? Colors.black : Colors.white).withOpacity(.9),
secondary: accent,
onSecondary: (accent.computeLuminance() > 0.5 ? Colors.black : Colors.white).withOpacity(.9),
background: highlightColor,
onBackground: Colors.white.withOpacity(.9),
brightness: Brightness.dark,
error: darkColors.red,
onError: Colors.black.withOpacity(.9),
surface: highlightColor,
onSurface: Colors.white.withOpacity(.9),
),
shadowColor: highlightColor.withOpacity(.5), //darkColors.shadow,
appBarTheme: AppBarTheme(backgroundColor: backgroundColor),
indicatorColor: accent,
iconTheme: IconThemeData(color: darkColors.text.withOpacity(.75)),
navigationBarTheme: NavigationBarThemeData(
indicatorColor: accent.withOpacity(accentColor == AccentColor.adaptive ? 0.4 : 0.8),
iconTheme: MaterialStateProperty.all(IconThemeData(color: darkColors.text)),
backgroundColor: highlightColor,
labelTextStyle: MaterialStateProperty.all(TextStyle(
fontSize: 13.0,
fontWeight: FontWeight.w500,
color: darkColors.text.withOpacity(0.8),
)),
labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
height: 76.0,
),
sliderTheme: SliderThemeData(
inactiveTrackColor: accent.withOpacity(.3),
),
progressIndicatorTheme: ProgressIndicatorThemeData(color: accent),
expansionTileTheme: ExpansionTileThemeData(iconColor: accent),
cardColor: highlightColor,
chipTheme: ChipThemeData(
backgroundColor: accent.withOpacity(.2),
elevation: 1,
),
bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: Provider.of<ThemeModeObserver>(context, listen: false).updateNavbarColor ? backgroundColor : null,
),
);
}
}
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/theme/colors/accent.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo/theme/observer.dart';
import 'package:flutter/material.dart';
import 'package:material_color_utilities/material_color_utilities.dart';
import 'package:provider/provider.dart';
class AppTheme {
// Dev note: All of these could be constant variables, but this is better for
// development (you don't have to hot-restart)
static const String _fontFamily = "Montserrat";
static Color? _paletteAccentLight(CorePalette? palette) => palette != null ? Color(palette.primary.get(70)) : null;
static Color? _paletteHighlightLight(CorePalette? palette) => palette != null ? Color(palette.neutral.get(100)) : null;
static Color? _paletteBackgroundLight(CorePalette? palette) => palette != null ? Color(palette.neutral.get(95)) : null;
static Color? _paletteAccentDark(CorePalette? palette) => palette != null ? Color(palette.primary.get(80)) : null;
static Color? _paletteBackgroundDark(CorePalette? palette) => palette != null ? Color(palette.neutralVariant.get(10)) : null;
static Color? _paletteHighlightDark(CorePalette? palette) => palette != null ? Color(palette.neutralVariant.get(20)) : null;
// Light Theme
static ThemeData lightTheme(BuildContext context, {CorePalette? palette}) {
var lightColors = AppColors.fromBrightness(Brightness.light);
final settings = Provider.of<SettingsProvider>(context, listen: false);
AccentColor accentColor = settings.accentColor;
final customAccentColor = accentColor == AccentColor.custom ? settings.customAccentColor : null;
Color accent = customAccentColor ?? accentColorMap[accentColor] ?? const Color(0x00000000);
if (accentColor == AccentColor.adaptive) {
if (palette != null) accent = _paletteAccentLight(palette)!;
} else {
palette = null;
}
Color backgroundColor =
(accentColor == AccentColor.custom ? settings.customBackgroundColor : _paletteBackgroundLight(palette)) ?? lightColors.background;
Color highlightColor =
(accentColor == AccentColor.custom ? settings.customHighlightColor : _paletteHighlightLight(palette)) ?? lightColors.highlight;
return ThemeData(
brightness: Brightness.light,
useMaterial3: true,
fontFamily: _fontFamily,
scaffoldBackgroundColor: backgroundColor,
primaryColor: lightColors.filc,
dividerColor: const Color(0x00000000),
colorScheme: ColorScheme(
primary: accent,
onPrimary: (accent.computeLuminance() > 0.5 ? Colors.black : Colors.white).withOpacity(.9),
secondary: accent,
onSecondary: (accent.computeLuminance() > 0.5 ? Colors.black : Colors.white).withOpacity(.9),
background: highlightColor,
onBackground: Colors.black.withOpacity(.9),
brightness: Brightness.light,
error: lightColors.red,
onError: Colors.white.withOpacity(.9),
surface: highlightColor,
onSurface: Colors.black.withOpacity(.9),
),
shadowColor: lightColors.shadow.withOpacity(.5),
appBarTheme: AppBarTheme(backgroundColor: backgroundColor),
indicatorColor: accent,
iconTheme: IconThemeData(color: lightColors.text.withOpacity(.75)),
navigationBarTheme: NavigationBarThemeData(
indicatorColor: accent.withOpacity(accentColor == AccentColor.adaptive ? 0.4 : 0.8),
iconTheme: MaterialStateProperty.all(IconThemeData(color: lightColors.text)),
backgroundColor: highlightColor,
labelTextStyle: MaterialStateProperty.all(TextStyle(
fontSize: 13.0,
fontWeight: FontWeight.w500,
color: lightColors.text.withOpacity(0.8),
)),
labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
height: 76.0,
),
sliderTheme: SliderThemeData(
inactiveTrackColor: accent.withOpacity(.3),
),
progressIndicatorTheme: ProgressIndicatorThemeData(color: accent),
expansionTileTheme: ExpansionTileThemeData(iconColor: accent),
cardColor: highlightColor,
bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: Provider.of<ThemeModeObserver>(context, listen: false).updateNavbarColor ? backgroundColor : null,
),
);
}
// Dark Theme
static ThemeData darkTheme(BuildContext context, {CorePalette? palette}) {
var darkColors = AppColors.fromBrightness(Brightness.dark);
final settings = Provider.of<SettingsProvider>(context, listen: false);
AccentColor accentColor = settings.accentColor;
final customAccentColor = accentColor == AccentColor.custom ? settings.customAccentColor : null;
Color accent = customAccentColor ?? accentColorMap[accentColor] ?? const Color(0x00000000);
if (accentColor == AccentColor.adaptive) {
if (palette != null) accent = _paletteAccentDark(palette)!;
} else {
palette = null;
}
Color backgroundColor =
(accentColor == AccentColor.custom ? settings.customBackgroundColor : _paletteBackgroundDark(palette)) ?? darkColors.background;
Color highlightColor =
(accentColor == AccentColor.custom ? settings.customHighlightColor : _paletteHighlightDark(palette)) ?? darkColors.highlight;
return ThemeData(
brightness: Brightness.dark,
useMaterial3: true,
fontFamily: _fontFamily,
scaffoldBackgroundColor: backgroundColor,
primaryColor: darkColors.filc,
dividerColor: const Color(0x00000000),
colorScheme: ColorScheme(
primary: accent,
onPrimary: (accent.computeLuminance() > 0.5 ? Colors.black : Colors.white).withOpacity(.9),
secondary: accent,
onSecondary: (accent.computeLuminance() > 0.5 ? Colors.black : Colors.white).withOpacity(.9),
background: highlightColor,
onBackground: Colors.white.withOpacity(.9),
brightness: Brightness.dark,
error: darkColors.red,
onError: Colors.black.withOpacity(.9),
surface: highlightColor,
onSurface: Colors.white.withOpacity(.9),
),
shadowColor: highlightColor.withOpacity(.5), //darkColors.shadow,
appBarTheme: AppBarTheme(backgroundColor: backgroundColor),
indicatorColor: accent,
iconTheme: IconThemeData(color: darkColors.text.withOpacity(.75)),
navigationBarTheme: NavigationBarThemeData(
indicatorColor: accent.withOpacity(accentColor == AccentColor.adaptive ? 0.4 : 0.8),
iconTheme: MaterialStateProperty.all(IconThemeData(color: darkColors.text)),
backgroundColor: highlightColor,
labelTextStyle: MaterialStateProperty.all(TextStyle(
fontSize: 13.0,
fontWeight: FontWeight.w500,
color: darkColors.text.withOpacity(0.8),
)),
labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
height: 76.0,
),
sliderTheme: SliderThemeData(
inactiveTrackColor: accent.withOpacity(.3),
),
progressIndicatorTheme: ProgressIndicatorThemeData(color: accent),
expansionTileTheme: ExpansionTileThemeData(iconColor: accent),
cardColor: highlightColor,
chipTheme: ChipThemeData(
backgroundColor: accent.withOpacity(.2),
elevation: 1,
),
bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: Provider.of<ThemeModeObserver>(context, listen: false).updateNavbarColor ? backgroundColor : null,
),
);
}
}

View File

@@ -1,8 +1,8 @@
import 'package:flutter/widgets.dart';
class DateWidget {
final DateTime date;
final Widget widget;
final String? key;
const DateWidget({required this.date, required this.widget, this.key});
}
import 'package:flutter/widgets.dart';
class DateWidget {
final DateTime date;
final Widget widget;
final String? key;
const DateWidget({required this.date, required this.widget, this.key});
}

View File

@@ -1,159 +1,159 @@
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo/ui/date_widget.dart';
import 'package:filcnaplo/ui/filter/widgets.dart';
import 'package:filcnaplo/ui/widgets/message/message_tile.dart';
import 'package:filcnaplo_kreta_api/models/message.dart';
import 'package:filcnaplo_mobile_ui/common/panel/panel.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/absence/absence_viewable.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/absence_group/absence_group_tile.dart';
import 'package:flutter/material.dart';
import 'package:animated_list_plus/animated_list_plus.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/lesson/changed_lesson_tile.dart';
import 'package:filcnaplo/utils/format.dart';
// difference.inDays is not reliable
bool _sameDate(DateTime a, DateTime b) => (a.year == b.year && a.month == b.month && a.day == b.day);
List<Widget> sortDateWidgets(
BuildContext context, {
required List<DateWidget> dateWidgets,
bool showTitle = true,
bool showDivider = false,
bool hasShadow = false,
EdgeInsetsGeometry? padding,
}) {
dateWidgets.sort((a, b) => -a.date.compareTo(b.date));
List<Conversation> conversations = [];
List<DateWidget> convMessages = [];
// Group messages into conversations
for (var w in dateWidgets) {
if (w.widget.runtimeType == MessageTile) {
Message message = (w.widget as MessageTile).message;
if (message.conversationId != null) {
convMessages.add(w);
Conversation conv = conversations.firstWhere((e) => e.id == message.conversationId, orElse: () => Conversation(id: message.conversationId!));
conv.add(message);
if (conv.messages.length == 1) conversations.add(conv);
}
if (conversations.any((c) => c.id == message.messageId)) {
Conversation conv = conversations.firstWhere((e) => e.id == message.messageId);
convMessages.add(w);
conv.add(message);
}
}
}
// remove individual messages
for (var e in convMessages) {
dateWidgets.remove(e);
}
// Add conversations
for (var conv in conversations) {
conv.sort();
dateWidgets.add(DateWidget(
key: "${conv.newest.date.millisecondsSinceEpoch}-msg",
date: conv.newest.date,
widget: MessageTile(
conv.newest,
),
));
}
dateWidgets.sort((a, b) => -a.date.compareTo(b.date));
List<List<DateWidget>> groupedDateWidgets = [[]];
for (var element in dateWidgets) {
if (groupedDateWidgets.last.isNotEmpty) {
if (!_sameDate(element.date, groupedDateWidgets.last.last.date)) {
groupedDateWidgets.add([element]);
continue;
}
}
groupedDateWidgets.last.add(element);
}
List<DateWidget> items = [];
if (groupedDateWidgets.first.isNotEmpty) {
for (var elements in groupedDateWidgets) {
bool cst = showTitle;
// Group Absence Tiles
List<DateWidget> absenceTileWidgets = elements.where((element) {
return element.widget is AbsenceViewable && (element.widget as AbsenceViewable).absence.delay == 0;
}).toList();
List<AbsenceViewable> absenceTiles = absenceTileWidgets.map((e) => e.widget as AbsenceViewable).toList();
if (absenceTiles.length > 1) {
elements.removeWhere((element) => element.widget.runtimeType == AbsenceViewable && (element.widget as AbsenceViewable).absence.delay == 0);
if (elements.isEmpty) {
cst = false;
}
elements.add(DateWidget(
widget: AbsenceGroupTile(absenceTiles, showDate: !cst),
date: absenceTileWidgets.first.date,
key: "${absenceTileWidgets.first.date.millisecondsSinceEpoch}-absence-group"));
}
// Bring Lesson Tiles to front & sort by index asc
List<DateWidget> lessonTiles = elements.where((element) {
return element.widget.runtimeType == ChangedLessonTile;
}).toList();
lessonTiles.sort((a, b) => (a.widget as ChangedLessonTile).lesson.lessonIndex.compareTo((b.widget as ChangedLessonTile).lesson.lessonIndex));
elements.removeWhere((element) => element.widget.runtimeType == ChangedLessonTile);
elements.insertAll(0, lessonTiles);
final date = (elements + absenceTileWidgets).first.date;
items.add(DateWidget(
date: date,
widget: Panel(
key: ValueKey(date),
padding: padding ?? const EdgeInsets.symmetric(vertical: 6.0),
title: cst ? Text(date.format(context, forceToday: true)) : null,
hasShadow: hasShadow,
child: ImplicitlyAnimatedList<DateWidget>(
areItemsTheSame: (a, b) => a.key == b.key,
spawnIsolate: false,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, animation, item, index) => filterItemBuilder(context, animation, item.widget, index),
items: elements,
),
),
));
}
}
final nh = DateTime.now();
final now = DateTime(nh.year, nh.month, nh.day).subtract(const Duration(seconds: 1));
if (showDivider && items.any((i) => i.date.isBefore(now)) && items.any((i) => i.date.isAfter(now))) {
items.add(
DateWidget(
date: now,
widget: Center(
child: Container(
margin: const EdgeInsets.symmetric(vertical: 12.0),
height: 3.0,
width: 150.0,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.0),
color: AppColors.of(context).text.withOpacity(.25),
),
),
),
),
);
}
// Sort future dates asc, past dates desc
items.sort((a, b) => (a.date.isAfter(now) && b.date.isAfter(now) ? 1 : -1) * a.date.compareTo(b.date));
return items.map((e) => e.widget).toList();
}
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo/ui/date_widget.dart';
import 'package:filcnaplo/ui/filter/widgets.dart';
import 'package:filcnaplo/ui/widgets/message/message_tile.dart';
import 'package:filcnaplo_kreta_api/models/message.dart';
import 'package:filcnaplo_mobile_ui/common/panel/panel.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/absence/absence_viewable.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/absence_group/absence_group_tile.dart';
import 'package:flutter/material.dart';
import 'package:animated_list_plus/animated_list_plus.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/lesson/changed_lesson_tile.dart';
import 'package:filcnaplo/utils/format.dart';
// difference.inDays is not reliable
bool _sameDate(DateTime a, DateTime b) => (a.year == b.year && a.month == b.month && a.day == b.day);
List<Widget> sortDateWidgets(
BuildContext context, {
required List<DateWidget> dateWidgets,
bool showTitle = true,
bool showDivider = false,
bool hasShadow = false,
EdgeInsetsGeometry? padding,
}) {
dateWidgets.sort((a, b) => -a.date.compareTo(b.date));
List<Conversation> conversations = [];
List<DateWidget> convMessages = [];
// Group messages into conversations
for (var w in dateWidgets) {
if (w.widget.runtimeType == MessageTile) {
Message message = (w.widget as MessageTile).message;
if (message.conversationId != null) {
convMessages.add(w);
Conversation conv = conversations.firstWhere((e) => e.id == message.conversationId, orElse: () => Conversation(id: message.conversationId!));
conv.add(message);
if (conv.messages.length == 1) conversations.add(conv);
}
if (conversations.any((c) => c.id == message.messageId)) {
Conversation conv = conversations.firstWhere((e) => e.id == message.messageId);
convMessages.add(w);
conv.add(message);
}
}
}
// remove individual messages
for (var e in convMessages) {
dateWidgets.remove(e);
}
// Add conversations
for (var conv in conversations) {
conv.sort();
dateWidgets.add(DateWidget(
key: "${conv.newest.date.millisecondsSinceEpoch}-msg",
date: conv.newest.date,
widget: MessageTile(
conv.newest,
),
));
}
dateWidgets.sort((a, b) => -a.date.compareTo(b.date));
List<List<DateWidget>> groupedDateWidgets = [[]];
for (var element in dateWidgets) {
if (groupedDateWidgets.last.isNotEmpty) {
if (!_sameDate(element.date, groupedDateWidgets.last.last.date)) {
groupedDateWidgets.add([element]);
continue;
}
}
groupedDateWidgets.last.add(element);
}
List<DateWidget> items = [];
if (groupedDateWidgets.first.isNotEmpty) {
for (var elements in groupedDateWidgets) {
bool cst = showTitle;
// Group Absence Tiles
List<DateWidget> absenceTileWidgets = elements.where((element) {
return element.widget is AbsenceViewable && (element.widget as AbsenceViewable).absence.delay == 0;
}).toList();
List<AbsenceViewable> absenceTiles = absenceTileWidgets.map((e) => e.widget as AbsenceViewable).toList();
if (absenceTiles.length > 1) {
elements.removeWhere((element) => element.widget.runtimeType == AbsenceViewable && (element.widget as AbsenceViewable).absence.delay == 0);
if (elements.isEmpty) {
cst = false;
}
elements.add(DateWidget(
widget: AbsenceGroupTile(absenceTiles, showDate: !cst),
date: absenceTileWidgets.first.date,
key: "${absenceTileWidgets.first.date.millisecondsSinceEpoch}-absence-group"));
}
// Bring Lesson Tiles to front & sort by index asc
List<DateWidget> lessonTiles = elements.where((element) {
return element.widget.runtimeType == ChangedLessonTile;
}).toList();
lessonTiles.sort((a, b) => (a.widget as ChangedLessonTile).lesson.lessonIndex.compareTo((b.widget as ChangedLessonTile).lesson.lessonIndex));
elements.removeWhere((element) => element.widget.runtimeType == ChangedLessonTile);
elements.insertAll(0, lessonTiles);
final date = (elements + absenceTileWidgets).first.date;
items.add(DateWidget(
date: date,
widget: Panel(
key: ValueKey(date),
padding: padding ?? const EdgeInsets.symmetric(vertical: 6.0),
title: cst ? Text(date.format(context, forceToday: true)) : null,
hasShadow: hasShadow,
child: ImplicitlyAnimatedList<DateWidget>(
areItemsTheSame: (a, b) => a.key == b.key,
spawnIsolate: false,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, animation, item, index) => filterItemBuilder(context, animation, item.widget, index),
items: elements,
),
),
));
}
}
final nh = DateTime.now();
final now = DateTime(nh.year, nh.month, nh.day).subtract(const Duration(seconds: 1));
if (showDivider && items.any((i) => i.date.isBefore(now)) && items.any((i) => i.date.isAfter(now))) {
items.add(
DateWidget(
date: now,
widget: Center(
child: Container(
margin: const EdgeInsets.symmetric(vertical: 12.0),
height: 3.0,
width: 150.0,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.0),
color: AppColors.of(context).text.withOpacity(.25),
),
),
),
),
);
}
// Sort future dates asc, past dates desc
items.sort((a, b) => (a.date.isAfter(now) && b.date.isAfter(now) ? 1 : -1) * a.date.compareTo(b.date));
return items.map((e) => e.widget).toList();
}

View File

@@ -1,186 +1,186 @@
import 'package:filcnaplo/api/providers/update_provider.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/ui/date_widget.dart';
import 'package:filcnaplo/ui/filter/widgets/grades.dart' as grade_filter;
import 'package:filcnaplo/ui/filter/widgets/certifications.dart' as certification_filter;
import 'package:filcnaplo/ui/filter/widgets/messages.dart' as message_filter;
import 'package:filcnaplo/ui/filter/widgets/absences.dart' as absence_filter;
import 'package:filcnaplo/ui/filter/widgets/homework.dart' as homework_filter;
import 'package:filcnaplo/ui/filter/widgets/exams.dart' as exam_filter;
import 'package:filcnaplo/ui/filter/widgets/notes.dart' as note_filter;
import 'package:filcnaplo/ui/filter/widgets/events.dart' as event_filter;
import 'package:filcnaplo/ui/filter/widgets/lessons.dart' as lesson_filter;
import 'package:filcnaplo/ui/filter/widgets/update.dart' as update_filter;
import 'package:filcnaplo/ui/filter/widgets/missed_exams.dart' as missed_exam_filter;
import 'package:filcnaplo_kreta_api/models/week.dart';
import 'package:filcnaplo_kreta_api/providers/absence_provider.dart';
import 'package:filcnaplo_kreta_api/providers/event_provider.dart';
import 'package:filcnaplo_kreta_api/providers/exam_provider.dart';
import 'package:filcnaplo_kreta_api/providers/grade_provider.dart';
import 'package:filcnaplo_kreta_api/providers/homework_provider.dart';
import 'package:filcnaplo_kreta_api/providers/message_provider.dart';
import 'package:filcnaplo_kreta_api/providers/note_provider.dart';
import 'package:filcnaplo_kreta_api/providers/timetable_provider.dart';
import 'package:filcnaplo_premium/providers/premium_provider.dart';
import 'package:filcnaplo_premium/ui/mobile/premium/premium_inline.dart';
import 'package:filcnaplo_mobile_ui/common/panel/panel.dart';
import 'package:flutter/material.dart';
import 'package:animated_list_plus/transitions.dart';
import 'package:provider/provider.dart';
const List<FilterType> homeFilters = [FilterType.all, FilterType.grades, FilterType.messages, FilterType.absences];
enum FilterType { all, grades, newGrades, messages, absences, homework, exams, notes, events, lessons, updates, certifications, missedExams }
Future<List<DateWidget>> getFilterWidgets(FilterType activeData, {bool absencesNoExcused = false, required BuildContext context}) async {
final gradeProvider = Provider.of<GradeProvider>(context);
final timetableProvider = Provider.of<TimetableProvider>(context);
final messageProvider = Provider.of<MessageProvider>(context);
final absenceProvider = Provider.of<AbsenceProvider>(context);
final homeworkProvider = Provider.of<HomeworkProvider>(context);
final examProvider = Provider.of<ExamProvider>(context);
final noteProvider = Provider.of<NoteProvider>(context);
final eventProvider = Provider.of<EventProvider>(context);
final updateProvider = Provider.of<UpdateProvider>(context);
final settingsProvider = Provider.of<SettingsProvider>(context);
List<DateWidget> items = [];
switch (activeData) {
// All
case FilterType.all:
final all = await Future.wait<List<DateWidget>>([
getFilterWidgets(FilterType.grades, context: context),
getFilterWidgets(FilterType.lessons, context: context),
getFilterWidgets(FilterType.messages, context: context),
getFilterWidgets(FilterType.absences, context: context, absencesNoExcused: true),
getFilterWidgets(FilterType.homework, context: context),
getFilterWidgets(FilterType.exams, context: context),
getFilterWidgets(FilterType.updates, context: context),
getFilterWidgets(FilterType.certifications, context: context),
getFilterWidgets(FilterType.missedExams, context: context),
]);
items = all.expand((x) => x).toList();
break;
// Grades
case FilterType.grades:
items = grade_filter.getWidgets(gradeProvider.grades, gradeProvider.lastSeenDate);
if (settingsProvider.gradeOpeningFun) {
items.addAll(await getFilterWidgets(FilterType.newGrades, context: context));
}
break;
// Grades
case FilterType.newGrades:
items = grade_filter.getNewWidgets(gradeProvider.grades, gradeProvider.lastSeenDate);
break;
// Certifications
case FilterType.certifications:
items = certification_filter.getWidgets(gradeProvider.grades);
break;
// Messages
case FilterType.messages:
items = message_filter.getWidgets(
messageProvider.messages,
noteProvider.notes,
eventProvider.events,
);
break;
// Absences
case FilterType.absences:
items = absence_filter.getWidgets(absenceProvider.absences, noExcused: absencesNoExcused);
break;
// Homework
case FilterType.homework:
items = homework_filter.getWidgets(homeworkProvider.homework);
break;
// Exams
case FilterType.exams:
items = exam_filter.getWidgets(examProvider.exams);
break;
// Notes
case FilterType.notes:
items = note_filter.getWidgets(noteProvider.notes);
break;
// Events
case FilterType.events:
items = event_filter.getWidgets(eventProvider.events);
break;
// Changed Lessons
case FilterType.lessons:
items = lesson_filter.getWidgets(timetableProvider.getWeek(Week.current()) ?? []);
break;
// Updates
case FilterType.updates:
if (updateProvider.available) items = [update_filter.getWidget(updateProvider.releases.first)];
break;
// Missed Exams
case FilterType.missedExams:
items = missed_exam_filter.getWidgets(timetableProvider.getWeek(Week.current()) ?? []);
break;
}
return items;
}
Widget filterItemBuilder(BuildContext context, Animation<double> animation, Widget item, int index) {
if (item.key == const Key("\$premium")) {
return Provider.of<PremiumProvider>(context, listen: false).hasPremium || DateTime.now().weekday <= 5
? const SizedBox()
: const Padding(
padding: EdgeInsets.only(bottom: 24.0),
child: PremiumInline(features: [
PremiumInlineFeature.nickname,
PremiumInlineFeature.theme,
PremiumInlineFeature.widget,
]),
);
}
final wrappedItem = SizeFadeTransition(
curve: Curves.easeInOutCubic,
animation: animation,
child: item,
);
return item is Panel
// Re-add & animate shadow
? AnimatedBuilder(
animation: animation,
child: wrappedItem,
builder: (context, child) {
return Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: DecoratedBox(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
offset: const Offset(0, 21),
blurRadius: 23.0,
color: Theme.of(context).shadowColor.withOpacity(
Theme.of(context).shadowColor.opacity *
CurvedAnimation(
parent: CurvedAnimation(parent: animation, curve: Curves.easeInOutCubic),
curve: const Interval(2 / 3, 1.0),
).value,
),
),
],
),
child: child,
),
);
})
: wrappedItem;
}
import 'package:filcnaplo/api/providers/update_provider.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/ui/date_widget.dart';
import 'package:filcnaplo/ui/filter/widgets/grades.dart' as grade_filter;
import 'package:filcnaplo/ui/filter/widgets/certifications.dart' as certification_filter;
import 'package:filcnaplo/ui/filter/widgets/messages.dart' as message_filter;
import 'package:filcnaplo/ui/filter/widgets/absences.dart' as absence_filter;
import 'package:filcnaplo/ui/filter/widgets/homework.dart' as homework_filter;
import 'package:filcnaplo/ui/filter/widgets/exams.dart' as exam_filter;
import 'package:filcnaplo/ui/filter/widgets/notes.dart' as note_filter;
import 'package:filcnaplo/ui/filter/widgets/events.dart' as event_filter;
import 'package:filcnaplo/ui/filter/widgets/lessons.dart' as lesson_filter;
import 'package:filcnaplo/ui/filter/widgets/update.dart' as update_filter;
import 'package:filcnaplo/ui/filter/widgets/missed_exams.dart' as missed_exam_filter;
import 'package:filcnaplo_kreta_api/models/week.dart';
import 'package:filcnaplo_kreta_api/providers/absence_provider.dart';
import 'package:filcnaplo_kreta_api/providers/event_provider.dart';
import 'package:filcnaplo_kreta_api/providers/exam_provider.dart';
import 'package:filcnaplo_kreta_api/providers/grade_provider.dart';
import 'package:filcnaplo_kreta_api/providers/homework_provider.dart';
import 'package:filcnaplo_kreta_api/providers/message_provider.dart';
import 'package:filcnaplo_kreta_api/providers/note_provider.dart';
import 'package:filcnaplo_kreta_api/providers/timetable_provider.dart';
import 'package:filcnaplo_premium/providers/premium_provider.dart';
import 'package:filcnaplo_premium/ui/mobile/premium/premium_inline.dart';
import 'package:filcnaplo_mobile_ui/common/panel/panel.dart';
import 'package:flutter/material.dart';
import 'package:animated_list_plus/transitions.dart';
import 'package:provider/provider.dart';
const List<FilterType> homeFilters = [FilterType.all, FilterType.grades, FilterType.messages, FilterType.absences];
enum FilterType { all, grades, newGrades, messages, absences, homework, exams, notes, events, lessons, updates, certifications, missedExams }
Future<List<DateWidget>> getFilterWidgets(FilterType activeData, {bool absencesNoExcused = false, required BuildContext context}) async {
final gradeProvider = Provider.of<GradeProvider>(context);
final timetableProvider = Provider.of<TimetableProvider>(context);
final messageProvider = Provider.of<MessageProvider>(context);
final absenceProvider = Provider.of<AbsenceProvider>(context);
final homeworkProvider = Provider.of<HomeworkProvider>(context);
final examProvider = Provider.of<ExamProvider>(context);
final noteProvider = Provider.of<NoteProvider>(context);
final eventProvider = Provider.of<EventProvider>(context);
final updateProvider = Provider.of<UpdateProvider>(context);
final settingsProvider = Provider.of<SettingsProvider>(context);
List<DateWidget> items = [];
switch (activeData) {
// All
case FilterType.all:
final all = await Future.wait<List<DateWidget>>([
getFilterWidgets(FilterType.grades, context: context),
getFilterWidgets(FilterType.lessons, context: context),
getFilterWidgets(FilterType.messages, context: context),
getFilterWidgets(FilterType.absences, context: context, absencesNoExcused: true),
getFilterWidgets(FilterType.homework, context: context),
getFilterWidgets(FilterType.exams, context: context),
getFilterWidgets(FilterType.updates, context: context),
getFilterWidgets(FilterType.certifications, context: context),
getFilterWidgets(FilterType.missedExams, context: context),
]);
items = all.expand((x) => x).toList();
break;
// Grades
case FilterType.grades:
items = grade_filter.getWidgets(gradeProvider.grades, gradeProvider.lastSeenDate);
if (settingsProvider.gradeOpeningFun) {
items.addAll(await getFilterWidgets(FilterType.newGrades, context: context));
}
break;
// Grades
case FilterType.newGrades:
items = grade_filter.getNewWidgets(gradeProvider.grades, gradeProvider.lastSeenDate);
break;
// Certifications
case FilterType.certifications:
items = certification_filter.getWidgets(gradeProvider.grades);
break;
// Messages
case FilterType.messages:
items = message_filter.getWidgets(
messageProvider.messages,
noteProvider.notes,
eventProvider.events,
);
break;
// Absences
case FilterType.absences:
items = absence_filter.getWidgets(absenceProvider.absences, noExcused: absencesNoExcused);
break;
// Homework
case FilterType.homework:
items = homework_filter.getWidgets(homeworkProvider.homework);
break;
// Exams
case FilterType.exams:
items = exam_filter.getWidgets(examProvider.exams);
break;
// Notes
case FilterType.notes:
items = note_filter.getWidgets(noteProvider.notes);
break;
// Events
case FilterType.events:
items = event_filter.getWidgets(eventProvider.events);
break;
// Changed Lessons
case FilterType.lessons:
items = lesson_filter.getWidgets(timetableProvider.getWeek(Week.current()) ?? []);
break;
// Updates
case FilterType.updates:
if (updateProvider.available) items = [update_filter.getWidget(updateProvider.releases.first)];
break;
// Missed Exams
case FilterType.missedExams:
items = missed_exam_filter.getWidgets(timetableProvider.getWeek(Week.current()) ?? []);
break;
}
return items;
}
Widget filterItemBuilder(BuildContext context, Animation<double> animation, Widget item, int index) {
if (item.key == const Key("\$premium")) {
return Provider.of<PremiumProvider>(context, listen: false).hasPremium || DateTime.now().weekday <= 5
? const SizedBox()
: const Padding(
padding: EdgeInsets.only(bottom: 24.0),
child: PremiumInline(features: [
PremiumInlineFeature.nickname,
PremiumInlineFeature.theme,
PremiumInlineFeature.widget,
]),
);
}
final wrappedItem = SizeFadeTransition(
curve: Curves.easeInOutCubic,
animation: animation,
child: item,
);
return item is Panel
// Re-add & animate shadow
? AnimatedBuilder(
animation: animation,
child: wrappedItem,
builder: (context, child) {
return Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: DecoratedBox(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
offset: const Offset(0, 21),
blurRadius: 23.0,
color: Theme.of(context).shadowColor.withOpacity(
Theme.of(context).shadowColor.opacity *
CurvedAnimation(
parent: CurvedAnimation(parent: animation, curve: Curves.easeInOutCubic),
curve: const Interval(2 / 3, 1.0),
).value,
),
),
],
),
child: child,
),
);
})
: wrappedItem;
}

View File

@@ -1,15 +1,15 @@
import 'package:filcnaplo/ui/date_widget.dart';
import 'package:filcnaplo_kreta_api/models/absence.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/absence/absence_viewable.dart' as mobile;
List<DateWidget> getWidgets(List<Absence> providerAbsences, {bool noExcused = false}) {
List<DateWidget> items = [];
providerAbsences.where((a) => !noExcused || a.state != Justification.excused).forEach((absence) {
items.add(DateWidget(
key: absence.id,
date: absence.date,
widget: mobile.AbsenceViewable(absence),
));
});
return items;
}
import 'package:filcnaplo/ui/date_widget.dart';
import 'package:filcnaplo_kreta_api/models/absence.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/absence/absence_viewable.dart' as mobile;
List<DateWidget> getWidgets(List<Absence> providerAbsences, {bool noExcused = false}) {
List<DateWidget> items = [];
providerAbsences.where((a) => !noExcused || a.state != Justification.excused).forEach((absence) {
items.add(DateWidget(
key: absence.id,
date: absence.date,
widget: mobile.AbsenceViewable(absence),
));
});
return items;
}

View File

@@ -1,24 +1,24 @@
import 'package:filcnaplo/ui/date_widget.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/cretification/certification_card.dart' as mobile;
List<DateWidget> getWidgets(List<Grade> providerGrades) {
List<DateWidget> items = [];
for (var gradeType in GradeType.values) {
if ([GradeType.midYear, GradeType.unknown, GradeType.levelExam].contains(gradeType)) continue;
List<Grade> grades = providerGrades.where((grade) => grade.type == gradeType).toList();
if (grades.isNotEmpty) {
grades.sort((a, b) => -a.date.compareTo(b.date));
items.add(DateWidget(
date: grades.first.date,
widget: mobile.CertificationCard(
grades,
gradeType: gradeType,
),
));
}
}
return items;
}
import 'package:filcnaplo/ui/date_widget.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/cretification/certification_card.dart' as mobile;
List<DateWidget> getWidgets(List<Grade> providerGrades) {
List<DateWidget> items = [];
for (var gradeType in GradeType.values) {
if ([GradeType.midYear, GradeType.unknown, GradeType.levelExam].contains(gradeType)) continue;
List<Grade> grades = providerGrades.where((grade) => grade.type == gradeType).toList();
if (grades.isNotEmpty) {
grades.sort((a, b) => -a.date.compareTo(b.date));
items.add(DateWidget(
date: grades.first.date,
widget: mobile.CertificationCard(
grades,
gradeType: gradeType,
),
));
}
}
return items;
}

View File

@@ -1,15 +1,15 @@
import 'package:filcnaplo/ui/date_widget.dart';
import 'package:filcnaplo_kreta_api/models/event.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/event/event_viewable.dart' as mobile;
List<DateWidget> getWidgets(List<Event> providerEvents) {
List<DateWidget> items = [];
for (var event in providerEvents) {
items.add(DateWidget(
key: event.id,
date: event.start,
widget: mobile.EventViewable(event),
));
}
return items;
}
import 'package:filcnaplo/ui/date_widget.dart';
import 'package:filcnaplo_kreta_api/models/event.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/event/event_viewable.dart' as mobile;
List<DateWidget> getWidgets(List<Event> providerEvents) {
List<DateWidget> items = [];
for (var event in providerEvents) {
items.add(DateWidget(
key: event.id,
date: event.start,
widget: mobile.EventViewable(event),
));
}
return items;
}

View File

@@ -1,15 +1,15 @@
import 'package:filcnaplo/ui/date_widget.dart';
import 'package:filcnaplo_kreta_api/models/exam.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/exam/exam_viewable.dart' as mobile;
List<DateWidget> getWidgets(List<Exam> providerExams) {
List<DateWidget> items = [];
for (var exam in providerExams) {
items.add(DateWidget(
key: exam.id,
date: exam.writeDate.year != 0 ? exam.writeDate : exam.date,
widget: mobile.ExamViewable(exam),
));
}
return items;
}
import 'package:filcnaplo/ui/date_widget.dart';
import 'package:filcnaplo_kreta_api/models/exam.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/exam/exam_viewable.dart' as mobile;
List<DateWidget> getWidgets(List<Exam> providerExams) {
List<DateWidget> items = [];
for (var exam in providerExams) {
items.add(DateWidget(
key: exam.id,
date: exam.writeDate.year != 0 ? exam.writeDate : exam.date,
widget: mobile.ExamViewable(exam),
));
}
return items;
}

View File

@@ -1,41 +1,41 @@
import 'package:filcnaplo/ui/date_widget.dart';
import 'package:filcnaplo/utils/platform.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/grade/grade_viewable.dart' as mobile;
import 'package:filcnaplo_mobile_ui/common/widgets/grade/new_grades.dart' as mobile;
import 'package:filcnaplo_desktop_ui/common/widgets/grade/grade_viewable.dart' as desktop;
List<DateWidget> getWidgets(List<Grade> providerGrades, DateTime? lastSeenDate) {
List<DateWidget> items = [];
for (var grade in providerGrades) {
final surprise = (!(lastSeenDate != null && grade.date.isAfter(lastSeenDate)) || grade.value.value == 0);
if (grade.type == GradeType.midYear && surprise) {
items.add(DateWidget(
key: grade.id,
date: grade.date,
widget: PlatformUtils.isMobile ? mobile.GradeViewable(grade) : desktop.GradeViewable(grade),
));
}
}
return items;
}
List<DateWidget> getNewWidgets(List<Grade> providerGrades, DateTime? lastSeenDate) {
List<DateWidget> items = [];
List<Grade> newGrades = [];
for (var grade in providerGrades) {
final surprise = !(lastSeenDate != null && !grade.date.isAfter(lastSeenDate)) && grade.value.value != 0;
if (grade.type == GradeType.midYear && surprise) {
newGrades.add(grade);
}
}
newGrades.sort((a, b) => a.date.compareTo(b.date));
if (newGrades.isNotEmpty) {
items.add(DateWidget(
key: newGrades.last.id,
date: newGrades.last.date,
widget: mobile.NewGradesSurprise(newGrades),
));
}
return items;
}
import 'package:filcnaplo/ui/date_widget.dart';
import 'package:filcnaplo/utils/platform.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/grade/grade_viewable.dart' as mobile;
import 'package:filcnaplo_mobile_ui/common/widgets/grade/new_grades.dart' as mobile;
import 'package:filcnaplo_desktop_ui/common/widgets/grade/grade_viewable.dart' as desktop;
List<DateWidget> getWidgets(List<Grade> providerGrades, DateTime? lastSeenDate) {
List<DateWidget> items = [];
for (var grade in providerGrades) {
final surprise = (!(lastSeenDate != null && grade.date.isAfter(lastSeenDate)) || grade.value.value == 0);
if (grade.type == GradeType.midYear && surprise) {
items.add(DateWidget(
key: grade.id,
date: grade.date,
widget: PlatformUtils.isMobile ? mobile.GradeViewable(grade) : desktop.GradeViewable(grade),
));
}
}
return items;
}
List<DateWidget> getNewWidgets(List<Grade> providerGrades, DateTime? lastSeenDate) {
List<DateWidget> items = [];
List<Grade> newGrades = [];
for (var grade in providerGrades) {
final surprise = !(lastSeenDate != null && !grade.date.isAfter(lastSeenDate)) && grade.value.value != 0;
if (grade.type == GradeType.midYear && surprise) {
newGrades.add(grade);
}
}
newGrades.sort((a, b) => a.date.compareTo(b.date));
if (newGrades.isNotEmpty) {
items.add(DateWidget(
key: newGrades.last.id,
date: newGrades.last.date,
widget: mobile.NewGradesSurprise(newGrades),
));
}
return items;
}

View File

@@ -1,15 +1,15 @@
import 'package:filcnaplo/ui/date_widget.dart';
import 'package:filcnaplo_kreta_api/models/homework.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/homework/homework_viewable.dart' as mobile;
List<DateWidget> getWidgets(List<Homework> providerHomework) {
List<DateWidget> items = [];
for (var homework in providerHomework) {
items.add(DateWidget(
key: homework.id,
date: homework.deadline.year != 0 ? homework.deadline : homework.date,
widget: mobile.HomeworkViewable(homework),
));
}
return items;
}
import 'package:filcnaplo/ui/date_widget.dart';
import 'package:filcnaplo_kreta_api/models/homework.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/homework/homework_viewable.dart' as mobile;
List<DateWidget> getWidgets(List<Homework> providerHomework) {
List<DateWidget> items = [];
for (var homework in providerHomework) {
items.add(DateWidget(
key: homework.id,
date: homework.deadline.year != 0 ? homework.deadline : homework.date,
widget: mobile.HomeworkViewable(homework),
));
}
return items;
}

View File

@@ -1,15 +1,15 @@
import 'package:filcnaplo/ui/date_widget.dart';
import 'package:filcnaplo_kreta_api/models/lesson.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/lesson/changed_lesson_viewable.dart' as mobile;
List<DateWidget> getWidgets(List<Lesson> providerLessons) {
List<DateWidget> items = [];
providerLessons.where((l) => l.isChanged && l.start.isAfter(DateTime.now())).forEach((lesson) {
items.add(DateWidget(
key: lesson.id,
date: DateTime(lesson.date.year, lesson.date.month, lesson.date.day, lesson.start.hour, lesson.start.minute),
widget: mobile.ChangedLessonViewable(lesson),
));
});
return items;
}
import 'package:filcnaplo/ui/date_widget.dart';
import 'package:filcnaplo_kreta_api/models/lesson.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/lesson/changed_lesson_viewable.dart' as mobile;
List<DateWidget> getWidgets(List<Lesson> providerLessons) {
List<DateWidget> items = [];
providerLessons.where((l) => l.isChanged && l.start.isAfter(DateTime.now())).forEach((lesson) {
items.add(DateWidget(
key: lesson.id,
date: DateTime(lesson.date.year, lesson.date.month, lesson.date.day, lesson.start.hour, lesson.start.minute),
widget: mobile.ChangedLessonViewable(lesson),
));
});
return items;
}

View File

@@ -1,23 +1,23 @@
import 'package:filcnaplo/ui/date_widget.dart';
import 'package:filcnaplo/ui/filter/widgets/notes.dart' as note_filter;
import 'package:filcnaplo/ui/filter/widgets/events.dart' as event_filter;
import 'package:filcnaplo_kreta_api/models/event.dart';
import 'package:filcnaplo_kreta_api/models/message.dart';
import 'package:filcnaplo_kreta_api/models/note.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/message/message_viewable.dart' as mobile;
List<DateWidget> getWidgets(List<Message> providerMessages, List<Note> providerNotes, List<Event> providerEvents) {
List<DateWidget> items = [];
for (var message in providerMessages) {
if (message.type == MessageType.inbox) {
items.add(DateWidget(
key: "${message.id}",
date: message.date,
widget: mobile.MessageViewable(message),
));
}
}
items.addAll(note_filter.getWidgets(providerNotes));
items.addAll(event_filter.getWidgets(providerEvents));
return items;
}
import 'package:filcnaplo/ui/date_widget.dart';
import 'package:filcnaplo/ui/filter/widgets/notes.dart' as note_filter;
import 'package:filcnaplo/ui/filter/widgets/events.dart' as event_filter;
import 'package:filcnaplo_kreta_api/models/event.dart';
import 'package:filcnaplo_kreta_api/models/message.dart';
import 'package:filcnaplo_kreta_api/models/note.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/message/message_viewable.dart' as mobile;
List<DateWidget> getWidgets(List<Message> providerMessages, List<Note> providerNotes, List<Event> providerEvents) {
List<DateWidget> items = [];
for (var message in providerMessages) {
if (message.type == MessageType.inbox) {
items.add(DateWidget(
key: "${message.id}",
date: message.date,
widget: mobile.MessageViewable(message),
));
}
}
items.addAll(note_filter.getWidgets(providerNotes));
items.addAll(event_filter.getWidgets(providerEvents));
return items;
}

View File

@@ -1,35 +1,35 @@
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo/ui/date_widget.dart';
import 'package:filcnaplo_kreta_api/models/lesson.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/missed_exam/missed_exam_viewable.dart';
List<DateWidget> getWidgets(List<Lesson> providerLessons) {
List<DateWidget> items = [];
List<Lesson> missedExams = [];
for (var lesson in providerLessons) {
final desc = lesson.description.toLowerCase().specialChars();
// Check if lesson description includes hints for an exam written during the lesson
if (!lesson.studentPresence &&
(lesson.exam != "" ||
desc.contains("dolgozat") ||
desc.contains("feleles") ||
desc.contains("temazaro") ||
desc.contains("szamonkeres") ||
desc == "tz") &&
!(desc.contains("felkeszules") || desc.contains("gyakorlas"))) {
missedExams.add(lesson);
}
}
if (missedExams.isNotEmpty) {
missedExams.sort((a, b) => -a.date.compareTo(b.date));
items.add(DateWidget(
date: missedExams.first.date,
widget: MissedExamViewable(missedExams),
));
}
return items;
}
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo/ui/date_widget.dart';
import 'package:filcnaplo_kreta_api/models/lesson.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/missed_exam/missed_exam_viewable.dart';
List<DateWidget> getWidgets(List<Lesson> providerLessons) {
List<DateWidget> items = [];
List<Lesson> missedExams = [];
for (var lesson in providerLessons) {
final desc = lesson.description.toLowerCase().specialChars();
// Check if lesson description includes hints for an exam written during the lesson
if (!lesson.studentPresence &&
(lesson.exam != "" ||
desc.contains("dolgozat") ||
desc.contains("feleles") ||
desc.contains("temazaro") ||
desc.contains("szamonkeres") ||
desc == "tz") &&
!(desc.contains("felkeszules") || desc.contains("gyakorlas"))) {
missedExams.add(lesson);
}
}
if (missedExams.isNotEmpty) {
missedExams.sort((a, b) => -a.date.compareTo(b.date));
items.add(DateWidget(
date: missedExams.first.date,
widget: MissedExamViewable(missedExams),
));
}
return items;
}

View File

@@ -1,15 +1,15 @@
import 'package:filcnaplo/ui/date_widget.dart';
import 'package:filcnaplo_kreta_api/models/note.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/note/note_viewable.dart' as mobile;
List<DateWidget> getWidgets(List<Note> providerNotes) {
List<DateWidget> items = [];
for (var note in providerNotes) {
items.add(DateWidget(
key: note.id,
date: note.date,
widget: mobile.NoteViewable(note),
));
}
return items;
}
import 'package:filcnaplo/ui/date_widget.dart';
import 'package:filcnaplo_kreta_api/models/note.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/note/note_viewable.dart' as mobile;
List<DateWidget> getWidgets(List<Note> providerNotes) {
List<DateWidget> items = [];
for (var note in providerNotes) {
items.add(DateWidget(
key: note.id,
date: note.date,
widget: mobile.NoteViewable(note),
));
}
return items;
}

View File

@@ -1,10 +1,10 @@
import 'package:filcnaplo/models/release.dart';
import 'package:filcnaplo/ui/date_widget.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/update/update_viewable.dart' as mobile;
DateWidget getWidget(Release providerRelease) {
return DateWidget(
date: DateTime.now(),
widget: mobile.UpdateViewable(providerRelease),
);
}
import 'package:filcnaplo/models/release.dart';
import 'package:filcnaplo/ui/date_widget.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/update/update_viewable.dart' as mobile;
DateWidget getWidget(Release providerRelease) {
return DateWidget(
date: DateTime.now(),
widget: mobile.UpdateViewable(providerRelease),
);
}

View File

@@ -1,302 +1,302 @@
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo/helpers/subject.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo_mobile_ui/pages/grades/calculator/grade_calculator_provider.dart';
import 'package:filcnaplo_mobile_ui/pages/grades/subject_grades_container.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:provider/provider.dart';
class GradeTile extends StatelessWidget {
const GradeTile(this.grade, {Key? key, this.onTap, this.padding, this.censored = false}) : super(key: key);
final Grade grade;
final void Function()? onTap;
final EdgeInsetsGeometry? padding;
final bool censored;
@override
Widget build(BuildContext context) {
String title;
String subtitle;
EdgeInsets leadingPadding = EdgeInsets.zero;
bool isSubjectView = SubjectGradesContainer.of(context) != null;
String subjectName = grade.subject.renamedTo ?? grade.subject.name.capital();
String modeDescription = grade.mode.description.capital();
String description = grade.description.capital();
GradeCalculatorProvider calculatorProvider = Provider.of<GradeCalculatorProvider>(context, listen: false);
// Test order:
// description
// mode
// value name
if (grade.type == GradeType.midYear || grade.type == GradeType.ghost) {
if (grade.description != "") {
title = description;
} else {
title = modeDescription != "" ? modeDescription : grade.value.valueName.split("(")[0];
}
} else {
title = subjectName;
}
// Test order:
// subject name
// mode + weight != 100
if (grade.type == GradeType.midYear) {
subtitle = isSubjectView
? description != ""
? modeDescription
: ""
: subjectName;
} else {
subtitle = grade.value.valueName.split("(")[0];
}
if (subtitle != "") leadingPadding = const EdgeInsets.only(top: 2.0);
return Material(
type: MaterialType.transparency,
child: Padding(
padding: padding ?? const EdgeInsets.symmetric(horizontal: 8.0),
child: ListTile(
visualDensity: VisualDensity.compact,
contentPadding: isSubjectView
? grade.type != GradeType.ghost
? const EdgeInsets.symmetric(horizontal: 12.0)
: const EdgeInsets.only(left: 12.0, right: 4.0)
: const EdgeInsets.only(left: 8.0, right: 12.0),
onTap: onTap,
// onLongPress: kDebugMode ? () => log(jsonEncode(grade.json)) : null,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14.0)),
leading: isSubjectView
? GradeValueWidget(grade.value)
: SizedBox(
width: 44,
height: 44,
child: censored
? Container(
decoration: BoxDecoration(
color: AppColors.of(context).text.withOpacity(.55),
borderRadius: BorderRadius.circular(60.0),
),
)
: Center(
child: Padding(
padding: leadingPadding,
child: Icon(
SubjectIcon.resolveVariant(subject: grade.subject, context: context),
size: 28.0,
color: AppColors.of(context).text.withOpacity(.75),
),
),
),
),
title: censored
? Wrap(
children: [
Container(
width: 110,
height: 15,
decoration: BoxDecoration(
color: AppColors.of(context).text.withOpacity(.85),
borderRadius: BorderRadius.circular(8.0),
),
),
],
)
: Text(
title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.w600, fontStyle: grade.subject.isRenamed && title == subjectName ? FontStyle.italic : null),
),
subtitle: subtitle != ""
? censored
? Wrap(
children: [
Container(
width: 50,
height: 10,
decoration: BoxDecoration(
color: AppColors.of(context).text.withOpacity(.45),
borderRadius: BorderRadius.circular(8.0),
),
),
],
)
: Text(
subtitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w500),
)
: null,
trailing: isSubjectView
? grade.type != GradeType.ghost
? Text(grade.date.format(context), style: const TextStyle(fontWeight: FontWeight.w500))
: IconButton(
splashRadius: 24.0,
icon: Icon(FeatherIcons.trash2, color: AppColors.of(context).red),
onPressed: () {
calculatorProvider.removeGrade(grade);
},
)
: censored
? Container(
width: 15,
height: 15,
decoration: BoxDecoration(
color: AppColors.of(context).text.withOpacity(.45),
borderRadius: BorderRadius.circular(8.0),
),
)
: GradeValueWidget(grade.value),
minLeadingWidth: isSubjectView ? 32.0 : 0,
),
),
);
}
}
class GradeValueWidget extends StatelessWidget {
const GradeValueWidget(
this.value, {
Key? key,
this.size = 38.0,
this.fill = false,
this.contrast = false,
this.shadow = false,
this.outline = false,
this.complemented = false,
this.nocolor = false,
}) : super(key: key);
final GradeValue value;
final double size;
final bool fill;
final bool contrast;
final bool shadow;
final bool outline;
final bool complemented;
final bool nocolor;
@override
Widget build(BuildContext context) {
GradeValue value = this.value;
bool isSubjectView = SubjectGradesContainer.of(context) != null;
Color color = gradeColor(context: context, value: value.value, nocolor: nocolor);
Widget valueText;
final percentage = value.percentage;
if (percentage) {
double multiplier = 1.0;
if (isSubjectView) multiplier = 0.75;
valueText = Text.rich(
TextSpan(
text: value.value.toString(),
children: [
TextSpan(
text: "\n%",
style: TextStyle(fontWeight: FontWeight.w700, fontSize: size / 2.5 * multiplier, height: 0.7),
),
],
style: TextStyle(fontWeight: FontWeight.w700, fontSize: size / 1 * multiplier, height: 1),
),
textAlign: TextAlign.center,
);
} else if (value.valueName.toLowerCase().specialChars() == 'nem irt') {
valueText = const Icon(FeatherIcons.slash);
} else {
valueText = Stack(alignment: Alignment.topRight, children: [
Transform.translate(
offset: (value.weight >= 200) ? const Offset(2, 1.5) : Offset.zero,
child: Text(
value.value.toString(),
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: value.weight == 50 ? FontWeight.w500 : FontWeight.bold,
fontSize: size,
color: contrast ? Colors.white : color,
shadows: [
if (value.weight >= 200)
Shadow(
color: (contrast ? Colors.white : color).withOpacity(.4),
offset: const Offset(-4, -3),
)
],
),
),
),
if (complemented)
Transform.translate(
offset: const Offset(9, 1),
child: Text(
"*",
style: TextStyle(fontSize: size / 1.6, fontWeight: FontWeight.bold),
),
),
]);
}
return fill
? Container(
width: size * 1.4,
height: size * 1.4,
decoration: BoxDecoration(
color: color.withOpacity(contrast ? 1.0 : .25),
shape: BoxShape.circle,
boxShadow: [
if (shadow)
BoxShadow(
color: color,
blurRadius: 62.0,
)
],
),
child: Center(child: valueText),
)
: valueText;
}
}
Color gradeColor({required BuildContext context, required num value, bool nocolor = false}) {
int valueInt = 0;
var settings = Provider.of<SettingsProvider>(context, listen: false);
try {
if (value < 2.0) {
valueInt = 1;
} else {
if (value >= value.floor() + settings.rounding / 10) {
valueInt = value.ceil();
} else {
valueInt = value.floor();
}
}
} catch (_) {}
if (nocolor) return AppColors.of(context).text;
switch (valueInt) {
case 5:
return settings.gradeColors[4];
case 4:
return settings.gradeColors[3];
case 3:
return settings.gradeColors[2];
case 2:
return settings.gradeColors[1];
case 1:
return settings.gradeColors[0];
default:
return AppColors.of(context).text;
}
}
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo/helpers/subject.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo_mobile_ui/pages/grades/calculator/grade_calculator_provider.dart';
import 'package:filcnaplo_mobile_ui/pages/grades/subject_grades_container.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:provider/provider.dart';
class GradeTile extends StatelessWidget {
const GradeTile(this.grade, {Key? key, this.onTap, this.padding, this.censored = false}) : super(key: key);
final Grade grade;
final void Function()? onTap;
final EdgeInsetsGeometry? padding;
final bool censored;
@override
Widget build(BuildContext context) {
String title;
String subtitle;
EdgeInsets leadingPadding = EdgeInsets.zero;
bool isSubjectView = SubjectGradesContainer.of(context) != null;
String subjectName = grade.subject.renamedTo ?? grade.subject.name.capital();
String modeDescription = grade.mode.description.capital();
String description = grade.description.capital();
GradeCalculatorProvider calculatorProvider = Provider.of<GradeCalculatorProvider>(context, listen: false);
// Test order:
// description
// mode
// value name
if (grade.type == GradeType.midYear || grade.type == GradeType.ghost) {
if (grade.description != "") {
title = description;
} else {
title = modeDescription != "" ? modeDescription : grade.value.valueName.split("(")[0];
}
} else {
title = subjectName;
}
// Test order:
// subject name
// mode + weight != 100
if (grade.type == GradeType.midYear) {
subtitle = isSubjectView
? description != ""
? modeDescription
: ""
: subjectName;
} else {
subtitle = grade.value.valueName.split("(")[0];
}
if (subtitle != "") leadingPadding = const EdgeInsets.only(top: 2.0);
return Material(
type: MaterialType.transparency,
child: Padding(
padding: padding ?? const EdgeInsets.symmetric(horizontal: 8.0),
child: ListTile(
visualDensity: VisualDensity.compact,
contentPadding: isSubjectView
? grade.type != GradeType.ghost
? const EdgeInsets.symmetric(horizontal: 12.0)
: const EdgeInsets.only(left: 12.0, right: 4.0)
: const EdgeInsets.only(left: 8.0, right: 12.0),
onTap: onTap,
// onLongPress: kDebugMode ? () => log(jsonEncode(grade.json)) : null,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14.0)),
leading: isSubjectView
? GradeValueWidget(grade.value)
: SizedBox(
width: 44,
height: 44,
child: censored
? Container(
decoration: BoxDecoration(
color: AppColors.of(context).text.withOpacity(.55),
borderRadius: BorderRadius.circular(60.0),
),
)
: Center(
child: Padding(
padding: leadingPadding,
child: Icon(
SubjectIcon.resolveVariant(subject: grade.subject, context: context),
size: 28.0,
color: AppColors.of(context).text.withOpacity(.75),
),
),
),
),
title: censored
? Wrap(
children: [
Container(
width: 110,
height: 15,
decoration: BoxDecoration(
color: AppColors.of(context).text.withOpacity(.85),
borderRadius: BorderRadius.circular(8.0),
),
),
],
)
: Text(
title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.w600, fontStyle: grade.subject.isRenamed && title == subjectName ? FontStyle.italic : null),
),
subtitle: subtitle != ""
? censored
? Wrap(
children: [
Container(
width: 50,
height: 10,
decoration: BoxDecoration(
color: AppColors.of(context).text.withOpacity(.45),
borderRadius: BorderRadius.circular(8.0),
),
),
],
)
: Text(
subtitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w500),
)
: null,
trailing: isSubjectView
? grade.type != GradeType.ghost
? Text(grade.date.format(context), style: const TextStyle(fontWeight: FontWeight.w500))
: IconButton(
splashRadius: 24.0,
icon: Icon(FeatherIcons.trash2, color: AppColors.of(context).red),
onPressed: () {
calculatorProvider.removeGrade(grade);
},
)
: censored
? Container(
width: 15,
height: 15,
decoration: BoxDecoration(
color: AppColors.of(context).text.withOpacity(.45),
borderRadius: BorderRadius.circular(8.0),
),
)
: GradeValueWidget(grade.value),
minLeadingWidth: isSubjectView ? 32.0 : 0,
),
),
);
}
}
class GradeValueWidget extends StatelessWidget {
const GradeValueWidget(
this.value, {
Key? key,
this.size = 38.0,
this.fill = false,
this.contrast = false,
this.shadow = false,
this.outline = false,
this.complemented = false,
this.nocolor = false,
}) : super(key: key);
final GradeValue value;
final double size;
final bool fill;
final bool contrast;
final bool shadow;
final bool outline;
final bool complemented;
final bool nocolor;
@override
Widget build(BuildContext context) {
GradeValue value = this.value;
bool isSubjectView = SubjectGradesContainer.of(context) != null;
Color color = gradeColor(context: context, value: value.value, nocolor: nocolor);
Widget valueText;
final percentage = value.percentage;
if (percentage) {
double multiplier = 1.0;
if (isSubjectView) multiplier = 0.75;
valueText = Text.rich(
TextSpan(
text: value.value.toString(),
children: [
TextSpan(
text: "\n%",
style: TextStyle(fontWeight: FontWeight.w700, fontSize: size / 2.5 * multiplier, height: 0.7),
),
],
style: TextStyle(fontWeight: FontWeight.w700, fontSize: size / 1 * multiplier, height: 1),
),
textAlign: TextAlign.center,
);
} else if (value.valueName.toLowerCase().specialChars() == 'nem irt') {
valueText = const Icon(FeatherIcons.slash);
} else {
valueText = Stack(alignment: Alignment.topRight, children: [
Transform.translate(
offset: (value.weight >= 200) ? const Offset(2, 1.5) : Offset.zero,
child: Text(
value.value.toString(),
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: value.weight == 50 ? FontWeight.w500 : FontWeight.bold,
fontSize: size,
color: contrast ? Colors.white : color,
shadows: [
if (value.weight >= 200)
Shadow(
color: (contrast ? Colors.white : color).withOpacity(.4),
offset: const Offset(-4, -3),
)
],
),
),
),
if (complemented)
Transform.translate(
offset: const Offset(9, 1),
child: Text(
"*",
style: TextStyle(fontSize: size / 1.6, fontWeight: FontWeight.bold),
),
),
]);
}
return fill
? Container(
width: size * 1.4,
height: size * 1.4,
decoration: BoxDecoration(
color: color.withOpacity(contrast ? 1.0 : .25),
shape: BoxShape.circle,
boxShadow: [
if (shadow)
BoxShadow(
color: color,
blurRadius: 62.0,
)
],
),
child: Center(child: valueText),
)
: valueText;
}
}
Color gradeColor({required BuildContext context, required num value, bool nocolor = false}) {
int valueInt = 0;
var settings = Provider.of<SettingsProvider>(context, listen: false);
try {
if (value < 2.0) {
valueInt = 1;
} else {
if (value >= value.floor() + settings.rounding / 10) {
valueInt = value.ceil();
} else {
valueInt = value.floor();
}
}
} catch (_) {}
if (nocolor) return AppColors.of(context).text;
switch (valueInt) {
case 5:
return settings.gradeColors[4];
case 4:
return settings.gradeColors[3];
case 3:
return settings.gradeColors[2];
case 2:
return settings.gradeColors[1];
case 1:
return settings.gradeColors[0];
default:
return AppColors.of(context).text;
}
}

View File

@@ -1,308 +1,308 @@
import 'package:filcnaplo_kreta_api/providers/exam_provider.dart';
import 'package:filcnaplo_kreta_api/providers/homework_provider.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo_kreta_api/models/exam.dart';
import 'package:filcnaplo_kreta_api/models/homework.dart';
import 'package:filcnaplo_kreta_api/models/lesson.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo_mobile_ui/common/panel/panel.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/exam/exam_view.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/homework/homework_view.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'lesson_tile.i18n.dart';
class LessonTile extends StatelessWidget {
const LessonTile(this.lesson, {Key? key, this.onTap, this.swapDesc = false}) : super(key: key);
final Lesson lesson;
final bool swapDesc;
final void Function()? onTap;
@override
Widget build(BuildContext context) {
List<Widget> subtiles = [];
Color accent = Theme.of(context).colorScheme.secondary;
bool fill = false;
bool fillLeading = false;
String lessonIndexTrailing = "";
// Only put a trailing . if its a digit
if (RegExp(r'\d').hasMatch(lesson.lessonIndex)) lessonIndexTrailing = ".";
var now = DateTime.now();
if (lesson.start.isBefore(now) && lesson.end.isAfter(now) && lesson.status?.name != "Elmaradt") {
fillLeading = true;
}
if (lesson.substituteTeacher != "") {
fill = true;
accent = AppColors.of(context).yellow;
}
if (lesson.status?.name == "Elmaradt") {
fill = true;
accent = AppColors.of(context).red;
}
if (lesson.isEmpty) {
accent = AppColors.of(context).text.withOpacity(0.6);
}
if (!lesson.studentPresence) {
subtiles.add(LessonSubtile(
type: LessonSubtileType.absence,
title: "absence".i18n,
));
}
if (lesson.homeworkId != "") {
Homework homework = Provider.of<HomeworkProvider>(context, listen: false)
.homework
.firstWhere((h) => h.id == lesson.homeworkId, orElse: () => Homework.fromJson({}));
if (homework.id != "") {
subtiles.add(LessonSubtile(
type: LessonSubtileType.homework,
title: homework.content,
onPressed: () => HomeworkView.show(homework, context: context),
));
}
}
if (lesson.exam != "") {
Exam exam = Provider.of<ExamProvider>(context, listen: false).exams.firstWhere((t) => t.id == lesson.exam, orElse: () => Exam.fromJson({}));
if (exam.id != "") {
subtiles.add(LessonSubtile(
type: LessonSubtileType.exam,
title: exam.description != "" ? exam.description : exam.mode?.description ?? "exam".i18n,
onPressed: () => ExamView.show(exam, context: context),
));
}
}
String description = '';
String room = '';
final cleanDesc = lesson.description.specialChars().toLowerCase().replaceAll(lesson.subject.name.specialChars().toLowerCase(), '');
if (!swapDesc) {
if (cleanDesc != "") {
description = lesson.description;
}
// Changed lesson Description
if (lesson.isChanged) {
if (lesson.status?.name == "Elmaradt") {
description = 'cancelled'.i18n;
} else if (lesson.substituteTeacher != "") {
description = 'substitution'.i18n;
}
}
room = lesson.room.replaceAll("_", " ");
} else {
description = lesson.room.replaceAll("_", " ");
}
return Padding(
padding: const EdgeInsets.only(bottom: 2.0),
child: Material(
color: fill ? accent.withOpacity(.25) : Colors.transparent,
borderRadius: BorderRadius.circular(12.0),
child: Visibility(
visible: lesson.subject.id != '' || lesson.isEmpty,
replacement: Padding(
padding: const EdgeInsets.only(top: 6.0),
child: PanelTitle(title: Text(lesson.name)),
),
child: Padding(
padding: EdgeInsets.only(bottom: subtiles.isEmpty ? 0.0 : 12.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
minVerticalPadding: 12.0,
dense: true,
onTap: onTap,
// onLongPress: kDebugMode ? () => log(jsonEncode(lesson.json)) : null,
visualDensity: VisualDensity.compact,
contentPadding: const EdgeInsets.symmetric(horizontal: 4.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
title: Text(
!lesson.isEmpty ? lesson.subject.renamedTo ?? lesson.subject.name.capital() : "empty".i18n,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 15.5,
color: AppColors.of(context).text.withOpacity(!lesson.isEmpty ? 1.0 : 0.5),
fontStyle: lesson.subject.isRenamed ? FontStyle.italic : null),
),
subtitle: description != ""
? Text(
description,
style: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14.0,
),
maxLines: 1,
softWrap: false,
overflow: TextOverflow.ellipsis,
)
: null,
minLeadingWidth: 34.0,
leading: AspectRatio(
aspectRatio: 1,
child: Center(
child: Stack(
children: [
Text(
lesson.lessonIndex + lessonIndexTrailing,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 30.0,
fontWeight: FontWeight.w600,
color: accent,
),
),
// Current lesson indicator
Transform.translate(
offset: const Offset(-12.0, -2.0),
child: Container(
decoration: BoxDecoration(
color: fillLeading ? Theme.of(context).colorScheme.secondary.withOpacity(.3) : const Color(0x00000000),
borderRadius: BorderRadius.circular(12.0),
boxShadow: [
if (fillLeading)
BoxShadow(
color: Theme.of(context).colorScheme.secondary.withOpacity(.25),
blurRadius: 6.0,
)
],
),
margin: const EdgeInsets.symmetric(vertical: 4.0),
width: 4.0,
height: double.infinity,
),
)
],
),
),
),
trailing: !lesson.isEmpty
? Row(
mainAxisSize: MainAxisSize.min,
children: [
if (!swapDesc)
SizedBox(
width: 52.0,
child: Padding(
padding: const EdgeInsets.only(right: 6.0),
child: Text(
room,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
maxLines: 2,
style: TextStyle(
fontWeight: FontWeight.w500,
color: AppColors.of(context).text.withOpacity(.75),
),
),
),
),
Stack(
alignment: Alignment.center,
children: [
// Fix alignment hack
const Opacity(opacity: 0, child: Text("EE:EE")),
Text(
"${DateFormat("H:mm").format(lesson.start)}\n${DateFormat("H:mm").format(lesson.end)}",
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.w500,
color: AppColors.of(context).text.withOpacity(.9),
),
),
],
),
],
)
: null,
),
// Homework & Exams
...subtiles,
],
),
),
),
),
);
}
}
enum LessonSubtileType { homework, exam, absence }
class LessonSubtile extends StatelessWidget {
const LessonSubtile({Key? key, this.onPressed, required this.title, required this.type}) : super(key: key);
final Function()? onPressed;
final String title;
final LessonSubtileType type;
@override
Widget build(BuildContext context) {
IconData icon;
Color iconColor = AppColors.of(context).text;
switch (type) {
case LessonSubtileType.absence:
icon = FeatherIcons.slash;
iconColor = AppColors.of(context).red;
break;
case LessonSubtileType.exam:
icon = FeatherIcons.file;
break;
case LessonSubtileType.homework:
icon = FeatherIcons.home;
break;
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(8.0),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
children: [
Center(
child: SizedBox(
width: 30.0,
child: Icon(icon, color: iconColor.withOpacity(.75), size: 20.0),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 20.0),
child: Text(
title.escapeHtml(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.w500, color: AppColors.of(context).text.withOpacity(.65)),
),
),
),
],
),
),
),
);
}
}
import 'package:filcnaplo_kreta_api/providers/exam_provider.dart';
import 'package:filcnaplo_kreta_api/providers/homework_provider.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo_kreta_api/models/exam.dart';
import 'package:filcnaplo_kreta_api/models/homework.dart';
import 'package:filcnaplo_kreta_api/models/lesson.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo_mobile_ui/common/panel/panel.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/exam/exam_view.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/homework/homework_view.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'lesson_tile.i18n.dart';
class LessonTile extends StatelessWidget {
const LessonTile(this.lesson, {Key? key, this.onTap, this.swapDesc = false}) : super(key: key);
final Lesson lesson;
final bool swapDesc;
final void Function()? onTap;
@override
Widget build(BuildContext context) {
List<Widget> subtiles = [];
Color accent = Theme.of(context).colorScheme.secondary;
bool fill = false;
bool fillLeading = false;
String lessonIndexTrailing = "";
// Only put a trailing . if its a digit
if (RegExp(r'\d').hasMatch(lesson.lessonIndex)) lessonIndexTrailing = ".";
var now = DateTime.now();
if (lesson.start.isBefore(now) && lesson.end.isAfter(now) && lesson.status?.name != "Elmaradt") {
fillLeading = true;
}
if (lesson.substituteTeacher != "") {
fill = true;
accent = AppColors.of(context).yellow;
}
if (lesson.status?.name == "Elmaradt") {
fill = true;
accent = AppColors.of(context).red;
}
if (lesson.isEmpty) {
accent = AppColors.of(context).text.withOpacity(0.6);
}
if (!lesson.studentPresence) {
subtiles.add(LessonSubtile(
type: LessonSubtileType.absence,
title: "absence".i18n,
));
}
if (lesson.homeworkId != "") {
Homework homework = Provider.of<HomeworkProvider>(context, listen: false)
.homework
.firstWhere((h) => h.id == lesson.homeworkId, orElse: () => Homework.fromJson({}));
if (homework.id != "") {
subtiles.add(LessonSubtile(
type: LessonSubtileType.homework,
title: homework.content,
onPressed: () => HomeworkView.show(homework, context: context),
));
}
}
if (lesson.exam != "") {
Exam exam = Provider.of<ExamProvider>(context, listen: false).exams.firstWhere((t) => t.id == lesson.exam, orElse: () => Exam.fromJson({}));
if (exam.id != "") {
subtiles.add(LessonSubtile(
type: LessonSubtileType.exam,
title: exam.description != "" ? exam.description : exam.mode?.description ?? "exam".i18n,
onPressed: () => ExamView.show(exam, context: context),
));
}
}
String description = '';
String room = '';
final cleanDesc = lesson.description.specialChars().toLowerCase().replaceAll(lesson.subject.name.specialChars().toLowerCase(), '');
if (!swapDesc) {
if (cleanDesc != "") {
description = lesson.description;
}
// Changed lesson Description
if (lesson.isChanged) {
if (lesson.status?.name == "Elmaradt") {
description = 'cancelled'.i18n;
} else if (lesson.substituteTeacher != "") {
description = 'substitution'.i18n;
}
}
room = lesson.room.replaceAll("_", " ");
} else {
description = lesson.room.replaceAll("_", " ");
}
return Padding(
padding: const EdgeInsets.only(bottom: 2.0),
child: Material(
color: fill ? accent.withOpacity(.25) : Colors.transparent,
borderRadius: BorderRadius.circular(12.0),
child: Visibility(
visible: lesson.subject.id != '' || lesson.isEmpty,
replacement: Padding(
padding: const EdgeInsets.only(top: 6.0),
child: PanelTitle(title: Text(lesson.name)),
),
child: Padding(
padding: EdgeInsets.only(bottom: subtiles.isEmpty ? 0.0 : 12.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
minVerticalPadding: 12.0,
dense: true,
onTap: onTap,
// onLongPress: kDebugMode ? () => log(jsonEncode(lesson.json)) : null,
visualDensity: VisualDensity.compact,
contentPadding: const EdgeInsets.symmetric(horizontal: 4.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
title: Text(
!lesson.isEmpty ? lesson.subject.renamedTo ?? lesson.subject.name.capital() : "empty".i18n,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 15.5,
color: AppColors.of(context).text.withOpacity(!lesson.isEmpty ? 1.0 : 0.5),
fontStyle: lesson.subject.isRenamed ? FontStyle.italic : null),
),
subtitle: description != ""
? Text(
description,
style: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14.0,
),
maxLines: 1,
softWrap: false,
overflow: TextOverflow.ellipsis,
)
: null,
minLeadingWidth: 34.0,
leading: AspectRatio(
aspectRatio: 1,
child: Center(
child: Stack(
children: [
Text(
lesson.lessonIndex + lessonIndexTrailing,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 30.0,
fontWeight: FontWeight.w600,
color: accent,
),
),
// Current lesson indicator
Transform.translate(
offset: const Offset(-12.0, -2.0),
child: Container(
decoration: BoxDecoration(
color: fillLeading ? Theme.of(context).colorScheme.secondary.withOpacity(.3) : const Color(0x00000000),
borderRadius: BorderRadius.circular(12.0),
boxShadow: [
if (fillLeading)
BoxShadow(
color: Theme.of(context).colorScheme.secondary.withOpacity(.25),
blurRadius: 6.0,
)
],
),
margin: const EdgeInsets.symmetric(vertical: 4.0),
width: 4.0,
height: double.infinity,
),
)
],
),
),
),
trailing: !lesson.isEmpty
? Row(
mainAxisSize: MainAxisSize.min,
children: [
if (!swapDesc)
SizedBox(
width: 52.0,
child: Padding(
padding: const EdgeInsets.only(right: 6.0),
child: Text(
room,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
maxLines: 2,
style: TextStyle(
fontWeight: FontWeight.w500,
color: AppColors.of(context).text.withOpacity(.75),
),
),
),
),
Stack(
alignment: Alignment.center,
children: [
// Fix alignment hack
const Opacity(opacity: 0, child: Text("EE:EE")),
Text(
"${DateFormat("H:mm").format(lesson.start)}\n${DateFormat("H:mm").format(lesson.end)}",
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.w500,
color: AppColors.of(context).text.withOpacity(.9),
),
),
],
),
],
)
: null,
),
// Homework & Exams
...subtiles,
],
),
),
),
),
);
}
}
enum LessonSubtileType { homework, exam, absence }
class LessonSubtile extends StatelessWidget {
const LessonSubtile({Key? key, this.onPressed, required this.title, required this.type}) : super(key: key);
final Function()? onPressed;
final String title;
final LessonSubtileType type;
@override
Widget build(BuildContext context) {
IconData icon;
Color iconColor = AppColors.of(context).text;
switch (type) {
case LessonSubtileType.absence:
icon = FeatherIcons.slash;
iconColor = AppColors.of(context).red;
break;
case LessonSubtileType.exam:
icon = FeatherIcons.file;
break;
case LessonSubtileType.homework:
icon = FeatherIcons.home;
break;
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(8.0),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
children: [
Center(
child: SizedBox(
width: 30.0,
child: Icon(icon, color: iconColor.withOpacity(.75), size: 20.0),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 20.0),
child: Text(
title.escapeHtml(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.w500, color: AppColors.of(context).text.withOpacity(.65)),
),
),
),
],
),
),
),
);
}
}

View File

@@ -1,33 +1,33 @@
import 'package:i18n_extension/i18n_extension.dart';
extension Localization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"empty": "Free period",
"cancelled": "Cancelled",
"substitution": "Substituted",
"absence": "You were absent on this lesson",
"exam": "Exam"
},
"hu_hu": {
"empty": "Lyukasóra",
"cancelled": "Elmarad",
"substitution": "Helyettesítés",
"absence": "Hiányoztál ezen az órán",
"exam": "Dolgozat"
},
"de_de": {
"empty": "Springstunde",
"cancelled": "Abgesagte",
"substitution": "Vertretene",
"absence": "Sie waren in dieser Lektion nicht anwesend",
"exam": "Prüfung"
}
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}
import 'package:i18n_extension/i18n_extension.dart';
extension Localization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"empty": "Free period",
"cancelled": "Cancelled",
"substitution": "Substituted",
"absence": "You were absent on this lesson",
"exam": "Exam"
},
"hu_hu": {
"empty": "Lyukasóra",
"cancelled": "Elmarad",
"substitution": "Helyettesítés",
"absence": "Hiányoztál ezen az órán",
"exam": "Dolgozat"
},
"de_de": {
"empty": "Springstunde",
"cancelled": "Abgesagte",
"substitution": "Vertretene",
"absence": "Sie waren in dieser Lektion nicht anwesend",
"exam": "Prüfung"
}
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@@ -1,121 +1,121 @@
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo/utils/color.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo_kreta_api/models/message.dart';
import 'package:filcnaplo_mobile_ui/common/profile_image/profile_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:provider/provider.dart';
class MessageTile extends StatelessWidget {
const MessageTile(
this.message, {
Key? key,
this.messages,
this.padding,
this.onTap,
this.censored = false,
}) : super(key: key);
final Message message;
final List<Message>? messages;
final EdgeInsetsGeometry? padding;
final Function()? onTap;
final bool censored;
@override
Widget build(BuildContext context) {
return Material(
type: MaterialType.transparency,
child: Padding(
padding: padding ?? const EdgeInsets.symmetric(horizontal: 8.0),
child: ListTile(
onTap: onTap,
visualDensity: VisualDensity.compact,
contentPadding: const EdgeInsets.only(left: 8.0, right: 4.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14.0)),
leading: !Provider.of<SettingsProvider>(context, listen: false).presentationMode
? ProfileImage(
name: message.author,
radius: 22.0,
backgroundColor: ColorUtils.stringToColor(message.author),
censored: censored,
)
: ProfileImage(
name: "Béla",
radius: 22.0,
backgroundColor: Theme.of(context).colorScheme.secondary,
censored: censored,
),
title: censored
? Wrap(
children: [
Container(
width: 105,
height: 15,
decoration: BoxDecoration(
color: AppColors.of(context).text.withOpacity(.85),
borderRadius: BorderRadius.circular(8.0),
),
),
],
)
: Row(
children: [
Expanded(
child: Text(
!Provider.of<SettingsProvider>(context, listen: false).presentationMode ? message.author : "Béla",
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 15.5),
),
),
if (message.attachments.isNotEmpty) const Icon(FeatherIcons.paperclip, size: 16.0)
],
),
subtitle: censored
? Wrap(
children: [
Container(
width: 150,
height: 10,
decoration: BoxDecoration(
color: AppColors.of(context).text.withOpacity(.45),
borderRadius: BorderRadius.circular(8.0),
),
),
],
)
: Text(
message.subject,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 14.0),
),
trailing: censored
? Wrap(
children: [
Container(
width: 35,
height: 15,
decoration: BoxDecoration(
color: AppColors.of(context).text.withOpacity(.45),
borderRadius: BorderRadius.circular(8.0),
),
),
],
)
: Text(
message.date.format(context),
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14.0,
color: AppColors.of(context).text.withOpacity(.75),
),
),
),
),
);
}
}
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo/utils/color.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo_kreta_api/models/message.dart';
import 'package:filcnaplo_mobile_ui/common/profile_image/profile_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:provider/provider.dart';
class MessageTile extends StatelessWidget {
const MessageTile(
this.message, {
Key? key,
this.messages,
this.padding,
this.onTap,
this.censored = false,
}) : super(key: key);
final Message message;
final List<Message>? messages;
final EdgeInsetsGeometry? padding;
final Function()? onTap;
final bool censored;
@override
Widget build(BuildContext context) {
return Material(
type: MaterialType.transparency,
child: Padding(
padding: padding ?? const EdgeInsets.symmetric(horizontal: 8.0),
child: ListTile(
onTap: onTap,
visualDensity: VisualDensity.compact,
contentPadding: const EdgeInsets.only(left: 8.0, right: 4.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14.0)),
leading: !Provider.of<SettingsProvider>(context, listen: false).presentationMode
? ProfileImage(
name: message.author,
radius: 22.0,
backgroundColor: ColorUtils.stringToColor(message.author),
censored: censored,
)
: ProfileImage(
name: "Béla",
radius: 22.0,
backgroundColor: Theme.of(context).colorScheme.secondary,
censored: censored,
),
title: censored
? Wrap(
children: [
Container(
width: 105,
height: 15,
decoration: BoxDecoration(
color: AppColors.of(context).text.withOpacity(.85),
borderRadius: BorderRadius.circular(8.0),
),
),
],
)
: Row(
children: [
Expanded(
child: Text(
!Provider.of<SettingsProvider>(context, listen: false).presentationMode ? message.author : "Béla",
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 15.5),
),
),
if (message.attachments.isNotEmpty) const Icon(FeatherIcons.paperclip, size: 16.0)
],
),
subtitle: censored
? Wrap(
children: [
Container(
width: 150,
height: 10,
decoration: BoxDecoration(
color: AppColors.of(context).text.withOpacity(.45),
borderRadius: BorderRadius.circular(8.0),
),
),
],
)
: Text(
message.subject,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 14.0),
),
trailing: censored
? Wrap(
children: [
Container(
width: 35,
height: 15,
decoration: BoxDecoration(
color: AppColors.of(context).text.withOpacity(.45),
borderRadius: BorderRadius.circular(8.0),
),
),
],
)
: Text(
message.date.format(context),
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14.0,
color: AppColors.of(context).text.withOpacity(.75),
),
),
),
),
);
}
}

View File

@@ -1,14 +1,14 @@
import 'package:flutter/material.dart';
class ColorUtils {
static Color stringToColor(String str) {
int hash = 0;
for (var i = 0; i < str.length; i++) {
hash = str.codeUnitAt(i) + ((hash << 5) - hash);
}
return HSLColor.fromAHSL(1, hash % 360, .8, .75).toColor();
}
static Color foregroundColor(Color color) => color.computeLuminance() >= .5 ? Colors.black : Colors.white;
}
import 'package:flutter/material.dart';
class ColorUtils {
static Color stringToColor(String str) {
int hash = 0;
for (var i = 0; i < str.length; i++) {
hash = str.codeUnitAt(i) + ((hash << 5) - hash);
}
return HSLColor.fromAHSL(1, hash % 360, .8, .75).toColor();
}
static Color foregroundColor(Color color) => color.computeLuminance() >= .5 ? Colors.black : Colors.white;
}

View File

@@ -1,70 +1,70 @@
import 'dart:math';
import 'package:filcnaplo_kreta_api/models/week.dart';
import 'package:flutter/widgets.dart';
import 'package:intl/intl.dart';
import 'package:i18n_extension/i18n_widget.dart';
import 'package:html/parser.dart';
import 'format.i18n.dart';
extension StringFormatUtils on String {
String specialChars() => replaceAll("é", "e")
.replaceAll("á", "a")
.replaceAll("ó", "o")
.replaceAll("ő", "o")
.replaceAll("ö", "o")
.replaceAll("ú", "u")
.replaceAll("ű", "u")
.replaceAll("ü", "u")
.replaceAll("í", "i");
String capital() => isNotEmpty ? this[0].toUpperCase() + substring(1) : "";
String capitalize() => split(" ").map((w) => w.capital()).join(" ");
String escapeHtml() {
String htmlString = this;
htmlString = htmlString.replaceAll("\r", "");
htmlString = htmlString.replaceAll(RegExp(r'<br ?/?>'), "\n");
htmlString = htmlString.replaceAll(RegExp(r'<p ?>'), "");
htmlString = htmlString.replaceAll(RegExp(r'</p ?>'), "\n");
var document = parse(htmlString);
return document.body?.text.trim() ?? htmlString;
}
String limit(int max) {
if (length <= max) return this;
return '${substring(0, min(length, 14))}';
}
}
extension DateFormatUtils on DateTime {
String format(BuildContext context, {bool timeOnly = false, bool forceToday = false, bool weekday = false}) {
// Time only
if (timeOnly) return DateFormat("HH:mm").format(this);
DateTime now = DateTime.now();
if (now.year == year && now.month == month && now.day == day) {
if (hour == 0 && minute == 0 && second == 0 || forceToday) return "Today".i18n;
return DateFormat("HH:mm").format(this);
}
if (now.year == year && now.month == month && now.subtract(const Duration(days: 1)).day == day) return "Yesterday".i18n;
if (now.year == year && now.month == month && now.add(const Duration(days: 1)).day == day) return "Tomorrow".i18n;
String formatString;
// If date is current week, show only weekday
if (Week.current().start.isBefore(this) && Week.current().end.isAfter(this)) {
formatString = "EEEE";
} else {
if (year == now.year) {
formatString = "MMM dd.";
} else {
formatString = "yy/MM/dd";
} // ex. 21/01/01
if (weekday) formatString += " (EEEE)"; // ex. (monday)
}
return DateFormat(formatString, I18n.of(context).locale.toString()).format(this).capital();
}
}
import 'dart:math';
import 'package:filcnaplo_kreta_api/models/week.dart';
import 'package:flutter/widgets.dart';
import 'package:intl/intl.dart';
import 'package:i18n_extension/i18n_widget.dart';
import 'package:html/parser.dart';
import 'format.i18n.dart';
extension StringFormatUtils on String {
String specialChars() => replaceAll("é", "e")
.replaceAll("á", "a")
.replaceAll("ó", "o")
.replaceAll("ő", "o")
.replaceAll("ö", "o")
.replaceAll("ú", "u")
.replaceAll("ű", "u")
.replaceAll("ü", "u")
.replaceAll("í", "i");
String capital() => isNotEmpty ? this[0].toUpperCase() + substring(1) : "";
String capitalize() => split(" ").map((w) => w.capital()).join(" ");
String escapeHtml() {
String htmlString = this;
htmlString = htmlString.replaceAll("\r", "");
htmlString = htmlString.replaceAll(RegExp(r'<br ?/?>'), "\n");
htmlString = htmlString.replaceAll(RegExp(r'<p ?>'), "");
htmlString = htmlString.replaceAll(RegExp(r'</p ?>'), "\n");
var document = parse(htmlString);
return document.body?.text.trim() ?? htmlString;
}
String limit(int max) {
if (length <= max) return this;
return '${substring(0, min(length, 14))}';
}
}
extension DateFormatUtils on DateTime {
String format(BuildContext context, {bool timeOnly = false, bool forceToday = false, bool weekday = false}) {
// Time only
if (timeOnly) return DateFormat("HH:mm").format(this);
DateTime now = DateTime.now();
if (now.year == year && now.month == month && now.day == day) {
if (hour == 0 && minute == 0 && second == 0 || forceToday) return "Today".i18n;
return DateFormat("HH:mm").format(this);
}
if (now.year == year && now.month == month && now.subtract(const Duration(days: 1)).day == day) return "Yesterday".i18n;
if (now.year == year && now.month == month && now.add(const Duration(days: 1)).day == day) return "Tomorrow".i18n;
String formatString;
// If date is current week, show only weekday
if (Week.current().start.isBefore(this) && Week.current().end.isAfter(this)) {
formatString = "EEEE";
} else {
if (year == now.year) {
formatString = "MMM dd.";
} else {
formatString = "yy/MM/dd";
} // ex. 21/01/01
if (weekday) formatString += " (EEEE)"; // ex. (monday)
}
return DateFormat(formatString, I18n.of(context).locale.toString()).format(this).capital();
}
}

View File

@@ -1,27 +1,27 @@
import 'package:i18n_extension/i18n_extension.dart';
extension Localization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"Today": "Today",
"Yesterday": "Yesterday",
"Tomorrow": "Tomorrow",
},
"hu_hu": {
"Today": "Ma",
"Yesterday": "Tegnap",
"Tomorrow": "Holnap",
},
"de_de": {
"Today": "Heute",
"Yesterday": "Gestern",
"Tomorrow": "Morgen",
}
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}
import 'package:i18n_extension/i18n_extension.dart';
extension Localization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"Today": "Today",
"Yesterday": "Yesterday",
"Tomorrow": "Tomorrow",
},
"hu_hu": {
"Today": "Ma",
"Yesterday": "Tegnap",
"Tomorrow": "Holnap",
},
"de_de": {
"Today": "Heute",
"Yesterday": "Gestern",
"Tomorrow": "Morgen",
}
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@@ -1,42 +1,42 @@
import 'dart:convert';
import 'package:filcnaplo/models/user.dart';
class JwtUtils {
static Map? decodeJwt(String jwt) {
var parts = jwt.split(".");
if (parts.length != 3) return null;
if (parts[1].length % 4 == 2) {
parts[1] += "==";
} else if (parts[1].length % 4 == 3) {
parts[1] += "=";
}
try {
var payload = utf8.decode(base64Url.decode(parts[1]));
return jsonDecode(payload);
} catch (error) {
// ignore: avoid_print
print("ERROR: JwtUtils.decodeJwt: $error");
}
return null;
}
static String? getNameFromJWT(String jwt) {
var jwtData = decodeJwt(jwt);
return jwtData?["name"];
}
static Role? getRoleFromJWT(String jwt) {
var jwtData = decodeJwt(jwt);
switch (jwtData?["role"]) {
case "Tanulo":
return Role.student;
case "Gondviselo":
return Role.parent;
}
return null;
}
}
import 'dart:convert';
import 'package:filcnaplo/models/user.dart';
class JwtUtils {
static Map? decodeJwt(String jwt) {
var parts = jwt.split(".");
if (parts.length != 3) return null;
if (parts[1].length % 4 == 2) {
parts[1] += "==";
} else if (parts[1].length % 4 == 3) {
parts[1] += "=";
}
try {
var payload = utf8.decode(base64Url.decode(parts[1]));
return jsonDecode(payload);
} catch (error) {
// ignore: avoid_print
print("ERROR: JwtUtils.decodeJwt: $error");
}
return null;
}
static String? getNameFromJWT(String jwt) {
var jwtData = decodeJwt(jwt);
return jwtData?["name"];
}
static Role? getRoleFromJWT(String jwt) {
var jwtData = decodeJwt(jwt);
switch (jwtData?["role"]) {
case "Tanulo":
return Role.student;
case "Gondviselo":
return Role.parent;
}
return null;
}
}

View File

@@ -1,6 +1,6 @@
import 'dart:io';
class PlatformUtils {
static bool get isDesktop => Platform.isWindows || Platform.isMacOS || Platform.isLinux;
static bool get isMobile => !isDesktop;
}
import 'dart:io';
class PlatformUtils {
static bool get isDesktop => Platform.isWindows || Platform.isMacOS || Platform.isLinux;
static bool get isMobile => !isDesktop;
}

View File

@@ -1,38 +1,38 @@
import 'dart:developer';
import 'package:filcnaplo_kreta_api/models/absence.dart';
import 'package:filcnaplo_kreta_api/models/lesson.dart';
import 'package:filcnaplo_kreta_api/models/week.dart';
import 'package:filcnaplo_kreta_api/providers/timetable_provider.dart';
import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
class ReverseSearch {
static Future<Lesson?> getLessonByAbsence(Absence absence, BuildContext context) async {
final timetableProvider = Provider.of<TimetableProvider>(context, listen: false);
List<Lesson> lessons = [];
final week = Week.fromDate(absence.date);
try {
await timetableProvider.fetch(week: week);
} catch (e) {
log("[ERROR] getLessonByAbsence: $e");
}
lessons = timetableProvider.getWeek(week) ?? [];
// Find absence lesson in timetable
Lesson lesson = lessons.firstWhere(
(l) => _sameDate(l.date, absence.date) && l.subject.id == absence.subject.id && l.lessonIndex == absence.lessonIndex.toString(),
orElse: () => Lesson.fromJson({'isEmpty': true}),
);
if (lesson.isEmpty) {
return null;
} else {
return lesson;
}
}
// difference.inDays is not reliable
static bool _sameDate(DateTime a, DateTime b) => (a.year == b.year && a.month == b.month && a.day == b.day);
}
import 'dart:developer';
import 'package:filcnaplo_kreta_api/models/absence.dart';
import 'package:filcnaplo_kreta_api/models/lesson.dart';
import 'package:filcnaplo_kreta_api/models/week.dart';
import 'package:filcnaplo_kreta_api/providers/timetable_provider.dart';
import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
class ReverseSearch {
static Future<Lesson?> getLessonByAbsence(Absence absence, BuildContext context) async {
final timetableProvider = Provider.of<TimetableProvider>(context, listen: false);
List<Lesson> lessons = [];
final week = Week.fromDate(absence.date);
try {
await timetableProvider.fetch(week: week);
} catch (e) {
log("[ERROR] getLessonByAbsence: $e");
}
lessons = timetableProvider.getWeek(week) ?? [];
// Find absence lesson in timetable
Lesson lesson = lessons.firstWhere(
(l) => _sameDate(l.date, absence.date) && l.subject.id == absence.subject.id && l.lessonIndex == absence.lessonIndex.toString(),
orElse: () => Lesson.fromJson({'isEmpty': true}),
);
if (lesson.isEmpty) {
return null;
} else {
return lesson;
}
}
// difference.inDays is not reliable
static bool _sameDate(DateTime a, DateTime b) => (a.year == b.year && a.month == b.month && a.day == b.day);
}