Merge branch 'dev' of github.com:refilc/naplo into dev

This commit is contained in:
Kima 2023-08-27 21:38:35 +02:00
commit 856b48675b
54 changed files with 1800 additions and 591 deletions

View File

@ -12,7 +12,7 @@
### Clone the project ### Clone the project
```sh ```sh
git clone --recursive https://github.com/refilc/naplo git clone https://github.com/refilc/naplo
cd naplo cd naplo
``` ```

View File

@ -1,7 +1,4 @@
// ignore_for_file: use_build_context_synchronously // ignore_for_file: use_build_context_synchronously
import 'dart:math';
import 'package:filcnaplo/api/client.dart'; import 'package:filcnaplo/api/client.dart';
import 'package:filcnaplo/models/news.dart'; import 'package:filcnaplo/models/news.dart';
import 'package:filcnaplo/models/settings.dart'; import 'package:filcnaplo/models/settings.dart';
@ -11,7 +8,7 @@ import 'package:provider/provider.dart';
class NewsProvider extends ChangeNotifier { class NewsProvider extends ChangeNotifier {
// Private // Private
late List<News> _news; late List<News> _news;
late int _state; //late int _state;
late int _fresh; late int _fresh;
bool show = false; bool show = false;
late BuildContext _context; late BuildContext _context;
@ -30,56 +27,83 @@ class NewsProvider extends ChangeNotifier {
Future<void> restore() async { Future<void> restore() async {
// Load news state from the database // Load news state from the database
var state_ = Provider.of<SettingsProvider>(_context, listen: false).newsState; var seen_ = Provider.of<SettingsProvider>(_context, listen: false).seenNews;
if (state_ == -1) { if (seen_.isEmpty) {
var news_ = await FilcAPI.getNews(); var news_ = await FilcAPI.getNews();
if (news_ != null) { if (news_ != null) {
state_ = news_.length;
_news = news_; _news = news_;
show = true;
} }
} }
_state = state_; //_state = seen_;
Provider.of<SettingsProvider>(_context, listen: false).update(newsState: _state); // Provider.of<SettingsProvider>(_context, listen: false)
// .update(seenNewsId: news_.id);
} }
Future<void> fetch() async { Future<void> fetch() async {
var news_ = await FilcAPI.getNews(); var news_ = await FilcAPI.getNews();
if (news_ == null) return; if (news_ == null) return;
show = false;
_news = news_; _news = news_;
_fresh = news_.length - _state;
if (_fresh < 0) { for (var news in news_) {
_state = news_.length; if (news.expireDate.isAfter(DateTime.now()) &&
Provider.of<SettingsProvider>(_context, listen: false).update(newsState: _state); Provider.of<SettingsProvider>(_context, listen: false)
} .seenNews
.contains(news.id) ==
_fresh = max(_fresh, 0); false) {
if (_fresh > 0) {
show = true; show = true;
Provider.of<SettingsProvider>(_context, listen: false)
.update(seenNewsId: news.id);
notifyListeners(); notifyListeners();
} }
} }
// print(news_.length);
// print(_state);
// _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();
// }
// print(_fresh);
// print(_state);
// print(show);
}
void lock() => show = false; void lock() => show = false;
void release() { void release() {
if (_fresh == 0) return; // if (_fresh == 0) return;
_fresh--; // _fresh--;
_state++; // //_state++;
Provider.of<SettingsProvider>(_context, listen: false).update(newsState: _state); // // Provider.of<SettingsProvider>(_context, listen: false)
// // .update(seenNewsId: _state);
if (_fresh > 0) { // if (_fresh > 0) {
show = true; // show = true;
} else { // } else {
show = false; // show = false;
} // }
notifyListeners(); // notifyListeners();
} }
} }

View File

@ -12,7 +12,8 @@ import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart';
const settingsDB = DatabaseStruct("settings", { const settingsDB = DatabaseStruct("settings", {
"language": String, "start_page": int, "rounding": int, "theme": int, "language": String, "start_page": int, "rounding": int, "theme": int,
"accent_color": int, "news": int, "news_state": int, "developer_mode": int, "accent_color": int, "news": int, "seen_news": String,
"developer_mode": int,
"update_channel": int, "config": String, "custom_accent_color": int, "update_channel": int, "config": String, "custom_accent_color": int,
"custom_background_color": int, "custom_highlight_color": int, // general "custom_background_color": int, "custom_highlight_color": int, // general
"grade_color1": int, "grade_color2": int, "grade_color3": int, "grade_color1": int, "grade_color2": int, "grade_color3": int,
@ -25,7 +26,8 @@ const settingsDB = DatabaseStruct("settings", {
"grade_opening_fun": int, "icon_pack": String, "premium_scopes": String, "grade_opening_fun": int, "icon_pack": String, "premium_scopes": String,
"premium_token": String, "premium_login": String, "premium_token": String, "premium_login": String,
"last_account_id": String, "renamed_subjects_enabled": int, "last_account_id": String, "renamed_subjects_enabled": int,
"renamed_subjects_italics": int, "renamed_subjects_italics": int, "renamed_teachers_enabled": int,
"renamed_teachers_italics": int,
}); });
// DON'T FORGET TO UPDATE DEFAULT VALUES IN `initDB` MIGRATION OR ELSE PARENTS WILL COMPLAIN ABOUT THEIR CHILDREN MISSING // DON'T FORGET TO UPDATE DEFAULT VALUES IN `initDB` MIGRATION OR ELSE PARENTS WILL COMPLAIN ABOUT THEIR CHILDREN MISSING
// YOU'VE BEEN WARNED!!! // YOU'VE BEEN WARNED!!!
@ -40,6 +42,8 @@ const userDataDB = DatabaseStruct("user_data", {
"events": String, "absences": String, "group_averages": String, "events": String, "absences": String, "group_averages": String,
// renamed subjects // non kreta data // renamed subjects // non kreta data
"renamed_subjects": String, "renamed_subjects": String,
// renamed teachers // non kreta data
"renamed_teachers": String,
// "subject_lesson_count": String, // non kreta data // "subject_lesson_count": String, // non kreta data
"last_seen_grade": int, "last_seen_grade": int,
}); });
@ -89,6 +93,8 @@ Future<Database> initDB(DatabaseProvider database) async {
"group_averages": "[]", "group_averages": "[]",
// renamed subjects // non kreta data // renamed subjects // non kreta data
"renamed_subjects": "{}", "renamed_subjects": "{}",
// renamed teachers // non kreta data
"renamed_teachers": "{}",
// "subject_lesson_count": "{}", // non kreta data // "subject_lesson_count": "{}", // non kreta data
"last_seen_grade": 0, "last_seen_grade": 0,
}); });

View File

@ -26,7 +26,8 @@ class DatabaseQuery {
Future<SettingsProvider> getSettings(DatabaseProvider database) async { Future<SettingsProvider> getSettings(DatabaseProvider database) async {
Map settingsMap = (await db.query("settings")).elementAt(0); Map settingsMap = (await db.query("settings")).elementAt(0);
SettingsProvider settings = SettingsProvider.fromMap(settingsMap, database: database); SettingsProvider settings =
SettingsProvider.fromMap(settingsMap, database: database);
return settings; return settings;
} }
@ -36,7 +37,10 @@ class DatabaseQuery {
for (var user in usersMap) { for (var user in usersMap) {
userProvider.addUser(User.fromMap(user)); userProvider.addUser(User.fromMap(user));
} }
if (userProvider.getUsers().map((e) => e.id).contains(settings.lastAccountId)) { if (userProvider
.getUsers()
.map((e) => e.id)
.contains(settings.lastAccountId)) {
userProvider.setUser(settings.lastAccountId); userProvider.setUser(settings.lastAccountId);
} else { } else {
if (usersMap.isNotEmpty) { if (usersMap.isNotEmpty) {
@ -54,100 +58,133 @@ class UserDatabaseQuery {
final Database db; final Database db;
Future<List<Grade>> getGrades({required String userId}) async { Future<List<Grade>> getGrades({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]); List<Map> userData =
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return []; if (userData.isEmpty) return [];
String? gradesJson = userData.elementAt(0)["grades"] as String?; String? gradesJson = userData.elementAt(0)["grades"] as String?;
if (gradesJson == null) return []; if (gradesJson == null) return [];
List<Grade> grades = (jsonDecode(gradesJson) as List).map((e) => Grade.fromJson(e)).toList(); List<Grade> grades =
(jsonDecode(gradesJson) as List).map((e) => Grade.fromJson(e)).toList();
return grades; return grades;
} }
Future<Map<Week, List<Lesson>>> getLessons({required String userId}) async { Future<Map<Week, List<Lesson>>> getLessons({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]); List<Map> userData =
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return {}; if (userData.isEmpty) return {};
String? lessonsJson = userData.elementAt(0)["timetable"] as String?; String? lessonsJson = userData.elementAt(0)["timetable"] as String?;
if (lessonsJson == null) return {}; if (lessonsJson == null) return {};
if (jsonDecode(lessonsJson) is List) return {}; if (jsonDecode(lessonsJson) is List) return {};
Map<Week, List<Lesson>> lessons = (jsonDecode(lessonsJson) as Map).cast<String, List>().map((key, value) { Map<Week, List<Lesson>> lessons =
return MapEntry(Week.fromId(int.parse(key)), value.cast<Map<String, Object?>>().map((e) => Lesson.fromJson(e)).toList()); (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(); }).cast();
return lessons; return lessons;
} }
Future<List<Exam>> getExams({required String userId}) async { Future<List<Exam>> getExams({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]); List<Map> userData =
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return []; if (userData.isEmpty) return [];
String? examsJson = userData.elementAt(0)["exams"] as String?; String? examsJson = userData.elementAt(0)["exams"] as String?;
if (examsJson == null) return []; if (examsJson == null) return [];
List<Exam> exams = (jsonDecode(examsJson) as List).map((e) => Exam.fromJson(e)).toList(); List<Exam> exams =
(jsonDecode(examsJson) as List).map((e) => Exam.fromJson(e)).toList();
return exams; return exams;
} }
Future<List<Homework>> getHomework({required String userId}) async { Future<List<Homework>> getHomework({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]); List<Map> userData =
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return []; if (userData.isEmpty) return [];
String? homeworkJson = userData.elementAt(0)["homework"] as String?; String? homeworkJson = userData.elementAt(0)["homework"] as String?;
if (homeworkJson == null) return []; if (homeworkJson == null) return [];
List<Homework> homework = (jsonDecode(homeworkJson) as List).map((e) => Homework.fromJson(e)).toList(); List<Homework> homework = (jsonDecode(homeworkJson) as List)
.map((e) => Homework.fromJson(e))
.toList();
return homework; return homework;
} }
Future<List<Message>> getMessages({required String userId}) async { Future<List<Message>> getMessages({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]); List<Map> userData =
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return []; if (userData.isEmpty) return [];
String? messagesJson = userData.elementAt(0)["messages"] as String?; String? messagesJson = userData.elementAt(0)["messages"] as String?;
if (messagesJson == null) return []; if (messagesJson == null) return [];
List<Message> messages = (jsonDecode(messagesJson) as List).map((e) => Message.fromJson(e)).toList(); List<Message> messages = (jsonDecode(messagesJson) as List)
.map((e) => Message.fromJson(e))
.toList();
return messages; return messages;
} }
Future<List<Note>> getNotes({required String userId}) async { Future<List<Note>> getNotes({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]); List<Map> userData =
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return []; if (userData.isEmpty) return [];
String? notesJson = userData.elementAt(0)["notes"] as String?; String? notesJson = userData.elementAt(0)["notes"] as String?;
if (notesJson == null) return []; if (notesJson == null) return [];
List<Note> notes = (jsonDecode(notesJson) as List).map((e) => Note.fromJson(e)).toList(); List<Note> notes =
(jsonDecode(notesJson) as List).map((e) => Note.fromJson(e)).toList();
return notes; return notes;
} }
Future<List<Event>> getEvents({required String userId}) async { Future<List<Event>> getEvents({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]); List<Map> userData =
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return []; if (userData.isEmpty) return [];
String? eventsJson = userData.elementAt(0)["events"] as String?; String? eventsJson = userData.elementAt(0)["events"] as String?;
if (eventsJson == null) return []; if (eventsJson == null) return [];
List<Event> events = (jsonDecode(eventsJson) as List).map((e) => Event.fromJson(e)).toList(); List<Event> events =
(jsonDecode(eventsJson) as List).map((e) => Event.fromJson(e)).toList();
return events; return events;
} }
Future<List<Absence>> getAbsences({required String userId}) async { Future<List<Absence>> getAbsences({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]); List<Map> userData =
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return []; if (userData.isEmpty) return [];
String? absencesJson = userData.elementAt(0)["absences"] as String?; String? absencesJson = userData.elementAt(0)["absences"] as String?;
if (absencesJson == null) return []; if (absencesJson == null) return [];
List<Absence> absences = (jsonDecode(absencesJson) as List).map((e) => Absence.fromJson(e)).toList(); List<Absence> absences = (jsonDecode(absencesJson) as List)
.map((e) => Absence.fromJson(e))
.toList();
return absences; return absences;
} }
Future<List<GroupAverage>> getGroupAverages({required String userId}) async { Future<List<GroupAverage>> getGroupAverages({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]); List<Map> userData =
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return []; if (userData.isEmpty) return [];
String? groupAveragesJson = userData.elementAt(0)["group_averages"] as String?; String? groupAveragesJson =
userData.elementAt(0)["group_averages"] as String?;
if (groupAveragesJson == null) return []; if (groupAveragesJson == null) return [];
List<GroupAverage> groupAverages = (jsonDecode(groupAveragesJson) as List).map((e) => GroupAverage.fromJson(e)).toList(); List<GroupAverage> groupAverages = (jsonDecode(groupAveragesJson) as List)
.map((e) => GroupAverage.fromJson(e))
.toList();
return groupAverages; return groupAverages;
} }
Future<SubjectLessonCount> getSubjectLessonCount({required String userId}) async { Future<SubjectLessonCount> getSubjectLessonCount(
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]); {required String userId}) async {
List<Map> userData =
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return SubjectLessonCount.fromMap({}); if (userData.isEmpty) return SubjectLessonCount.fromMap({});
String? lessonCountJson = userData.elementAt(0)["subject_lesson_count"] as String?; String? lessonCountJson =
userData.elementAt(0)["subject_lesson_count"] as String?;
if (lessonCountJson == null) return SubjectLessonCount.fromMap({}); if (lessonCountJson == null) return SubjectLessonCount.fromMap({});
SubjectLessonCount lessonCount = SubjectLessonCount.fromMap(jsonDecode(lessonCountJson) as Map); SubjectLessonCount lessonCount =
SubjectLessonCount.fromMap(jsonDecode(lessonCountJson) as Map);
return lessonCount; return lessonCount;
} }
Future<DateTime> lastSeenGrade({required String userId}) async { Future<DateTime> lastSeenGrade({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]); List<Map> userData =
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return DateTime(0); if (userData.isEmpty) return DateTime(0);
int? lastSeenDate = userData.elementAt(0)["last_seen_grade"] as int?; int? lastSeenDate = userData.elementAt(0)["last_seen_grade"] as int?;
if (lastSeenDate == null) return DateTime(0); if (lastSeenDate == null) return DateTime(0);
@ -156,10 +193,24 @@ class UserDatabaseQuery {
} }
Future<Map<String, String>> renamedSubjects({required String userId}) async { Future<Map<String, String>> renamedSubjects({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]); List<Map> userData =
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return {}; if (userData.isEmpty) return {};
String? renamedSubjectsJson = userData.elementAt(0)["renamed_subjects"] as String?; String? renamedSubjectsJson =
userData.elementAt(0)["renamed_subjects"] as String?;
if (renamedSubjectsJson == null) return {}; if (renamedSubjectsJson == null) return {};
return (jsonDecode(renamedSubjectsJson) as Map).map((key, value) => MapEntry(key.toString(), value.toString())); return (jsonDecode(renamedSubjectsJson) as Map)
.map((key, value) => MapEntry(key.toString(), value.toString()));
}
Future<Map<String, String>> renamedTeachers({required String userId}) async {
List<Map> userData =
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.isEmpty) return {};
String? renamedTeachersJson =
userData.elementAt(0)["renamed_teachers"] as String?;
if (renamedTeachersJson == null) return {};
return (jsonDecode(renamedTeachersJson) as Map)
.map((key, value) => MapEntry(key.toString(), value.toString()));
} }
} }

View File

@ -27,9 +27,11 @@ class DatabaseStore {
} }
Future<void> storeUser(User user) async { Future<void> storeUser(User user) async {
List userRes = await db.query("users", where: "id = ?", whereArgs: [user.id]); List userRes =
await db.query("users", where: "id = ?", whereArgs: [user.id]);
if (userRes.isNotEmpty) { if (userRes.isNotEmpty) {
await db.update("users", user.toMap(), where: "id = ?", whereArgs: [user.id]); await db
.update("users", user.toMap(), where: "id = ?", whereArgs: [user.id]);
} else { } else {
await db.insert("users", user.toMap()); await db.insert("users", user.toMap());
await db.insert("user_data", {"id": user.id}); await db.insert("user_data", {"id": user.id});
@ -49,64 +51,93 @@ class UserDatabaseStore {
Future<void> storeGrades(List<Grade> grades, {required String userId}) async { Future<void> storeGrades(List<Grade> grades, {required String userId}) async {
String gradesJson = jsonEncode(grades.map((e) => e.json).toList()); String gradesJson = jsonEncode(grades.map((e) => e.json).toList());
await db.update("user_data", {"grades": gradesJson}, where: "id = ?", whereArgs: [userId]); await db.update("user_data", {"grades": gradesJson},
where: "id = ?", whereArgs: [userId]);
} }
Future<void> storeLessons(Map<Week, List<Lesson>?> lessons, {required String userId}) async { Future<void> storeLessons(Map<Week, List<Lesson>?> lessons,
{required String userId}) async {
final map = lessons.map<String, List<Map<String, Object?>>>( 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()), (k, v) => MapEntry(k.id.toString(),
v!.where((e) => e.json != null).map((e) => e.json!).toList().cast()),
); );
String lessonsJson = jsonEncode(map); String lessonsJson = jsonEncode(map);
await db.update("user_data", {"timetable": lessonsJson}, where: "id = ?", whereArgs: [userId]); await db.update("user_data", {"timetable": lessonsJson},
where: "id = ?", whereArgs: [userId]);
} }
Future<void> storeExams(List<Exam> exams, {required String userId}) async { Future<void> storeExams(List<Exam> exams, {required String userId}) async {
String examsJson = jsonEncode(exams.map((e) => e.json).toList()); String examsJson = jsonEncode(exams.map((e) => e.json).toList());
await db.update("user_data", {"exams": examsJson}, where: "id = ?", whereArgs: [userId]); await db.update("user_data", {"exams": examsJson},
where: "id = ?", whereArgs: [userId]);
} }
Future<void> storeHomework(List<Homework> homework, {required String userId}) async { Future<void> storeHomework(List<Homework> homework,
{required String userId}) async {
String homeworkJson = jsonEncode(homework.map((e) => e.json).toList()); String homeworkJson = jsonEncode(homework.map((e) => e.json).toList());
await db.update("user_data", {"homework": homeworkJson}, where: "id = ?", whereArgs: [userId]); await db.update("user_data", {"homework": homeworkJson},
where: "id = ?", whereArgs: [userId]);
} }
Future<void> storeMessages(List<Message> messages, {required String userId}) async { Future<void> storeMessages(List<Message> messages,
{required String userId}) async {
String messagesJson = jsonEncode(messages.map((e) => e.json).toList()); String messagesJson = jsonEncode(messages.map((e) => e.json).toList());
await db.update("user_data", {"messages": messagesJson}, where: "id = ?", whereArgs: [userId]); await db.update("user_data", {"messages": messagesJson},
where: "id = ?", whereArgs: [userId]);
} }
Future<void> storeNotes(List<Note> notes, {required String userId}) async { Future<void> storeNotes(List<Note> notes, {required String userId}) async {
String notesJson = jsonEncode(notes.map((e) => e.json).toList()); String notesJson = jsonEncode(notes.map((e) => e.json).toList());
await db.update("user_data", {"notes": notesJson}, where: "id = ?", whereArgs: [userId]); await db.update("user_data", {"notes": notesJson},
where: "id = ?", whereArgs: [userId]);
} }
Future<void> storeEvents(List<Event> events, {required String userId}) async { Future<void> storeEvents(List<Event> events, {required String userId}) async {
String eventsJson = jsonEncode(events.map((e) => e.json).toList()); String eventsJson = jsonEncode(events.map((e) => e.json).toList());
await db.update("user_data", {"events": eventsJson}, where: "id = ?", whereArgs: [userId]); await db.update("user_data", {"events": eventsJson},
where: "id = ?", whereArgs: [userId]);
} }
Future<void> storeAbsences(List<Absence> absences, {required String userId}) async { Future<void> storeAbsences(List<Absence> absences,
{required String userId}) async {
String absencesJson = jsonEncode(absences.map((e) => e.json).toList()); String absencesJson = jsonEncode(absences.map((e) => e.json).toList());
await db.update("user_data", {"absences": absencesJson}, where: "id = ?", whereArgs: [userId]); await db.update("user_data", {"absences": absencesJson},
where: "id = ?", whereArgs: [userId]);
} }
Future<void> storeGroupAverages(List<GroupAverage> groupAverages, {required String userId}) async { Future<void> storeGroupAverages(List<GroupAverage> groupAverages,
String groupAveragesJson = jsonEncode(groupAverages.map((e) => e.json).toList()); {required String userId}) async {
await db.update("user_data", {"group_averages": groupAveragesJson}, where: "id = ?", whereArgs: [userId]); 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 { Future<void> storeSubjectLessonCount(SubjectLessonCount lessonCount,
{required String userId}) async {
String lessonCountJson = jsonEncode(lessonCount.toMap()); String lessonCountJson = jsonEncode(lessonCount.toMap());
await db.update("user_data", {"subject_lesson_count": lessonCountJson}, where: "id = ?", whereArgs: [userId]); await db.update("user_data", {"subject_lesson_count": lessonCountJson},
where: "id = ?", whereArgs: [userId]);
} }
Future<void> storeLastSeenGrade(DateTime date, {required String userId}) async { Future<void> storeLastSeenGrade(DateTime date,
{required String userId}) async {
int lastSeenDate = date.millisecondsSinceEpoch; int lastSeenDate = date.millisecondsSinceEpoch;
await db.update("user_data", {"last_seen_grade": lastSeenDate}, where: "id = ?", whereArgs: [userId]); await db.update("user_data", {"last_seen_grade": lastSeenDate},
where: "id = ?", whereArgs: [userId]);
} }
Future<void> storeRenamedSubjects(Map<String, String> subjects, {required String userId}) async { Future<void> storeRenamedSubjects(Map<String, String> subjects,
{required String userId}) async {
String renamedSubjectsJson = jsonEncode(subjects); String renamedSubjectsJson = jsonEncode(subjects);
await db.update("user_data", {"renamed_subjects": renamedSubjectsJson}, where: "id = ?", whereArgs: [userId]); await db.update("user_data", {"renamed_subjects": renamedSubjectsJson},
where: "id = ?", whereArgs: [userId]);
}
Future<void> storeRenamedTeachers(Map<String, String> teachers,
{required String userId}) async {
String renamedTeachersJson = jsonEncode(teachers);
await db.update("user_data", {"renamed_teachers": renamedTeachersJson},
where: "id = ?", whereArgs: [userId]);
} }
} }

View File

@ -42,17 +42,23 @@ class NotificationsHelper {
// loop through grades and see which hasn't been seen yet // loop through grades and see which hasn't been seen yet
for (Grade grade in grades) { for (Grade grade in grades) {
// if grade is not a normal grade (1-5), don't show it
if ([1, 2, 3, 4, 5].contains(grade.value.value)) {
// if the grade was added over a week ago, don't show it to avoid notification spam // if the grade was added over a week ago, don't show it to avoid notification spam
if (grade.seenDate.isAfter(lastSeenGrade) && if (grade.seenDate.isAfter(lastSeenGrade) &&
grade.date.difference(DateTime.now()).inDays * -1 < 7) { grade.date.difference(DateTime.now()).inDays * -1 < 7) {
// send notificiation about new grade // send notificiation about new grade
const AndroidNotificationDetails androidNotificationDetails = const AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails('GRADES', 'Jegyek', AndroidNotificationDetails(
'GRADES',
'Jegyek',
channelDescription: 'Értesítés jegyek beírásakor', channelDescription: 'Értesítés jegyek beírásakor',
importance: Importance.max, importance: Importance.max,
priority: Priority.max, priority: Priority.max,
color: Color(0xFF3D7BF4), color: Color(0xFF3D7BF4),
ticker: 'Jegyek'); ticker: 'Jegyek',
groupKey: 'refilc.notifications.GRADES_GROUP',
);
const NotificationDetails notificationDetails = const NotificationDetails notificationDetails =
NotificationDetails(android: androidNotificationDetails); NotificationDetails(android: androidNotificationDetails);
await flutterLocalNotificationsPlugin.show( await flutterLocalNotificationsPlugin.show(
@ -69,6 +75,7 @@ class NotificationsHelper {
notificationDetails); notificationDetails);
} }
} }
}
// set grade seen status // set grade seen status
gradeProvider.seenAll(); gradeProvider.seenAll();
} }

View File

@ -1,30 +1,36 @@
class News { class News {
String id;
String title; String title;
String content; String content;
String link; String link;
String openLabel; String openLabel;
String platform; String platform;
bool emergency; bool emergency;
DateTime expireDate;
Map? json; Map? json;
News({ News({
required this.id,
required this.title, required this.title,
required this.content, required this.content,
required this.link, required this.link,
required this.openLabel, required this.openLabel,
required this.platform, required this.platform,
required this.emergency, required this.emergency,
required this.expireDate,
this.json, this.json,
}); });
factory News.fromJson(Map json) { factory News.fromJson(Map json) {
return News( return News(
id: json["id"] ?? "",
title: json["title"] ?? "", title: json["title"] ?? "",
content: json["content"] ?? "", content: json["content"] ?? "",
link: json["link"] ?? "", link: json["link"] ?? "",
openLabel: json["open_label"] ?? "", openLabel: json["open_label"] ?? "",
platform: json["platform"] ?? "", platform: json["platform"] ?? "",
emergency: json["emergency"] ?? false, emergency: json["emergency"] ?? false,
expireDate: DateTime.parse(json["expire_date"] ?? ''),
json: json, json: json,
); );
} }

View File

@ -29,7 +29,7 @@ class SettingsProvider extends ChangeNotifier {
// zero is one, ... // zero is one, ...
List<Color> _gradeColors; List<Color> _gradeColors;
bool _newsEnabled; bool _newsEnabled;
int _newsState; String _seenNews;
bool _notificationsEnabled; bool _notificationsEnabled;
/* /*
notificationsBitfield values: notificationsBitfield values:
@ -69,6 +69,8 @@ class SettingsProvider extends ChangeNotifier {
String _lastAccountId; String _lastAccountId;
bool _renamedSubjectsEnabled; bool _renamedSubjectsEnabled;
bool _renamedSubjectsItalics; bool _renamedSubjectsItalics;
bool _renamedTeachersEnabled;
bool _renamedTeachersItalics;
SettingsProvider({ SettingsProvider({
DatabaseProvider? database, DatabaseProvider? database,
@ -79,7 +81,7 @@ class SettingsProvider extends ChangeNotifier {
required AccentColor accentColor, required AccentColor accentColor,
required List<Color> gradeColors, required List<Color> gradeColors,
required bool newsEnabled, required bool newsEnabled,
required int newsState, required String seenNews,
required bool notificationsEnabled, required bool notificationsEnabled,
required int notificationsBitfield, required int notificationsBitfield,
required bool developerMode, required bool developerMode,
@ -106,6 +108,8 @@ class SettingsProvider extends ChangeNotifier {
required String lastAccountId, required String lastAccountId,
required bool renameSubjectsEnabled, required bool renameSubjectsEnabled,
required bool renameSubjectsItalics, required bool renameSubjectsItalics,
required bool renameTeachersEnabled,
required bool renameTeachersItalics,
}) : _database = database, }) : _database = database,
_language = language, _language = language,
_startPage = startPage, _startPage = startPage,
@ -114,7 +118,7 @@ class SettingsProvider extends ChangeNotifier {
_accentColor = accentColor, _accentColor = accentColor,
_gradeColors = gradeColors, _gradeColors = gradeColors,
_newsEnabled = newsEnabled, _newsEnabled = newsEnabled,
_newsState = newsState, _seenNews = seenNews,
_notificationsEnabled = notificationsEnabled, _notificationsEnabled = notificationsEnabled,
_notificationsBitfield = notificationsBitfield, _notificationsBitfield = notificationsBitfield,
_developerMode = developerMode, _developerMode = developerMode,
@ -140,7 +144,9 @@ class SettingsProvider extends ChangeNotifier {
_premiumLogin = premiumLogin, _premiumLogin = premiumLogin,
_lastAccountId = lastAccountId, _lastAccountId = lastAccountId,
_renamedSubjectsEnabled = renameSubjectsEnabled, _renamedSubjectsEnabled = renameSubjectsEnabled,
_renamedSubjectsItalics = renameSubjectsItalics; _renamedSubjectsItalics = renameSubjectsItalics,
_renamedTeachersEnabled = renameTeachersEnabled,
_renamedTeachersItalics = renameTeachersItalics;
factory SettingsProvider.fromMap(Map map, factory SettingsProvider.fromMap(Map map,
{required DatabaseProvider database}) { {required DatabaseProvider database}) {
@ -152,6 +158,8 @@ class SettingsProvider extends ChangeNotifier {
log("[ERROR] SettingsProvider.fromMap: $e"); log("[ERROR] SettingsProvider.fromMap: $e");
} }
print(map['seen_news']);
return SettingsProvider( return SettingsProvider(
database: database, database: database,
language: map["language"], language: map["language"],
@ -167,7 +175,7 @@ class SettingsProvider extends ChangeNotifier {
Color(map["grade_color5"]), Color(map["grade_color5"]),
], ],
newsEnabled: map["news"] == 1, newsEnabled: map["news"] == 1,
newsState: map["news_state"], seenNews: map["seen_news"],
notificationsEnabled: map["notifications"] == 1, notificationsEnabled: map["notifications"] == 1,
notificationsBitfield: map["notifications_bitfield"], notificationsBitfield: map["notifications_bitfield"],
notificationPollInterval: map["notification_poll_interval"], notificationPollInterval: map["notification_poll_interval"],
@ -195,6 +203,8 @@ class SettingsProvider extends ChangeNotifier {
lastAccountId: map["last_account_id"], lastAccountId: map["last_account_id"],
renameSubjectsEnabled: map["renamed_subjects_enabled"] == 1, renameSubjectsEnabled: map["renamed_subjects_enabled"] == 1,
renameSubjectsItalics: map["renamed_subjects_italics"] == 1, renameSubjectsItalics: map["renamed_subjects_italics"] == 1,
renameTeachersEnabled: map["renamed_teachers_enabled"] == 1,
renameTeachersItalics: map["renamed_teachers_italics"] == 1,
); );
} }
@ -206,7 +216,7 @@ class SettingsProvider extends ChangeNotifier {
"theme": _theme.index, "theme": _theme.index,
"accent_color": _accentColor.index, "accent_color": _accentColor.index,
"news": _newsEnabled ? 1 : 0, "news": _newsEnabled ? 1 : 0,
"news_state": _newsState, "seen_news": _seenNews,
"notifications": _notificationsEnabled ? 1 : 0, "notifications": _notificationsEnabled ? 1 : 0,
"notifications_bitfield": _notificationsBitfield, "notifications_bitfield": _notificationsBitfield,
"developer_mode": _developerMode ? 1 : 0, "developer_mode": _developerMode ? 1 : 0,
@ -236,7 +246,9 @@ class SettingsProvider extends ChangeNotifier {
"premium_login": _premiumLogin, "premium_login": _premiumLogin,
"last_account_id": _lastAccountId, "last_account_id": _lastAccountId,
"renamed_subjects_enabled": _renamedSubjectsEnabled ? 1 : 0, "renamed_subjects_enabled": _renamedSubjectsEnabled ? 1 : 0,
"renamed_subjects_italics": _renamedSubjectsItalics ? 1 : 0 "renamed_subjects_italics": _renamedSubjectsItalics ? 1 : 0,
"renamed_teachers_enabled": _renamedTeachersEnabled ? 1 : 0,
"renamed_teachers_italics": _renamedTeachersItalics ? 1 : 0,
}; };
} }
@ -256,7 +268,7 @@ class SettingsProvider extends ChangeNotifier {
DarkMobileAppColors().gradeFive, DarkMobileAppColors().gradeFive,
], ],
newsEnabled: true, newsEnabled: true,
newsState: -1, seenNews: '',
notificationsEnabled: true, notificationsEnabled: true,
notificationsBitfield: 255, notificationsBitfield: 255,
developerMode: false, developerMode: false,
@ -283,6 +295,8 @@ class SettingsProvider extends ChangeNotifier {
lastAccountId: "", lastAccountId: "",
renameSubjectsEnabled: false, renameSubjectsEnabled: false,
renameSubjectsItalics: false, renameSubjectsItalics: false,
renameTeachersEnabled: false,
renameTeachersItalics: false,
); );
} }
@ -294,7 +308,7 @@ class SettingsProvider extends ChangeNotifier {
AccentColor get accentColor => _accentColor; AccentColor get accentColor => _accentColor;
List<Color> get gradeColors => _gradeColors; List<Color> get gradeColors => _gradeColors;
bool get newsEnabled => _newsEnabled; bool get newsEnabled => _newsEnabled;
int get newsState => _newsState; List<String> get seenNews => _seenNews.split(',');
bool get notificationsEnabled => _notificationsEnabled; bool get notificationsEnabled => _notificationsEnabled;
int get notificationsBitfield => _notificationsBitfield; int get notificationsBitfield => _notificationsBitfield;
bool get developerMode => _developerMode; bool get developerMode => _developerMode;
@ -324,6 +338,8 @@ class SettingsProvider extends ChangeNotifier {
String get lastAccountId => _lastAccountId; String get lastAccountId => _lastAccountId;
bool get renamedSubjectsEnabled => _renamedSubjectsEnabled; bool get renamedSubjectsEnabled => _renamedSubjectsEnabled;
bool get renamedSubjectsItalics => _renamedSubjectsItalics; bool get renamedSubjectsItalics => _renamedSubjectsItalics;
bool get renamedTeachersEnabled => _renamedTeachersEnabled;
bool get renamedTeachersItalics => _renamedTeachersItalics;
Future<void> update({ Future<void> update({
bool store = true, bool store = true,
@ -334,7 +350,7 @@ class SettingsProvider extends ChangeNotifier {
AccentColor? accentColor, AccentColor? accentColor,
List<Color>? gradeColors, List<Color>? gradeColors,
bool? newsEnabled, bool? newsEnabled,
int? newsState, String? seenNewsId,
bool? notificationsEnabled, bool? notificationsEnabled,
int? notificationsBitfield, int? notificationsBitfield,
bool? developerMode, bool? developerMode,
@ -361,6 +377,8 @@ class SettingsProvider extends ChangeNotifier {
String? lastAccountId, String? lastAccountId,
bool? renamedSubjectsEnabled, bool? renamedSubjectsEnabled,
bool? renamedSubjectsItalics, bool? renamedSubjectsItalics,
bool? renamedTeachersEnabled,
bool? renamedTeachersItalics,
}) async { }) async {
if (language != null && language != _language) _language = language; if (language != null && language != _language) _language = language;
if (startPage != null && startPage != _startPage) _startPage = startPage; if (startPage != null && startPage != _startPage) _startPage = startPage;
@ -375,7 +393,11 @@ class SettingsProvider extends ChangeNotifier {
if (newsEnabled != null && newsEnabled != _newsEnabled) { if (newsEnabled != null && newsEnabled != _newsEnabled) {
_newsEnabled = newsEnabled; _newsEnabled = newsEnabled;
} }
if (newsState != null && newsState != _newsState) _newsState = newsState; if (seenNewsId != null && !_seenNews.split(',').contains(seenNewsId)) {
var tempList = _seenNews.split(',');
tempList.add(seenNewsId);
_seenNews = tempList.join(',');
}
if (notificationsEnabled != null && if (notificationsEnabled != null &&
notificationsEnabled != _notificationsEnabled) { notificationsEnabled != _notificationsEnabled) {
_notificationsEnabled = notificationsEnabled; _notificationsEnabled = notificationsEnabled;
@ -448,6 +470,14 @@ class SettingsProvider extends ChangeNotifier {
renamedSubjectsItalics != _renamedSubjectsItalics) { renamedSubjectsItalics != _renamedSubjectsItalics) {
_renamedSubjectsItalics = renamedSubjectsItalics; _renamedSubjectsItalics = renamedSubjectsItalics;
} }
if (renamedTeachersEnabled != null &&
renamedTeachersEnabled != _renamedTeachersEnabled) {
_renamedTeachersEnabled = renamedTeachersEnabled;
}
if (renamedTeachersItalics != null &&
renamedTeachersItalics != _renamedTeachersItalics) {
_renamedTeachersItalics = renamedTeachersItalics;
}
if (store) await _database?.store.storeSettings(this); if (store) await _database?.store.storeSettings(this);
notifyListeners(); notifyListeners();
} }

View File

@ -12,7 +12,8 @@ import 'package:filcnaplo_mobile_ui/common/widgets/lesson/changed_lesson_tile.da
import 'package:filcnaplo/utils/format.dart'; import 'package:filcnaplo/utils/format.dart';
// difference.inDays is not reliable // difference.inDays is not reliable
bool _sameDate(DateTime a, DateTime b) => (a.year == b.year && a.month == b.month && a.day == b.day); bool _sameDate(DateTime a, DateTime b) =>
(a.year == b.year && a.month == b.month && a.day == b.day);
List<Widget> sortDateWidgets( List<Widget> sortDateWidgets(
BuildContext context, { BuildContext context, {
@ -35,13 +36,16 @@ List<Widget> sortDateWidgets(
if (message.conversationId != null) { if (message.conversationId != null) {
convMessages.add(w); convMessages.add(w);
Conversation conv = conversations.firstWhere((e) => e.id == message.conversationId, orElse: () => Conversation(id: message.conversationId!)); Conversation conv = conversations.firstWhere(
(e) => e.id == message.conversationId,
orElse: () => Conversation(id: message.conversationId!));
conv.add(message); conv.add(message);
if (conv.messages.length == 1) conversations.add(conv); if (conv.messages.length == 1) conversations.add(conv);
} }
if (conversations.any((c) => c.id == message.messageId)) { if (conversations.any((c) => c.id == message.messageId)) {
Conversation conv = conversations.firstWhere((e) => e.id == message.messageId); Conversation conv =
conversations.firstWhere((e) => e.id == message.messageId);
convMessages.add(w); convMessages.add(w);
conv.add(message); conv.add(message);
} }
@ -87,26 +91,41 @@ List<Widget> sortDateWidgets(
// Group Absence Tiles // Group Absence Tiles
List<DateWidget> absenceTileWidgets = elements.where((element) { List<DateWidget> absenceTileWidgets = elements.where((element) {
return element.widget is AbsenceViewable && (element.widget as AbsenceViewable).absence.delay == 0; return element.widget is AbsenceViewable &&
(element.widget as AbsenceViewable).absence.delay == 0;
}).toList(); }).toList();
List<AbsenceViewable> absenceTiles = absenceTileWidgets.map((e) => e.widget as AbsenceViewable).toList(); List<AbsenceViewable> absenceTiles =
absenceTileWidgets.map((e) => e.widget as AbsenceViewable).toList();
if (absenceTiles.length > 1) { if (absenceTiles.length > 1) {
elements.removeWhere((element) => element.widget.runtimeType == AbsenceViewable && (element.widget as AbsenceViewable).absence.delay == 0); elements.removeWhere((element) =>
element.widget.runtimeType == AbsenceViewable &&
(element.widget as AbsenceViewable).absence.delay == 0);
if (elements.isEmpty) { if (elements.isEmpty) {
cst = false; cst = false;
} }
elements.add(DateWidget( elements.add(
widget: AbsenceGroupTile(absenceTiles, showDate: !cst), DateWidget(
widget: AbsenceGroupTile(
absenceTiles,
showDate: !cst,
padding: const EdgeInsets.symmetric(horizontal: 6.0),
),
date: absenceTileWidgets.first.date, date: absenceTileWidgets.first.date,
key: "${absenceTileWidgets.first.date.millisecondsSinceEpoch}-absence-group")); key:
"${absenceTileWidgets.first.date.millisecondsSinceEpoch}-absence-group"),
);
} }
// Bring Lesson Tiles to front & sort by index asc // Bring Lesson Tiles to front & sort by index asc
List<DateWidget> lessonTiles = elements.where((element) { List<DateWidget> lessonTiles = elements.where((element) {
return element.widget.runtimeType == ChangedLessonTile; return element.widget.runtimeType == ChangedLessonTile;
}).toList(); }).toList();
lessonTiles.sort((a, b) => (a.widget as ChangedLessonTile).lesson.lessonIndex.compareTo((b.widget as ChangedLessonTile).lesson.lessonIndex)); lessonTiles.sort((a, b) => (a.widget as ChangedLessonTile)
elements.removeWhere((element) => element.widget.runtimeType == ChangedLessonTile); .lesson
.lessonIndex
.compareTo((b.widget as ChangedLessonTile).lesson.lessonIndex));
elements.removeWhere(
(element) => element.widget.runtimeType == ChangedLessonTile);
elements.insertAll(0, lessonTiles); elements.insertAll(0, lessonTiles);
final date = (elements + absenceTileWidgets).first.date; final date = (elements + absenceTileWidgets).first.date;
@ -122,7 +141,8 @@ List<Widget> sortDateWidgets(
spawnIsolate: false, spawnIsolate: false,
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, animation, item, index) => filterItemBuilder(context, animation, item.widget, index), itemBuilder: (context, animation, item, index) =>
filterItemBuilder(context, animation, item.widget, index),
items: elements, items: elements,
), ),
), ),
@ -131,9 +151,12 @@ List<Widget> sortDateWidgets(
} }
final nh = DateTime.now(); final nh = DateTime.now();
final now = DateTime(nh.year, nh.month, nh.day).subtract(const Duration(seconds: 1)); 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))) { if (showDivider &&
items.any((i) => i.date.isBefore(now)) &&
items.any((i) => i.date.isAfter(now))) {
items.add( items.add(
DateWidget( DateWidget(
date: now, date: now,
@ -153,7 +176,9 @@ List<Widget> sortDateWidgets(
} }
// Sort future dates asc, past dates desc // 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)); 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(); return items.map((e) => e.widget).toList();
} }

View File

@ -35,7 +35,8 @@ class SubjectAbsence {
List<Absence> absences; List<Absence> absences;
double percentage; double percentage;
SubjectAbsence({required this.subject, this.absences = const [], this.percentage = 0.0}); SubjectAbsence(
{required this.subject, this.absences = const [], this.percentage = 0.0});
} }
class AbsencesPage extends StatefulWidget { class AbsencesPage extends StatefulWidget {
@ -45,7 +46,8 @@ class AbsencesPage extends StatefulWidget {
_AbsencesPageState createState() => _AbsencesPageState(); _AbsencesPageState createState() => _AbsencesPageState();
} }
class _AbsencesPageState extends State<AbsencesPage> with TickerProviderStateMixin { class _AbsencesPageState extends State<AbsencesPage>
with TickerProviderStateMixin {
late UserProvider user; late UserProvider user;
late AbsenceProvider absenceProvider; late AbsenceProvider absenceProvider;
late TimetableProvider timetableProvider; late TimetableProvider timetableProvider;
@ -65,7 +67,9 @@ class _AbsencesPageState extends State<AbsencesPage> with TickerProviderStateMix
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
for (final lesson in timetableProvider.getWeek(Week.current()) ?? []) { for (final lesson in timetableProvider.getWeek(Week.current()) ?? []) {
if (!lesson.isEmpty && lesson.subject.id != '' && lesson.lessonYearIndex != null) { if (!lesson.isEmpty &&
lesson.subject.id != '' &&
lesson.lessonYearIndex != null) {
_lessonCount.update( _lessonCount.update(
lesson.subject, lesson.subject,
(value) { (value) {
@ -89,25 +93,30 @@ class _AbsencesPageState extends State<AbsencesPage> with TickerProviderStateMix
if (absence.delay != 0) continue; if (absence.delay != 0) continue;
if (!_absences.containsKey(absence.subject)) { if (!_absences.containsKey(absence.subject)) {
_absences[absence.subject] = SubjectAbsence(subject: absence.subject, absences: [absence]); _absences[absence.subject] =
SubjectAbsence(subject: absence.subject, absences: [absence]);
} else { } else {
_absences[absence.subject]?.absences.add(absence); _absences[absence.subject]?.absences.add(absence);
} }
} }
_absences.forEach((subject, absence) { _absences.forEach((subject, absence) {
final absentLessonsOfSubject = absenceProvider.absences.where((e) => e.subject == subject && e.delay == 0).length; final absentLessonsOfSubject = absenceProvider.absences
.where((e) => e.subject == subject && e.delay == 0)
.length;
final totalLessonsOfSubject = _lessonCount[subject]?.lessonYearIndex ?? 0; final totalLessonsOfSubject = _lessonCount[subject]?.lessonYearIndex ?? 0;
double absentLessonsOfSubjectPercentage; double absentLessonsOfSubjectPercentage;
if (absentLessonsOfSubject <= totalLessonsOfSubject) { if (absentLessonsOfSubject <= totalLessonsOfSubject) {
absentLessonsOfSubjectPercentage = absentLessonsOfSubject / totalLessonsOfSubject * 100; absentLessonsOfSubjectPercentage =
absentLessonsOfSubject / totalLessonsOfSubject * 100;
} else { } else {
absentLessonsOfSubjectPercentage = -1; absentLessonsOfSubjectPercentage = -1;
} }
_absences[subject]?.percentage = absentLessonsOfSubjectPercentage.clamp(-1, 100.0); _absences[subject]?.percentage =
absentLessonsOfSubjectPercentage.clamp(-1, 100.0);
}); });
absences = _absences.values.toList(); absences = _absences.values.toList();
@ -131,7 +140,8 @@ class _AbsencesPageState extends State<AbsencesPage> with TickerProviderStateMix
body: Padding( body: Padding(
padding: const EdgeInsets.only(top: 12.0), padding: const EdgeInsets.only(top: 12.0),
child: NestedScrollView( child: NestedScrollView(
physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics()),
headerSliverBuilder: (context, _) => [ headerSliverBuilder: (context, _) => [
SliverAppBar( SliverAppBar(
pinned: true, pinned: true,
@ -145,7 +155,10 @@ class _AbsencesPageState extends State<AbsencesPage> with TickerProviderStateMix
padding: const EdgeInsets.only(left: 8.0), padding: const EdgeInsets.only(left: 8.0),
child: Text( child: Text(
"Absences".i18n, "Absences".i18n,
style: TextStyle(color: AppColors.of(context).text, fontSize: 32.0, fontWeight: FontWeight.bold), style: TextStyle(
color: AppColors.of(context).text,
fontSize: 32.0,
fontWeight: FontWeight.bold),
), ),
), ),
bottom: FilterBar(items: [ bottom: FilterBar(items: [
@ -158,7 +171,8 @@ class _AbsencesPageState extends State<AbsencesPage> with TickerProviderStateMix
body: TabBarView( body: TabBarView(
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
controller: _tabController, controller: _tabController,
children: List.generate(3, (index) => filterViewBuilder(context, index))), children: List.generate(
3, (index) => filterViewBuilder(context, index))),
), ),
), ),
); );
@ -174,10 +188,17 @@ class _AbsencesPageState extends State<AbsencesPage> with TickerProviderStateMix
widget: AbsenceSubjectTile( widget: AbsenceSubjectTile(
a.subject, a.subject,
percentage: a.percentage, percentage: a.percentage,
excused: a.absences.where((a) => a.state == Justification.excused).length, excused: a.absences
unexcused: a.absences.where((a) => a.state == Justification.unexcused).length, .where((a) => a.state == Justification.excused)
pending: a.absences.where((a) => a.state == Justification.pending).length, .length,
onTap: () => AbsenceSubjectView.show(a.subject, a.absences, context: context), unexcused: a.absences
.where((a) => a.state == Justification.unexcused)
.length,
pending: a.absences
.where((a) => a.state == Justification.pending)
.length,
onTap: () => AbsenceSubjectView.show(a.subject, a.absences,
context: context),
), ),
)); ));
} }
@ -187,14 +208,17 @@ class _AbsencesPageState extends State<AbsencesPage> with TickerProviderStateMix
if (absence.delay != 0) { if (absence.delay != 0) {
items.add(DateWidget( items.add(DateWidget(
date: absence.date, date: absence.date,
widget: AbsenceViewable(absence, padding: EdgeInsets.zero), widget: AbsenceViewable(
)); absence,
padding: EdgeInsets.zero,
)));
} }
} }
break; break;
case AbsenceFilter.misses: case AbsenceFilter.misses:
for (var note in noteProvider.notes) { for (var note in noteProvider.notes) {
if (note.type?.name == "HaziFeladatHiany" || note.type?.name == "Felszereleshiany") { if (note.type?.name == "HaziFeladatHiany" ||
note.type?.name == "Felszereleshiany") {
items.add(DateWidget( items.add(DateWidget(
date: note.date, date: note.date,
widget: MissTile(note), widget: MissTile(note),
@ -232,10 +256,15 @@ class _AbsencesPageState extends State<AbsencesPage> with TickerProviderStateMix
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0)),
title: Text("attention".i18n), title: Text("attention".i18n),
content: Text("attention_body".i18n), content: Text("attention_body".i18n),
actions: [ActionButton(label: "Ok", onTap: () => Navigator.of(context).pop())], actions: [
ActionButton(
label: "Ok",
onTap: () => Navigator.of(context).pop())
],
), ),
); );
}, },
@ -262,7 +291,10 @@ class _AbsencesPageState extends State<AbsencesPage> with TickerProviderStateMix
); );
}, },
child: Column( child: Column(
children: getFilterWidgets(AbsenceFilter.values[activeData]).map((e) => e.widget).cast<Widget>().toList(), children: getFilterWidgets(AbsenceFilter.values[activeData])
.map((e) => e.widget)
.cast<Widget>()
.toList(),
), ),
), ),
), ),
@ -284,7 +316,8 @@ class _AbsencesPageState extends State<AbsencesPage> with TickerProviderStateMix
itemCount: max(filterWidgets.length + (activeData <= 1 ? 1 : 0), 1), itemCount: max(filterWidgets.length + (activeData <= 1 ? 1 : 0), 1),
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (filterWidgets.isNotEmpty) { if (filterWidgets.isNotEmpty) {
if ((index == 0 && activeData == 1) || (index == 0 && activeData == 0)) { if ((index == 0 && activeData == 1) ||
(index == 0 && activeData == 0)) {
int value1 = 0; int value1 = 0;
int value2 = 0; int value2 = 0;
String title1 = ""; String title1 = "";
@ -292,18 +325,26 @@ class _AbsencesPageState extends State<AbsencesPage> with TickerProviderStateMix
String suffix = ""; String suffix = "";
if (activeData == AbsenceFilter.absences.index) { if (activeData == AbsenceFilter.absences.index) {
value1 = absenceProvider.absences.where((e) => e.delay == 0 && e.state == Justification.excused).length; value1 = absenceProvider.absences
value2 = absenceProvider.absences.where((e) => e.delay == 0 && e.state == Justification.unexcused).length; .where((e) =>
e.delay == 0 && e.state == Justification.excused)
.length;
value2 = absenceProvider.absences
.where((e) =>
e.delay == 0 && e.state == Justification.unexcused)
.length;
title1 = "stat_1".i18n; title1 = "stat_1".i18n;
title2 = "stat_2".i18n; title2 = "stat_2".i18n;
suffix = " " + "hr".i18n; suffix = " " + "hr".i18n;
} else if (activeData == AbsenceFilter.delays.index) { } else if (activeData == AbsenceFilter.delays.index) {
value1 = absenceProvider.absences value1 = absenceProvider.absences
.where((e) => e.delay != 0 && e.state == Justification.excused) .where((e) =>
e.delay != 0 && e.state == Justification.excused)
.map((e) => e.delay) .map((e) => e.delay)
.fold(0, (a, b) => a + b); .fold(0, (a, b) => a + b);
value2 = absenceProvider.absences value2 = absenceProvider.absences
.where((e) => e.delay != 0 && e.state == Justification.unexcused) .where((e) =>
e.delay != 0 && e.state == Justification.unexcused)
.map((e) => e.delay) .map((e) => e.delay)
.fold(0, (a, b) => a + b); .fold(0, (a, b) => a + b);
title1 = "stat_3".i18n; title1 = "stat_3".i18n;
@ -312,7 +353,8 @@ class _AbsencesPageState extends State<AbsencesPage> with TickerProviderStateMix
} }
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 24.0, left: 24.0, right: 24.0), padding: const EdgeInsets.only(
bottom: 24.0, left: 24.0, right: 24.0),
child: Row(children: [ child: Row(children: [
Expanded( Expanded(
child: StatisticsTile( child: StatisticsTile(
@ -348,7 +390,8 @@ class _AbsencesPageState extends State<AbsencesPage> with TickerProviderStateMix
} }
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 6.0), padding:
const EdgeInsets.symmetric(horizontal: 24.0, vertical: 6.0),
child: filterWidgets[index - (activeData <= 1 ? 1 : 0)], child: filterWidgets[index - (activeData <= 1 ? 1 : 0)],
); );
} else { } else {

View File

@ -1,6 +1,4 @@
import 'package:filcnaplo/api/providers/update_provider.dart'; import 'package:filcnaplo/api/providers/update_provider.dart';
import 'package:filcnaplo/theme/colors/accent.dart';
import 'package:filcnaplo/theme/observer.dart';
import 'package:filcnaplo_kreta_api/providers/absence_provider.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/event_provider.dart';
import 'package:filcnaplo_kreta_api/providers/exam_provider.dart'; import 'package:filcnaplo_kreta_api/providers/exam_provider.dart';

View File

@ -1,5 +1,6 @@
import "category.dart"; import "category.dart";
import "subject.dart"; import "subject.dart";
import "teacher.dart";
class Absence { class Absence {
Map? json; Map? json;
@ -7,7 +8,7 @@ class Absence {
DateTime date; DateTime date;
int delay; int delay;
DateTime submitDate; DateTime submitDate;
String teacher; Teacher teacher;
Justification state; Justification state;
Category? justification; Category? justification;
Category? type; Category? type;
@ -41,8 +42,12 @@ class Absence {
DateTime lessonEnd; DateTime lessonEnd;
int? lessonIndex; int? lessonIndex;
if (json["Ora"] != null) { if (json["Ora"] != null) {
lessonStart = json["Ora"]["KezdoDatum"] != null ? DateTime.parse(json["Ora"]["KezdoDatum"]).toLocal() : DateTime(0); lessonStart = json["Ora"]["KezdoDatum"] != null
lessonEnd = json["Ora"]["VegDatum"] != null ? DateTime.parse(json["Ora"]["VegDatum"]).toLocal() : DateTime(0); ? DateTime.parse(json["Ora"]["KezdoDatum"]).toLocal()
: DateTime(0);
lessonEnd = json["Ora"]["VegDatum"] != null
? DateTime.parse(json["Ora"]["VegDatum"]).toLocal()
: DateTime(0);
lessonIndex = json["Ora"]["Oraszam"]; lessonIndex = json["Ora"]["Oraszam"];
} else { } else {
lessonStart = DateTime(0); lessonStart = DateTime(0);
@ -51,23 +56,30 @@ class Absence {
return Absence( return Absence(
id: json["Uid"], id: json["Uid"],
date: json["Datum"] != null ? DateTime.parse(json["Datum"]).toLocal() : DateTime(0), date: json["Datum"] != null
? DateTime.parse(json["Datum"]).toLocal()
: DateTime(0),
delay: json["KesesPercben"] ?? 0, delay: json["KesesPercben"] ?? 0,
submitDate: json["KeszitesDatuma"] != null ? DateTime.parse(json["KeszitesDatuma"]).toLocal() : DateTime(0), submitDate: json["KeszitesDatuma"] != null
teacher: (json["RogzitoTanarNeve"] ?? "").trim(), ? DateTime.parse(json["KeszitesDatuma"]).toLocal()
: DateTime(0),
teacher: Teacher.fromString((json["RogzitoTanarNeve"] ?? "").trim()),
state: json["IgazolasAllapota"] == "Igazolt" state: json["IgazolasAllapota"] == "Igazolt"
? Justification.excused ? Justification.excused
: json["IgazolasAllapota"] == "Igazolando" : json["IgazolasAllapota"] == "Igazolando"
? Justification.pending ? Justification.pending
: Justification.unexcused, : Justification.unexcused,
justification: json["IgazolasTipusa"] != null ? Category.fromJson(json["IgazolasTipusa"]) : null, justification: json["IgazolasTipusa"] != null
? Category.fromJson(json["IgazolasTipusa"])
: null,
type: json["Tipus"] != null ? Category.fromJson(json["Tipus"]) : null, type: json["Tipus"] != null ? Category.fromJson(json["Tipus"]) : null,
mode: json["Mod"] != null ? Category.fromJson(json["Mod"]) : null, mode: json["Mod"] != null ? Category.fromJson(json["Mod"]) : null,
subject: Subject.fromJson(json["Tantargy"] ?? {}), subject: Subject.fromJson(json["Tantargy"] ?? {}),
lessonStart: lessonStart, lessonStart: lessonStart,
lessonEnd: lessonEnd, lessonEnd: lessonEnd,
lessonIndex: lessonIndex, lessonIndex: lessonIndex,
group: json["OsztalyCsoport"] != null ? json["OsztalyCsoport"]["Uid"] : "", group:
json["OsztalyCsoport"] != null ? json["OsztalyCsoport"]["Uid"] : "",
json: json, json: json,
); );
} }

View File

@ -1,4 +1,5 @@
import 'category.dart'; import 'category.dart';
import 'teacher.dart';
class Exam { class Exam {
Map? json; Map? json;
@ -7,7 +8,7 @@ class Exam {
Category? mode; Category? mode;
int? subjectIndex; int? subjectIndex;
String subjectName; String subjectName;
String teacher; Teacher teacher;
String description; String description;
String group; String group;
String id; String id;
@ -28,14 +29,20 @@ class Exam {
factory Exam.fromJson(Map json) { factory Exam.fromJson(Map json) {
return Exam( return Exam(
id: json["Uid"] ?? "", id: json["Uid"] ?? "",
date: json["BejelentesDatuma"] != null ? DateTime.parse(json["BejelentesDatuma"]).toLocal() : DateTime(0), date: json["BejelentesDatuma"] != null
writeDate: json["Datum"] != null ? DateTime.parse(json["Datum"]).toLocal() : DateTime(0), ? DateTime.parse(json["BejelentesDatuma"]).toLocal()
: DateTime(0),
writeDate: json["Datum"] != null
? DateTime.parse(json["Datum"]).toLocal()
: DateTime(0),
mode: json["Modja"] != null ? Category.fromJson(json["Modja"]) : null, mode: json["Modja"] != null ? Category.fromJson(json["Modja"]) : null,
subjectIndex: json["OrarendiOraOraszama"], subjectIndex: json["OrarendiOraOraszama"],
subjectName: json["TantargyNeve"] ?? "", subjectName: json["TantargyNeve"] ?? "",
teacher: (json["RogzitoTanarNeve"] ?? "").trim(), teacher: Teacher.fromString((json["RogzitoTanarNeve"] ?? "").trim()),
description: (json["Temaja"] ?? "").trim(), description: (json["Temaja"] ?? "").trim(),
group: json["OsztalyCsoport"] != null ? json["OsztalyCsoport"]["Uid"] ?? "" : "", group: json["OsztalyCsoport"] != null
? json["OsztalyCsoport"]["Uid"] ?? ""
: "",
json: json, json: json,
); );
} }

View File

@ -1,13 +1,14 @@
import 'package:filcnaplo/utils/format.dart'; import 'package:filcnaplo/utils/format.dart';
import 'category.dart'; import 'category.dart';
import 'subject.dart'; import 'subject.dart';
import 'teacher.dart';
class Grade { class Grade {
Map? json; Map? json;
String id; String id;
DateTime date; DateTime date;
GradeValue value; GradeValue value;
String teacher; Teacher teacher;
String description; String description;
GradeType type; GradeType type;
String groupId; String groupId;
@ -38,23 +39,35 @@ class Grade {
factory Grade.fromJson(Map json) { factory Grade.fromJson(Map json) {
return Grade( return Grade(
id: json["Uid"] ?? "", id: json["Uid"] ?? "",
date: json["KeszitesDatuma"] != null ? DateTime.parse(json["KeszitesDatuma"]).toLocal() : DateTime(0), date: json["KeszitesDatuma"] != null
? DateTime.parse(json["KeszitesDatuma"]).toLocal()
: DateTime(0),
value: GradeValue( value: GradeValue(
json["SzamErtek"] ?? 0, json["SzamErtek"] ?? 0,
json["SzovegesErtek"] ?? "", json["SzovegesErtek"] ?? "",
json["SzovegesErtekelesRovidNev"] ?? "", json["SzovegesErtekelesRovidNev"] ?? "",
json["SulySzazalekErteke"] ?? 0, json["SulySzazalekErteke"] ?? 0,
percentage: json["ErtekFajta"] != null ? json["ErtekFajta"]["Uid"] == "3,Szazalekos" : false, percentage: json["ErtekFajta"] != null
? json["ErtekFajta"]["Uid"] == "3,Szazalekos"
: false,
), ),
teacher: (json["ErtekeloTanarNeve"] ?? "").trim(), teacher: Teacher.fromString((json["ErtekeloTanarNeve"] ?? "").trim()),
description: json["Tema"] ?? "", description: json["Tema"] ?? "",
type: json["Tipus"] != null ? Category.getGradeType(json["Tipus"]["Nev"]) : GradeType.unknown, type: json["Tipus"] != null
? Category.getGradeType(json["Tipus"]["Nev"])
: GradeType.unknown,
groupId: (json["OsztalyCsoport"] ?? {})["Uid"] ?? "", groupId: (json["OsztalyCsoport"] ?? {})["Uid"] ?? "",
subject: Subject.fromJson(json["Tantargy"] ?? {}), subject: Subject.fromJson(json["Tantargy"] ?? {}),
gradeType: json["ErtekFajta"] != null ? Category.fromJson(json["ErtekFajta"]) : null, gradeType: json["ErtekFajta"] != null
? Category.fromJson(json["ErtekFajta"])
: null,
mode: Category.fromJson(json["Mod"] ?? {}), mode: Category.fromJson(json["Mod"] ?? {}),
writeDate: json["RogzitesDatuma"] != null ? DateTime.parse(json["RogzitesDatuma"]).toLocal() : DateTime(0), writeDate: json["RogzitesDatuma"] != null
seenDate: json["LattamozasDatuma"] != null ? DateTime.parse(json["LattamozasDatuma"]).toLocal() : DateTime(0), ? DateTime.parse(json["RogzitesDatuma"]).toLocal()
: DateTime(0),
seenDate: json["LattamozasDatuma"] != null
? DateTime.parse(json["LattamozasDatuma"]).toLocal()
: DateTime(0),
form: (json["Jelleg"] ?? "Na") != "Na" ? json["Jelleg"] : "", form: (json["Jelleg"] ?? "Na") != "Na" ? json["Jelleg"] : "",
json: json, json: json,
); );
@ -76,7 +89,8 @@ class GradeValue {
set value(int v) => _value = v; set value(int v) => _value = v;
int get value { int get value {
String _valueName = valueName.toLowerCase().specialChars(); String _valueName = valueName.toLowerCase().specialChars();
if (_value == 0 && ["peldas", "jo", "valtozo", "rossz", "hanyag"].contains(_valueName)) { if (_value == 0 &&
["peldas", "jo", "valtozo", "rossz", "hanyag"].contains(_valueName)) {
switch (_valueName) { switch (_valueName) {
case "peldas": case "peldas":
return 5; return 5;
@ -87,7 +101,16 @@ class GradeValue {
case "rossz": case "rossz":
return 2; return 2;
case "hanyag": case "hanyag":
return 1;
// other
case "jeles":
return 5;
case "kozepes":
return 3;
case "elegseges":
return 2; return 2;
case "elegtelen":
return 1;
} }
} }
return _value; return _value;
@ -101,7 +124,8 @@ class GradeValue {
set weight(int v) => _weight = v; set weight(int v) => _weight = v;
int get weight { int get weight {
String _valueName = valueName.toLowerCase().specialChars(); String _valueName = valueName.toLowerCase().specialChars();
if (_value == 0 && ["peldas", "jo", "valtozo", "rossz", "hanyag"].contains(_valueName)) { if (_value == 0 &&
["peldas", "jo", "valtozo", "rossz", "hanyag"].contains(_valueName)) {
return 0; return 0;
} }
return _weight; return _weight;
@ -110,11 +134,23 @@ class GradeValue {
final bool _percentage; final bool _percentage;
bool get percentage => _percentage; bool get percentage => _percentage;
GradeValue(int value, String valueName, this.shortName, int weight, {bool percentage = false}) GradeValue(int value, String valueName, this.shortName, int weight,
{bool percentage = false})
: _value = value, : _value = value,
_valueName = valueName, _valueName = valueName,
_weight = weight, _weight = weight,
_percentage = percentage; _percentage = percentage;
} }
enum GradeType { midYear, firstQ, secondQ, halfYear, thirdQ, fourthQ, endYear, levelExam, ghost, unknown } enum GradeType {
midYear,
firstQ,
secondQ,
halfYear,
thirdQ,
fourthQ,
endYear,
levelExam,
ghost,
unknown
}

View File

@ -1,6 +1,7 @@
import 'package:filcnaplo_kreta_api/client/api.dart'; import 'package:filcnaplo_kreta_api/client/api.dart';
import 'subject.dart'; import 'subject.dart';
import 'teacher.dart';
class Homework { class Homework {
Map? json; Map? json;
@ -9,7 +10,7 @@ class Homework {
DateTime deadline; DateTime deadline;
bool byTeacher; bool byTeacher;
bool homeworkEnabled; bool homeworkEnabled;
String teacher; Teacher teacher;
String content; String content;
Subject subject; Subject subject;
String group; String group;
@ -45,7 +46,7 @@ class Homework {
: DateTime(0), : DateTime(0),
byTeacher: json["IsTanarRogzitette"] ?? true, byTeacher: json["IsTanarRogzitette"] ?? true,
homeworkEnabled: json["IsTanuloHaziFeladatEnabled"] ?? false, homeworkEnabled: json["IsTanuloHaziFeladatEnabled"] ?? false,
teacher: (json["RogzitoTanarNeve"] ?? "").trim(), teacher: Teacher.fromString((json["RogzitoTanarNeve"] ?? "").trim()),
content: (json["Szoveg"] ?? "").trim(), content: (json["Szoveg"] ?? "").trim(),
subject: Subject.fromJson(json["Tantargy"] ?? {}), subject: Subject.fromJson(json["Tantargy"] ?? {}),
group: json["OsztalyCsoport"] != null group: json["OsztalyCsoport"] != null

View File

@ -1,5 +1,6 @@
import 'subject.dart'; import 'subject.dart';
import 'category.dart'; import 'category.dart';
import 'teacher.dart';
class Lesson { class Lesson {
Map? json; Map? json;
@ -8,8 +9,8 @@ class Lesson {
Subject subject; Subject subject;
String lessonIndex; String lessonIndex;
int? lessonYearIndex; int? lessonYearIndex;
String substituteTeacher; Teacher? substituteTeacher;
String teacher; Teacher teacher;
bool homeworkEnabled; bool homeworkEnabled;
DateTime start; DateTime start;
DateTime end; DateTime end;
@ -31,7 +32,7 @@ class Lesson {
required this.subject, required this.subject,
required this.lessonIndex, required this.lessonIndex,
this.lessonYearIndex, this.lessonYearIndex,
this.substituteTeacher = "", this.substituteTeacher,
required this.teacher, required this.teacher,
this.homeworkEnabled = false, this.homeworkEnabled = false,
required this.start, required this.start,
@ -53,27 +54,38 @@ class Lesson {
factory Lesson.fromJson(Map json) { factory Lesson.fromJson(Map json) {
return Lesson( return Lesson(
id: json["Uid"] ?? "", id: json["Uid"] ?? "",
status: json["Allapot"] != null ? Category.fromJson(json["Allapot"]) : null, status:
date: json["Datum"] != null ? DateTime.parse(json["Datum"]).toLocal() : DateTime(0), json["Allapot"] != null ? Category.fromJson(json["Allapot"]) : null,
date: json["Datum"] != null
? DateTime.parse(json["Datum"]).toLocal()
: DateTime(0),
subject: Subject.fromJson(json["Tantargy"] ?? {}), subject: Subject.fromJson(json["Tantargy"] ?? {}),
lessonIndex: json["Oraszam"] != null ? json["Oraszam"].toString() : "+", lessonIndex: json["Oraszam"] != null ? json["Oraszam"].toString() : "+",
lessonYearIndex: json["OraEvesSorszama"], lessonYearIndex: json["OraEvesSorszama"],
substituteTeacher: (json["HelyettesTanarNeve"] ?? "").trim(), substituteTeacher:
teacher: (json["TanarNeve"] ?? "").trim(), Teacher.fromString((json["HelyettesTanarNeve"] ?? "").trim()),
teacher: Teacher.fromString((json["TanarNeve"] ?? "").trim()),
homeworkEnabled: json["IsTanuloHaziFeladatEnabled"] ?? false, homeworkEnabled: json["IsTanuloHaziFeladatEnabled"] ?? false,
start: json["KezdetIdopont"] != null ? DateTime.parse(json["KezdetIdopont"]).toLocal() : DateTime(0), start: json["KezdetIdopont"] != null
? DateTime.parse(json["KezdetIdopont"]).toLocal()
: DateTime(0),
studentPresence: json["TanuloJelenlet"] != null studentPresence: json["TanuloJelenlet"] != null
? (json["TanuloJelenlet"]["Nev"] ?? "") == "Hianyzas" ? (json["TanuloJelenlet"]["Nev"] ?? "") == "Hianyzas"
? false ? false
: true : true
: true, : true,
end: json["VegIdopont"] != null ? DateTime.parse(json["VegIdopont"]).toLocal() : DateTime(0), end: json["VegIdopont"] != null
? DateTime.parse(json["VegIdopont"]).toLocal()
: DateTime(0),
homeworkId: json["HaziFeladatUid"] ?? "", homeworkId: json["HaziFeladatUid"] ?? "",
exam: json["BejelentettSzamonkeresUid"] ?? "", exam: json["BejelentettSzamonkeresUid"] ?? "",
type: json["Tipus"] != null ? Category.fromJson(json["Tipus"]) : null, type: json["Tipus"] != null ? Category.fromJson(json["Tipus"]) : null,
description: json["Tema"] ?? "", description: json["Tema"] ?? "",
room: ((json["TeremNeve"] ?? "").split("_").join(" ") as String).replaceAll(RegExp(r" ?terem ?", caseSensitive: false), ""), room: ((json["TeremNeve"] ?? "").split("_").join(" ") as String)
groupName: json["OsztalyCsoport"] != null ? json["OsztalyCsoport"]["Nev"] ?? "" : "", .replaceAll(RegExp(r" ?terem ?", caseSensitive: false), ""),
groupName: json["OsztalyCsoport"] != null
? json["OsztalyCsoport"]["Nev"] ?? ""
: "",
name: json["Nev"] ?? "", name: json["Nev"] ?? "",
online: json["IsDigitalisOra"] ?? false, online: json["IsDigitalisOra"] ?? false,
isEmpty: json['isEmpty'] ?? false, isEmpty: json['isEmpty'] ?? false,
@ -92,6 +104,6 @@ class Lesson {
return null; return null;
} }
bool get isChanged => status?.name == "Elmaradt" || substituteTeacher != ""; bool get isChanged => status?.name == "Elmaradt" || substituteTeacher != null;
bool get swapDesc => room.length > 8; bool get swapDesc => room.length > 8;
} }

View File

@ -1,4 +1,5 @@
import 'category.dart'; import 'category.dart';
import 'teacher.dart';
class Note { class Note {
Map? json; Map? json;
@ -6,7 +7,7 @@ class Note {
String title; String title;
DateTime date; DateTime date;
DateTime submitDate; DateTime submitDate;
String teacher; Teacher teacher;
DateTime seenDate; DateTime seenDate;
String groupId; String groupId;
String content; String content;
@ -29,11 +30,19 @@ class Note {
return Note( return Note(
id: json["Uid"] ?? "", id: json["Uid"] ?? "",
title: json["Cim"] ?? "", title: json["Cim"] ?? "",
date: json["Datum"] != null ? DateTime.parse(json["Datum"]).toLocal() : DateTime(0), date: json["Datum"] != null
submitDate: json["KeszitesDatuma"] != null ? DateTime.parse(json["KeszitesDatuma"]).toLocal() : DateTime(0), ? DateTime.parse(json["Datum"]).toLocal()
teacher: (json["KeszitoTanarNeve"] ?? "").trim(), : DateTime(0),
seenDate: json["LattamozasDatuma"] != null ? DateTime.parse(json["LattamozasDatuma"]).toLocal() : DateTime(0), submitDate: json["KeszitesDatuma"] != null
groupId: json["OsztalyCsoport"] != null ? json["OsztalyCsoport"]["Uid"] ?? "" : "", ? DateTime.parse(json["KeszitesDatuma"]).toLocal()
: DateTime(0),
teacher: Teacher.fromString((json["KeszitoTanarNeve"] ?? "").trim()),
seenDate: json["LattamozasDatuma"] != null
? DateTime.parse(json["LattamozasDatuma"]).toLocal()
: DateTime(0),
groupId: json["OsztalyCsoport"] != null
? json["OsztalyCsoport"]["Uid"] ?? ""
: "",
content: json["Tartalom"].replaceAll("\r", "") ?? "", content: json["Tartalom"].replaceAll("\r", "") ?? "",
type: json["Tipus"] != null ? Category.fromJson(json["Tipus"]) : null, type: json["Tipus"] != null ? Category.fromJson(json["Tipus"]) : null,
json: json, json: json,

View File

@ -0,0 +1,34 @@
import 'package:filcnaplo/utils/format.dart';
class Teacher {
String id;
String name;
String? renamedTo;
bool get isRenamed => renamedTo != null;
Teacher({required this.id, required this.name, this.renamedTo});
factory Teacher.fromJson(Map json) {
return Teacher(
id: json["Uid"] ?? "",
name: (json["Nev"] ?? "").trim(),
);
}
factory Teacher.fromString(String string) {
return Teacher(
id: string.trim().replaceAll(' ', '').toLowerCase().specialChars(),
name: string.trim(),
);
}
@override
bool operator ==(other) {
if (other is! Teacher) return false;
return id == other.id;
}
@override
int get hashCode => id.hashCode;
}

View File

@ -27,7 +27,10 @@ class AbsenceProvider with ChangeNotifier {
// Load absences from the database // Load absences from the database
if (userId != null) { if (userId != null) {
var dbAbsences = await Provider.of<DatabaseProvider>(_context, listen: false).userQuery.getAbsences(userId: userId); var dbAbsences =
await Provider.of<DatabaseProvider>(_context, listen: false)
.userQuery
.getAbsences(userId: userId);
_absences = dbAbsences; _absences = dbAbsences;
await convertBySettings(); await convertBySettings();
} }
@ -36,12 +39,26 @@ class AbsenceProvider with ChangeNotifier {
// for renamed subjects // for renamed subjects
Future<void> convertBySettings() async { Future<void> convertBySettings() async {
final _database = Provider.of<DatabaseProvider>(_context, listen: false); final _database = Provider.of<DatabaseProvider>(_context, listen: false);
Map<String, String> renamedSubjects = (await _database.query.getSettings(_database)).renamedSubjectsEnabled Map<String, String> renamedSubjects =
? await _database.userQuery.renamedSubjects(userId: Provider.of<UserProvider>(_context, listen: false).user!.id) (await _database.query.getSettings(_database)).renamedSubjectsEnabled
? await _database.userQuery.renamedSubjects(
userId:
Provider.of<UserProvider>(_context, listen: false).user!.id)
: {};
Map<String, String> renamedTeachers =
(await _database.query.getSettings(_database)).renamedTeachersEnabled
? await _database.userQuery.renamedTeachers(
userId:
Provider.of<UserProvider>(_context, listen: false).user!.id)
: {}; : {};
for (Absence absence in _absences) { for (Absence absence in _absences) {
absence.subject.renamedTo = renamedSubjects.isNotEmpty ? renamedSubjects[absence.subject.id] : null; absence.subject.renamedTo = renamedSubjects.isNotEmpty
? renamedSubjects[absence.subject.id]
: null;
absence.teacher.renamedTo = renamedTeachers.isNotEmpty
? renamedTeachers[absence.teacher.id]
: null;
} }
notifyListeners(); notifyListeners();
@ -53,9 +70,11 @@ class AbsenceProvider with ChangeNotifier {
if (user == null) throw "Cannot fetch Absences for User null"; if (user == null) throw "Cannot fetch Absences for User null";
String iss = user.instituteCode; String iss = user.instituteCode;
List? absencesJson = await Provider.of<KretaClient>(_context, listen: false).getAPI(KretaAPI.absences(iss)); List? absencesJson = await Provider.of<KretaClient>(_context, listen: false)
.getAPI(KretaAPI.absences(iss));
if (absencesJson == null) throw "Cannot fetch Absences for User ${user.id}"; if (absencesJson == null) throw "Cannot fetch Absences for User ${user.id}";
List<Absence> absences = absencesJson.map((e) => Absence.fromJson(e)).toList(); List<Absence> absences =
absencesJson.map((e) => Absence.fromJson(e)).toList();
if (absences.isNotEmpty || _absences.isNotEmpty) await store(absences); if (absences.isNotEmpty || _absences.isNotEmpty) await store(absences);
} }
@ -66,7 +85,9 @@ class AbsenceProvider with ChangeNotifier {
if (user == null) throw "Cannot store Absences for User null"; if (user == null) throw "Cannot store Absences for User null";
String userId = user.id; String userId = user.id;
await Provider.of<DatabaseProvider>(_context, listen: false).userStore.storeAbsences(absences, userId: userId); await Provider.of<DatabaseProvider>(_context, listen: false)
.userStore
.storeAbsences(absences, userId: userId);
_absences = absences; _absences = absences;
await convertBySettings(); await convertBySettings();
} }

View File

@ -83,10 +83,15 @@ class GradeProvider with ChangeNotifier {
Map<String, String> renamedSubjects = _settings.renamedSubjectsEnabled Map<String, String> renamedSubjects = _settings.renamedSubjectsEnabled
? await _database.userQuery.renamedSubjects(userId: _user.user!.id) ? await _database.userQuery.renamedSubjects(userId: _user.user!.id)
: {}; : {};
Map<String, String> renamedTeachers = _settings.renamedTeachersEnabled
? await _database.userQuery.renamedTeachers(userId: _user.user!.id)
: {};
for (Grade grade in _grades) { for (Grade grade in _grades) {
grade.subject.renamedTo = grade.subject.renamedTo =
renamedSubjects.isNotEmpty ? renamedSubjects[grade.subject.id] : null; renamedSubjects.isNotEmpty ? renamedSubjects[grade.subject.id] : null;
grade.teacher.renamedTo =
renamedTeachers.isNotEmpty ? renamedTeachers[grade.teacher.id] : null;
grade.value.value = grade.value.value =
_settings.goodStudent ? 5 : grade.json!["SzamErtek"] ?? 0; _settings.goodStudent ? 5 : grade.json!["SzamErtek"] ?? 0;

View File

@ -48,11 +48,18 @@ class HomeworkProvider with ChangeNotifier {
(await _database.query.getSettings(_database)).renamedSubjectsEnabled (await _database.query.getSettings(_database)).renamedSubjectsEnabled
? await _database.userQuery.renamedSubjects(userId: _user.id!) ? await _database.userQuery.renamedSubjects(userId: _user.id!)
: {}; : {};
Map<String, String> renamedTeachers =
(await _database.query.getSettings(_database)).renamedTeachersEnabled
? await _database.userQuery.renamedTeachers(userId: _user.id!)
: {};
for (Homework homework in _homework) { for (Homework homework in _homework) {
homework.subject.renamedTo = renamedSubjects.isNotEmpty homework.subject.renamedTo = renamedSubjects.isNotEmpty
? renamedSubjects[homework.subject.id] ? renamedSubjects[homework.subject.id]
: null; : null;
homework.teacher.renamedTo = renamedTeachers.isNotEmpty
? renamedTeachers[homework.teacher.id]
: null;
} }
notifyListeners(); notifyListeners();

View File

@ -40,11 +40,18 @@ class TimetableProvider with ChangeNotifier {
(await _database.query.getSettings(_database)).renamedSubjectsEnabled (await _database.query.getSettings(_database)).renamedSubjectsEnabled
? await _database.userQuery.renamedSubjects(userId: _user.id!) ? await _database.userQuery.renamedSubjects(userId: _user.id!)
: {}; : {};
Map<String, String> renamedTeachers =
(await _database.query.getSettings(_database)).renamedTeachersEnabled
? await _database.userQuery.renamedTeachers(userId: _user.id!)
: {};
for (Lesson lesson in _lessons.values.expand((e) => e)) { for (Lesson lesson in _lessons.values.expand((e) => e)) {
lesson.subject.renamedTo = renamedSubjects.isNotEmpty lesson.subject.renamedTo = renamedSubjects.isNotEmpty
? renamedSubjects[lesson.subject.id] ? renamedSubjects[lesson.subject.id]
: null; : null;
lesson.teacher.renamedTo = renamedTeachers.isNotEmpty
? renamedTeachers[lesson.teacher.id]
: null;
} }
notifyListeners(); notifyListeners();

View File

@ -0,0 +1,41 @@
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:flutter/material.dart';
class BetaChip extends StatelessWidget {
const BetaChip({Key? key, this.disabled = false}) : super(key: key);
final bool disabled;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 25,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
child: Padding(
padding: const EdgeInsets.only(left: 8, right: 8),
child: Center(
child: Text(
"BETA",
softWrap: true,
style: TextStyle(
fontSize: 10,
color: disabled
? AppColors.of(context).text.withOpacity(.5)
: Colors.white,
fontWeight: FontWeight.w600,
overflow: TextOverflow.ellipsis,
),
),
),
),
decoration: BoxDecoration(
color: !disabled
? Theme.of(context).colorScheme.secondary
: AppColors.of(context).text.withOpacity(.25),
borderRadius: BorderRadius.circular(40),
),
),
);
}
}

View File

@ -9,7 +9,9 @@ import 'package:provider/provider.dart';
import 'absence_tile.i18n.dart'; import 'absence_tile.i18n.dart';
class AbsenceTile extends StatelessWidget { class AbsenceTile extends StatelessWidget {
const AbsenceTile(this.absence, {Key? key, this.onTap, this.elevation = 0.0, this.padding}) : super(key: key); const AbsenceTile(this.absence,
{Key? key, this.onTap, this.elevation = 0.0, this.padding})
: super(key: key);
final Absence absence; final Absence absence;
final void Function()? onTap; final void Function()? onTap;
@ -37,39 +39,61 @@ class AbsenceTile extends StatelessWidget {
child: Material( child: Material(
type: MaterialType.transparency, type: MaterialType.transparency,
child: Padding( child: Padding(
padding: padding ?? (group ? EdgeInsets.zero : const EdgeInsets.symmetric(horizontal: 8.0)), padding: padding ??
(group
? EdgeInsets.zero
: const EdgeInsets.symmetric(
horizontal: 8.0,
)),
child: ListTile( child: ListTile(
onTap: onTap, onTap: onTap,
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
dense: group, dense: group,
contentPadding: const EdgeInsets.only(left: 8.0, right: 12.0), contentPadding: const EdgeInsets.only(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(!group ? 14.0 : 12.0)), left: 14.0, right: 12.0, top: 2.0, bottom: 2.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(!group ? 14.0 : 12.0)),
leading: Container( leading: Container(
width: 44.0, width: 44.0,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: !group ? color.withOpacity(.25) : null, color: !group ? color.withOpacity(.25) : null,
), ),
child: Center(child: Icon(justificationIcon(absence.state), color: color)), child: Center(
child: Icon(justificationIcon(absence.state), color: color)),
), ),
title: !group title: !group
? Text.rich(TextSpan( ? Text.rich(TextSpan(
text: "${absence.delay == 0 ? "" : absence.delay}", text: "${absence.delay == 0 ? "" : absence.delay}",
style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 15.5), style: const TextStyle(
fontWeight: FontWeight.w700, fontSize: 15.5),
children: [ children: [
TextSpan( TextSpan(
text: absence.delay == 0 text: absence.delay == 0
? justificationName(absence.state).fill(["absence".i18n]).capital() ? justificationName(absence.state)
: 'minute'.plural(absence.delay) + justificationName(absence.state).fill(["delay".i18n]), .fill(["absence".i18n]).capital()
: 'minute'.plural(absence.delay) +
justificationName(absence.state)
.fill(["delay".i18n]),
style: const TextStyle(fontWeight: FontWeight.w600), style: const TextStyle(fontWeight: FontWeight.w600),
), ),
], ],
)) ))
: Text( : Text(
(absence.lessonIndex != null ? "${absence.lessonIndex}. " : "") + (absence.subject.renamedTo ?? absence.subject.name.capital()), (absence.lessonIndex != null
? "${absence.lessonIndex}. "
: "") +
(absence.subject.renamedTo ??
absence.subject.name.capital()),
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 14.0, fontStyle: absence.subject.isRenamed && settingsProvider.renamedSubjectsItalics ? FontStyle.italic : null), style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14.0,
fontStyle: absence.subject.isRenamed &&
settingsProvider.renamedSubjectsItalics
? FontStyle.italic
: null),
), ),
subtitle: !group subtitle: !group
? Text( ? Text(
@ -77,7 +101,12 @@ class AbsenceTile extends StatelessWidget {
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
// DateFormat("MM. dd. (EEEEE)", I18n.of(context).locale.toString()).format(absence.date), // DateFormat("MM. dd. (EEEEE)", I18n.of(context).locale.toString()).format(absence.date),
style: TextStyle(fontWeight: FontWeight.w500, fontStyle: absence.subject.isRenamed && settingsProvider.renamedSubjectsItalics ? FontStyle.italic : null), style: TextStyle(
fontWeight: FontWeight.w500,
fontStyle: absence.subject.isRenamed &&
settingsProvider.renamedSubjectsItalics
? FontStyle.italic
: null),
) )
: null, : null,
), ),
@ -97,7 +126,8 @@ class AbsenceTile extends StatelessWidget {
} }
} }
static Color justificationColor(Justification state, {required BuildContext context}) { static Color justificationColor(Justification state,
{required BuildContext context}) {
switch (state) { switch (state) {
case Justification.excused: case Justification.excused:
return AppColors.of(context).green; return AppColors.of(context).green;

View File

@ -17,19 +17,23 @@ import 'package:provider/provider.dart';
import 'absence_view.i18n.dart'; import 'absence_view.i18n.dart';
class AbsenceView extends StatelessWidget { class AbsenceView extends StatelessWidget {
const AbsenceView(this.absence, {Key? key, this.outsideContext, this.viewable = false}) : super(key: key); const AbsenceView(this.absence,
{Key? key, this.outsideContext, this.viewable = false})
: super(key: key);
final Absence absence; final Absence absence;
final BuildContext? outsideContext; final BuildContext? outsideContext;
final bool viewable; final bool viewable;
static show(Absence absence, {required BuildContext context}) { static show(Absence absence, {required BuildContext context}) {
showBottomCard(context: context, child: AbsenceView(absence, outsideContext: context)); showBottomCard(
context: context, child: AbsenceView(absence, outsideContext: context));
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Color color = AbsenceTile.justificationColor(absence.state, context: context); Color color =
AbsenceTile.justificationColor(absence.state, context: context);
SettingsProvider settingsProvider = Provider.of<SettingsProvider>(context); SettingsProvider settingsProvider = Provider.of<SettingsProvider>(context);
return Padding( return Padding(
@ -41,7 +45,8 @@ class AbsenceView extends StatelessWidget {
ListTile( ListTile(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
contentPadding: const EdgeInsets.only(left: 16.0, right: 12.0), contentPadding: const EdgeInsets.only(left: 16.0, right: 12.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0)),
leading: Container( leading: Container(
width: 44.0, width: 44.0,
height: 44.0, height: 44.0,
@ -60,10 +65,18 @@ class AbsenceView extends StatelessWidget {
absence.subject.renamedTo ?? absence.subject.name.capital(), absence.subject.renamedTo ?? absence.subject.name.capital(),
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.w700, fontStyle: absence.subject.isRenamed && settingsProvider.renamedSubjectsItalics ? FontStyle.italic : null), style: TextStyle(
fontWeight: FontWeight.w700,
fontStyle: absence.subject.isRenamed &&
settingsProvider.renamedSubjectsItalics
? FontStyle.italic
: null),
), ),
subtitle: Text( subtitle: Text(
absence.teacher, (absence.teacher.isRenamed
? absence.teacher.renamedTo
: absence.teacher.name) ??
'',
// DateFormat("MM. dd. (EEEEE)", I18n.of(context).locale.toString()).format(absence.date), // DateFormat("MM. dd. (EEEEE)", I18n.of(context).locale.toString()).format(absence.date),
style: const TextStyle(fontWeight: FontWeight.w500), style: const TextStyle(fontWeight: FontWeight.w500),
), ),
@ -77,12 +90,15 @@ class AbsenceView extends StatelessWidget {
if (absence.delay > 0) if (absence.delay > 0)
Detail( Detail(
title: "delay".i18n, title: "delay".i18n,
description: absence.delay.toString() + " " + "minutes".i18n.plural(absence.delay), description: absence.delay.toString() +
" " +
"minutes".i18n.plural(absence.delay),
), ),
if (absence.lessonIndex != null) if (absence.lessonIndex != null)
Detail( Detail(
title: "Lesson".i18n, title: "Lesson".i18n,
description: "${absence.lessonIndex}. (${absence.lessonStart.format(context, timeOnly: true)}" description:
"${absence.lessonIndex}. (${absence.lessonStart.format(context, timeOnly: true)}"
" - " " - "
"${absence.lessonEnd.format(context, timeOnly: true)})", "${absence.lessonEnd.format(context, timeOnly: true)})",
), ),
@ -91,13 +107,19 @@ class AbsenceView extends StatelessWidget {
title: "Excuse".i18n, title: "Excuse".i18n,
description: absence.justification?.description ?? "", description: absence.justification?.description ?? "",
), ),
if (absence.mode != null) Detail(title: "Mode".i18n, description: absence.mode?.description ?? ""), if (absence.mode != null)
Detail(title: "Submit date".i18n, description: absence.submitDate.format(context)), Detail(
title: "Mode".i18n,
description: absence.mode?.description ?? ""),
Detail(
title: "Submit date".i18n,
description: absence.submitDate.format(context)),
// Show in timetable // Show in timetable
if (!viewable) if (!viewable)
Padding( Padding(
padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 6.0, top: 12.0), padding: const EdgeInsets.only(
left: 16.0, right: 16.0, bottom: 6.0, top: 12.0),
child: PanelActionButton( child: PanelActionButton(
leading: const Icon(FeatherIcons.calendar), leading: const Icon(FeatherIcons.calendar),
title: Text( title: Text(
@ -109,12 +131,15 @@ class AbsenceView extends StatelessWidget {
Navigator.of(context).pop(); Navigator.of(context).pop();
if (outsideContext != null) { if (outsideContext != null) {
ReverseSearch.getLessonByAbsence(absence, context).then((lesson) { ReverseSearch.getLessonByAbsence(absence, context)
.then((lesson) {
if (lesson != null) { if (lesson != null) {
TimetablePage.jump(outsideContext!, lesson: lesson); TimetablePage.jump(outsideContext!, lesson: lesson);
} else { } else {
ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar( ScaffoldMessenger.of(context)
content: Text("Cannot find lesson".i18n, style: const TextStyle(color: Colors.white)), .showSnackBar(CustomSnackBar(
content: Text("Cannot find lesson".i18n,
style: const TextStyle(color: Colors.white)),
backgroundColor: AppColors.of(context).red, backgroundColor: AppColors.of(context).red,
context: context, context: context,
)); ));

View File

@ -5,6 +5,8 @@ import 'package:filcnaplo_mobile_ui/common/widgets/absence_group/absence_group_c
import 'package:filcnaplo_mobile_ui/common/widgets/absence/absence_tile.dart'; import 'package:filcnaplo_mobile_ui/common/widgets/absence/absence_tile.dart';
import 'package:filcnaplo/utils/format.dart'; import 'package:filcnaplo/utils/format.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:rounded_expansion_tile/rounded_expansion_tile.dart';
import 'absence_group_tile.i18n.dart'; import 'absence_group_tile.i18n.dart';
import 'package:rounded_expansion_tile/rounded_expansion_tile.dart'; import 'package:rounded_expansion_tile/rounded_expansion_tile.dart';
@ -31,13 +33,17 @@ class AbsenceGroupTile extends StatelessWidget {
child: Material( child: Material(
type: MaterialType.transparency, type: MaterialType.transparency,
child: Padding( child: Padding(
padding: padding ?? const EdgeInsets.symmetric(horizontal: 8.0), padding: padding ??
const EdgeInsets.symmetric(horizontal: 0.0, vertical: 0.0),
child: AbsenceGroupContainer( child: AbsenceGroupContainer(
child: RoundedExpansionTile( child: RoundedExpansionTile(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)), borderRadius: BorderRadius.circular(10)),
tilePadding: const EdgeInsets.symmetric(horizontal: 8.0), childrenPadding: const EdgeInsets.symmetric(horizontal: 8.0),
backgroundColor: Colors.transparent, tileColor: Colors.transparent,
duration: const Duration(milliseconds: 250),
trailingDuration: 0.5,
trailing: const Icon(FeatherIcons.chevronDown),
leading: Container( leading: Container(
width: 44.0, width: 44.0,
height: 44.0, height: 44.0,

View File

@ -12,7 +12,8 @@ class ExamView extends StatelessWidget {
final Exam exam; final Exam exam;
static show(Exam exam, {required BuildContext context}) => showBottomCard(context: context, child: ExamView(exam)); static show(Exam exam, {required BuildContext context}) =>
showBottomCard(context: context, child: ExamView(exam));
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -27,7 +28,8 @@ class ExamView extends StatelessWidget {
leading: Padding( leading: Padding(
padding: const EdgeInsets.only(left: 6.0), padding: const EdgeInsets.only(left: 6.0),
child: Icon( child: Icon(
SubjectIcon.resolveVariant(subjectName: exam.subjectName, context: context), SubjectIcon.resolveVariant(
subjectName: exam.subjectName, context: context),
size: 36.0, size: 36.0,
color: AppColors.of(context).text.withOpacity(.75), color: AppColors.of(context).text.withOpacity(.75),
), ),
@ -39,7 +41,10 @@ class ExamView extends StatelessWidget {
style: const TextStyle(fontWeight: FontWeight.w600), style: const TextStyle(fontWeight: FontWeight.w600),
), ),
subtitle: Text( subtitle: Text(
exam.teacher, (exam.teacher.isRenamed
? exam.teacher.renamedTo
: exam.teacher.name) ??
'',
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w500), style: const TextStyle(fontWeight: FontWeight.w500),
@ -51,9 +56,14 @@ class ExamView extends StatelessWidget {
), ),
// Details // Details
if (exam.writeDate.year != 0) Detail(title: "date".i18n, description: exam.writeDate.format(context)), if (exam.writeDate.year != 0)
if (exam.description != "") Detail(title: "description".i18n, description: exam.description), Detail(
if (exam.mode != null) Detail(title: "mode".i18n, description: exam.mode!.description), title: "date".i18n,
description: exam.writeDate.format(context)),
if (exam.description != "")
Detail(title: "description".i18n, description: exam.description),
if (exam.mode != null)
Detail(title: "mode".i18n, description: exam.mode!.description),
], ],
), ),
); );

View File

@ -11,7 +11,8 @@ import 'grade_view.i18n.dart';
class GradeView extends StatelessWidget { class GradeView extends StatelessWidget {
const GradeView(this.grade, {Key? key}) : super(key: key); const GradeView(this.grade, {Key? key}) : super(key: key);
static show(Grade grade, {required BuildContext context}) => showBottomCard(context: context, child: GradeView(grade)); static show(Grade grade, {required BuildContext context}) =>
showBottomCard(context: context, child: GradeView(grade));
final Grade grade; final Grade grade;
@ -30,10 +31,21 @@ class GradeView extends StatelessWidget {
grade.subject.renamedTo ?? grade.subject.name.capital(), grade.subject.renamedTo ?? grade.subject.name.capital(),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.w600, fontStyle: grade.subject.isRenamed && settingsProvider.renamedSubjectsItalics ? FontStyle.italic : null), style: TextStyle(
fontWeight: FontWeight.w600,
fontStyle: grade.subject.isRenamed &&
settingsProvider.renamedSubjectsItalics
? FontStyle.italic
: null),
), ),
subtitle: Text( subtitle: Text(
!Provider.of<SettingsProvider>(context, listen: false).presentationMode ? grade.teacher : "Tanár", !Provider.of<SettingsProvider>(context, listen: false)
.presentationMode
? (grade.teacher.isRenamed
? grade.teacher.renamedTo
: grade.teacher.name) ??
''
: "Tanár",
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w500), style: const TextStyle(fontWeight: FontWeight.w500),
@ -49,13 +61,20 @@ class GradeView extends StatelessWidget {
title: "value".i18n, title: "value".i18n,
description: "${grade.value.valueName} " + percentText(), description: "${grade.value.valueName} " + percentText(),
), ),
if (grade.description != "") Detail(title: "description".i18n, description: grade.description), if (grade.description != "")
if (grade.mode.description != "") Detail(title: "mode".i18n, description: grade.mode.description), Detail(title: "description".i18n, description: grade.description),
if (grade.writeDate.year != 0) Detail(title: "date".i18n, description: grade.writeDate.format(context)), if (grade.mode.description != "")
Detail(title: "mode".i18n, description: grade.mode.description),
if (grade.writeDate.year != 0)
Detail(
title: "date".i18n,
description: grade.writeDate.format(context)),
], ],
), ),
); );
} }
String percentText() => grade.value.weight != 100 && grade.value.weight > 0 ? "${grade.value.weight}%" : ""; String percentText() => grade.value.weight != 100 && grade.value.weight > 0
? "${grade.value.weight}%"
: "";
} }

View File

@ -51,10 +51,18 @@ class HomeworkView extends StatelessWidget {
homework.subject.renamedTo ?? homework.subject.name.capital(), homework.subject.renamedTo ?? homework.subject.name.capital(),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.w600, fontStyle: homework.subject.isRenamed && settingsProvider.renamedSubjectsItalics ? FontStyle.italic : null), style: TextStyle(
fontWeight: FontWeight.w600,
fontStyle: homework.subject.isRenamed &&
settingsProvider.renamedSubjectsItalics
? FontStyle.italic
: null),
), ),
subtitle: Text( subtitle: Text(
homework.teacher, (homework.teacher.isRenamed
? homework.teacher.renamedTo
: homework.teacher.name) ??
'',
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w500), style: const TextStyle(fontWeight: FontWeight.w500),

View File

@ -54,10 +54,23 @@ class LessonView extends StatelessWidget {
lesson.subject.renamedTo ?? lesson.subject.name.capital(), lesson.subject.renamedTo ?? lesson.subject.name.capital(),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.w600, fontStyle: lesson.subject.isRenamed && settingsProvider.renamedSubjectsItalics ? FontStyle.italic : null), style: TextStyle(
fontWeight: FontWeight.w600,
fontStyle: lesson.subject.isRenamed &&
settingsProvider.renamedSubjectsItalics
? FontStyle.italic
: null),
), ),
subtitle: Text( subtitle: Text(
lesson.substituteTeacher == "" ? lesson.teacher : lesson.substituteTeacher, ((lesson.substituteTeacher == null ||
lesson.substituteTeacher!.name == "")
? (lesson.teacher.isRenamed
? lesson.teacher.renamedTo
: lesson.teacher.name)
: (lesson.substituteTeacher!.isRenamed
? lesson.substituteTeacher!.renamedTo
: lesson.substituteTeacher!.name)) ??
'',
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w500), style: const TextStyle(fontWeight: FontWeight.w500),
@ -69,10 +82,18 @@ class LessonView extends StatelessWidget {
), ),
// Details // Details
if (lesson.room != "") Detail(title: "Room".i18n, description: lesson.room.replaceAll("_", " ")), if (lesson.room != "")
if (lesson.description != "") Detail(title: "Description".i18n, description: lesson.description), Detail(
if (lesson.lessonYearIndex != null) Detail(title: "Lesson Number".i18n, description: "${lesson.lessonYearIndex}."), title: "Room".i18n,
if (lesson.groupName != "") Detail(title: "Group".i18n, description: lesson.groupName), description: lesson.room.replaceAll("_", " ")),
if (lesson.description != "")
Detail(title: "Description".i18n, description: lesson.description),
if (lesson.lessonYearIndex != null)
Detail(
title: "Lesson Number".i18n,
description: "${lesson.lessonYearIndex}."),
if (lesson.groupName != "")
Detail(title: "Group".i18n, description: lesson.groupName),
], ],
), ),
); );

View File

@ -4,7 +4,8 @@ import 'package:filcnaplo_mobile_ui/common/profile_image/profile_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class NoteTile extends StatelessWidget { class NoteTile extends StatelessWidget {
const NoteTile(this.note, {Key? key, this.onTap, this.padding}) : super(key: key); const NoteTile(this.note, {Key? key, this.onTap, this.padding})
: super(key: key);
final Note note; final Note note;
final void Function()? onTap; final void Function()? onTap;
@ -20,11 +21,20 @@ class NoteTile extends StatelessWidget {
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
contentPadding: const EdgeInsets.only(left: 8.0, right: 12.0), contentPadding: const EdgeInsets.only(left: 8.0, right: 12.0),
onTap: onTap, onTap: onTap,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14.0)), shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(14.0)),
leading: ProfileImage( leading: ProfileImage(
name: note.teacher, name: (note.teacher.isRenamed
? note.teacher.renamedTo
: note.teacher.name) ??
'',
radius: 22.0, radius: 22.0,
backgroundColor: ColorUtils.stringToColor(note.teacher), backgroundColor: ColorUtils.stringToColor(
(note.teacher.isRenamed
? note.teacher.renamedTo
: note.teacher.name) ??
'',
),
), ),
title: Text( title: Text(
note.title, note.title,

View File

@ -12,7 +12,8 @@ class NoteView extends StatelessWidget {
final Note note; final Note note;
static void show(Note note, {required BuildContext context}) => showSlidingBottomSheet(context: context, child: NoteView(note)); static void show(Note note, {required BuildContext context}) =>
showSlidingBottomSheet(context: context, child: NoteView(note));
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -25,9 +26,17 @@ class NoteView extends StatelessWidget {
// Header // Header
ListTile( ListTile(
leading: ProfileImage( leading: ProfileImage(
name: note.teacher, name: (note.teacher.isRenamed
? note.teacher.renamedTo
: note.teacher.name) ??
'',
radius: 22.0, radius: 22.0,
backgroundColor: ColorUtils.stringToColor(note.teacher), backgroundColor: ColorUtils.stringToColor(
(note.teacher.isRenamed
? note.teacher.renamedTo
: note.teacher.name) ??
'',
),
), ),
title: Text( title: Text(
note.title, note.title,
@ -36,7 +45,10 @@ class NoteView extends StatelessWidget {
style: const TextStyle(fontWeight: FontWeight.w600), style: const TextStyle(fontWeight: FontWeight.w600),
), ),
subtitle: Text( subtitle: Text(
note.teacher, (note.teacher.isRenamed
? note.teacher.renamedTo
: note.teacher.name) ??
'',
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w500), style: const TextStyle(fontWeight: FontWeight.w500),

View File

@ -19,14 +19,18 @@ import 'package:filcnaplo_mobile_ui/common/widgets/absence/absence_view.i18n.dar
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class AbsenceSubjectView extends StatelessWidget { class AbsenceSubjectView extends StatelessWidget {
const AbsenceSubjectView(this.subject, {Key? key, this.absences = const []}) : super(key: key); const AbsenceSubjectView(this.subject, {Key? key, this.absences = const []})
: super(key: key);
final Subject subject; final Subject subject;
final List<Absence> absences; final List<Absence> absences;
static void show(Subject subject, List<Absence> absences, {required BuildContext context}) { static void show(Subject subject, List<Absence> absences,
{required BuildContext context}) {
Navigator.of(context, rootNavigator: true) Navigator.of(context, rootNavigator: true)
.push<Absence>(CupertinoPageRoute(builder: (context) => AbsenceSubjectView(subject, absences: absences))) .push<Absence>(CupertinoPageRoute(
builder: (context) =>
AbsenceSubjectView(subject, absences: absences)))
.then((value) { .then((value) {
if (value == null) return; if (value == null) return;
@ -36,7 +40,8 @@ class AbsenceSubjectView extends StatelessWidget {
TimetablePage.jump(context, lesson: lesson); TimetablePage.jump(context, lesson: lesson);
} else { } else {
ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar( ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar(
content: Text("Cannot find lesson".i18n, style: const TextStyle(color: Colors.white)), content: Text("Cannot find lesson".i18n,
style: const TextStyle(color: Colors.white)),
backgroundColor: AppColors.of(context).red, backgroundColor: AppColors.of(context).red,
context: context, context: context,
)); ));
@ -54,7 +59,10 @@ class AbsenceSubjectView extends StatelessWidget {
date: a.date, date: a.date,
)) ))
.toList(); .toList();
List<Widget> absenceTiles = sortDateWidgets(context, dateWidgets: dateWidgets, padding: EdgeInsets.zero, hasShadow: true); List<Widget> absenceTiles = sortDateWidgets(context,
dateWidgets: dateWidgets,
padding: const EdgeInsets.symmetric(vertical: 6.0),
hasShadow: true);
SettingsProvider settingsProvider = Provider.of<SettingsProvider>(context); SettingsProvider settingsProvider = Provider.of<SettingsProvider>(context);

View File

@ -3,6 +3,7 @@ import 'dart:math';
import 'package:filcnaplo_kreta_api/models/category.dart'; import 'package:filcnaplo_kreta_api/models/category.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart'; import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo_kreta_api/models/subject.dart'; import 'package:filcnaplo_kreta_api/models/subject.dart';
import 'package:filcnaplo_kreta_api/models/teacher.dart';
import 'package:filcnaplo_mobile_ui/common/custom_snack_bar.dart'; import 'package:filcnaplo_mobile_ui/common/custom_snack_bar.dart';
import 'package:filcnaplo_mobile_ui/common/material_action_button.dart'; import 'package:filcnaplo_mobile_ui/common/material_action_button.dart';
import 'package:filcnaplo/ui/widgets/grade/grade_tile.dart'; import 'package:filcnaplo/ui/widgets/grade/grade_tile.dart';
@ -42,7 +43,8 @@ class _GradeCalculatorState extends State<GradeCalculator> {
padding: const EdgeInsets.only(bottom: 8.0), padding: const EdgeInsets.only(bottom: 8.0),
child: Text( child: Text(
"Grade Calculator".i18n, "Grade Calculator".i18n,
style: const TextStyle(fontSize: 20.0, fontWeight: FontWeight.w600), style:
const TextStyle(fontSize: 20.0, fontWeight: FontWeight.w600),
), ),
), ),
@ -63,7 +65,9 @@ class _GradeCalculatorState extends State<GradeCalculator> {
Container( Container(
width: 80.0, width: 80.0,
padding: const EdgeInsets.only(right: 12.0), padding: const EdgeInsets.only(right: 12.0),
child: Center(child: GradeValueWidget(GradeValue(newValue.toInt(), "", "", 0))), child: Center(
child: GradeValueWidget(
GradeValue(newValue.toInt(), "", "", 0))),
), ),
]), ]),
@ -90,7 +94,8 @@ class _GradeCalculatorState extends State<GradeCalculator> {
child: Center( child: Center(
child: TextField( child: TextField(
controller: _weightController, controller: _weightController,
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 22.0), style: const TextStyle(
fontWeight: FontWeight.w600, fontSize: 22.0),
autocorrect: false, autocorrect: false,
textAlign: TextAlign.right, textAlign: TextAlign.right,
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
@ -122,7 +127,8 @@ class _GradeCalculatorState extends State<GradeCalculator> {
child: Text("Add Grade".i18n), child: Text("Add Grade".i18n),
onPressed: () { onPressed: () {
if (calculatorProvider.ghosts.length >= 50) { if (calculatorProvider.ghosts.length >= 50) {
ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar(content: Text("limit_reached".i18n), context: context)); ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar(
content: Text("limit_reached".i18n), context: context));
return; return;
} }
@ -133,7 +139,11 @@ class _GradeCalculatorState extends State<GradeCalculator> {
grades.sort((a, b) => -a.writeDate.compareTo(b.writeDate)); grades.sort((a, b) => -a.writeDate.compareTo(b.writeDate));
date = grades.first.date.add(const Duration(days: 7)); date = grades.first.date.add(const Duration(days: 7));
} else { } else {
List<Grade> grades = calculatorProvider.grades.where((e) => e.type == GradeType.midYear && e.subject == widget.subject).toList(); List<Grade> grades = calculatorProvider.grades
.where((e) =>
e.type == GradeType.midYear &&
e.subject == widget.subject)
.toList();
grades.sort((a, b) => -a.writeDate.compareTo(b.writeDate)); grades.sort((a, b) => -a.writeDate.compareTo(b.writeDate));
date = grades.first.date; date = grades.first.date;
} }
@ -143,8 +153,9 @@ class _GradeCalculatorState extends State<GradeCalculator> {
date: date, date: date,
writeDate: date, writeDate: date,
description: "Ghost Grade".i18n, description: "Ghost Grade".i18n,
value: GradeValue(newValue.toInt(), "", "", newWeight.toInt()), value:
teacher: "Ghost", GradeValue(newValue.toInt(), "", "", newWeight.toInt()),
teacher: Teacher.fromString("Ghost"),
type: GradeType.ghost, type: GradeType.ghost,
form: "", form: "",
subject: widget.subject, subject: widget.subject,

View File

@ -21,6 +21,10 @@ import 'package:filcnaplo_mobile_ui/pages/grades/calculator/grade_calculator_pro
import 'package:filcnaplo_mobile_ui/pages/grades/grades_count.dart'; import 'package:filcnaplo_mobile_ui/pages/grades/grades_count.dart';
import 'package:filcnaplo_mobile_ui/pages/grades/graph.dart'; import 'package:filcnaplo_mobile_ui/pages/grades/graph.dart';
import 'package:filcnaplo_mobile_ui/pages/grades/subject_grades_container.dart'; import 'package:filcnaplo_mobile_ui/pages/grades/subject_grades_container.dart';
import 'package:filcnaplo_premium/ui/mobile/goal_planner/test.dart';
import 'package:filcnaplo_premium/models/premium_scopes.dart';
import 'package:filcnaplo_premium/providers/premium_provider.dart';
import 'package:filcnaplo_premium/ui/mobile/premium/upsell.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart'; import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
@ -215,25 +219,25 @@ class _GradeSubjectViewState extends State<GradeSubjectView> {
gradeCalc(context); gradeCalc(context);
}, },
), ),
// FloatingActionButton.small( FloatingActionButton.small(
// child: const Icon(FeatherIcons.flag, size: 20.0), child: const Icon(FeatherIcons.flag, size: 20.0),
// backgroundColor: Theme.of(context).colorScheme.secondary, backgroundColor: Theme.of(context).colorScheme.secondary,
// onPressed: () { onPressed: () {
// if (!Provider.of<PremiumProvider>(context, listen: false) if (!Provider.of<PremiumProvider>(context, listen: false)
// .hasScope(PremiumScopes.goalPlanner)) { .hasScope(PremiumScopes.goalPlanner)) {
// PremiumLockedFeatureUpsell.show( PremiumLockedFeatureUpsell.show(
// context: context, feature: PremiumFeature.goalplanner); context: context, feature: PremiumFeature.goalplanner);
// return; return;
// } }
// ScaffoldMessenger.of(context).showSnackBar( // ScaffoldMessenger.of(context).showSnackBar(
// const SnackBar(content: Text("Hamarosan..."))); // const SnackBar(content: Text("Hamarosan...")));
// Navigator.of(context).push(CupertinoPageRoute( Navigator.of(context).push(CupertinoPageRoute(
// builder: (context) => PremiumGoalplannerNewGoalScreen( builder: (context) =>
// subject: widget.subject))); GoalPlannerTest(subject: widget.subject)));
// }, },
// ), ),
], ],
), ),
), ),
@ -263,7 +267,8 @@ class _GradeSubjectViewState extends State<GradeSubjectView> {
subject: widget.subject, context: context), subject: widget.subject, context: context),
scrollController: _scrollController, scrollController: _scrollController,
title: widget.subject.renamedTo ?? widget.subject.name.capital(), title: widget.subject.renamedTo ?? widget.subject.name.capital(),
italic: settingsProvider.renamedSubjectsItalics && widget.subject.isRenamed, italic: settingsProvider.renamedSubjectsItalics &&
widget.subject.isRenamed,
child: SubjectGradesContainer( child: SubjectGradesContainer(
child: CupertinoScrollbar( child: CupertinoScrollbar(
child: ListView.builder( child: ListView.builder(

View File

@ -356,14 +356,14 @@ class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
// confetti 🎊 // confetti 🎊
if (_confettiController != null) if (_confettiController != null)
Align( Align(
alignment: Alignment.bottomCenter, alignment: Alignment.topCenter,
child: ConfettiWidget( child: ConfettiWidget(
confettiController: _confettiController!, confettiController: _confettiController!,
blastDirection: -pi / 2, blastDirectionality: BlastDirectionality.explosive,
emissionFrequency: 0.01, emissionFrequency: 0.02,
numberOfParticles: 80, numberOfParticles: 120,
maxBlastForce: 100, maxBlastForce: 20,
minBlastForce: 90, minBlastForce: 10,
gravity: 0.3, gravity: 0.3,
minimumSize: const Size(5, 5), minimumSize: const Size(5, 5),
maximumSize: const Size(20, 20), maximumSize: const Size(20, 20),

View File

@ -139,7 +139,6 @@ class NavigationScreenState extends State<NavigationScreen>
void initState() { void initState() {
super.initState(); super.initState();
HomeWidget.setAppGroupId('hu.refilc.naplo.group'); HomeWidget.setAppGroupId('hu.refilc.naplo.group');
_checkForWidgetLaunch(); _checkForWidgetLaunch();
@ -199,9 +198,9 @@ class NavigationScreenState extends State<NavigationScreen>
// Show news // Show news
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (newsProvider.show) { if (newsProvider.show) {
newsProvider.lock(); NewsView.show(newsProvider.news[0], context: context)
NewsView.show(newsProvider.news[newsProvider.state], context: context)
.then((value) => newsProvider.release()); .then((value) => newsProvider.release());
newsProvider.lock();
} }
}); });

View File

@ -31,23 +31,30 @@ class NewsView extends StatelessWidget {
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.only(left: 8.0, right: 6.0, top: 14.0, bottom: 8.0), padding: const EdgeInsets.only(
left: 8.0, right: 6.0, top: 14.0, bottom: 8.0),
child: Text( child: Text(
news.title, news.title,
maxLines: 3, maxLines: 3,
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 18.0), style: const TextStyle(
fontWeight: FontWeight.w600, fontSize: 18.0),
), ),
), ),
SelectableLinkify( SelectableLinkify(
text: news.content.escapeHtml(), text: news.content.escapeHtml(),
options: const LinkifyOptions(looseUrl: true, removeWww: true), options:
const LinkifyOptions(looseUrl: true, removeWww: true),
onOpen: (link) { onOpen: (link) {
launch( launch(
link.url, link.url,
customTabsOption: CustomTabsOption(showPageTitle: true, toolbarColor: Theme.of(context).scaffoldBackgroundColor), customTabsOption: CustomTabsOption(
showPageTitle: true,
toolbarColor:
Theme.of(context).scaffoldBackgroundColor),
); );
}, },
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 14.0), style: const TextStyle(
fontWeight: FontWeight.w500, fontSize: 14.0),
), ),
], ],
), ),
@ -59,10 +66,15 @@ class NewsView extends StatelessWidget {
children: [ children: [
if (news.link != "") if (news.link != "")
DialogButton( DialogButton(
label: news.openLabel != "" ? news.openLabel : "open".i18n.toUpperCase(), label: news.openLabel != ""
? news.openLabel
: "open".i18n.toUpperCase(),
onTap: () => launch( onTap: () => launch(
news.link, news.link,
customTabsOption: CustomTabsOption(showPageTitle: true, toolbarColor: Theme.of(context).scaffoldBackgroundColor), customTabsOption: CustomTabsOption(
showPageTitle: true,
toolbarColor:
Theme.of(context).scaffoldBackgroundColor),
), ),
), ),
DialogButton( DialogButton(
@ -78,12 +90,15 @@ class NewsView extends StatelessWidget {
); );
} }
static Future<T?> show<T>(News news, {required BuildContext context, bool force = false}) { static Future<T?> show<T>(News news,
{required BuildContext context, bool force = false}) {
if (news.title == "") return Future<T?>.value(null); if (news.title == "") return Future<T?>.value(null);
bool popup = news.platform == '' || force; bool popup = news.platform == 'all' || force;
if (Provider.of<SettingsProvider>(context, listen: false).newsEnabled || news.emergency || force) { if (Provider.of<SettingsProvider>(context, listen: false).newsEnabled ||
news.emergency ||
force) {
switch (news.platform.trim().toLowerCase()) { switch (news.platform.trim().toLowerCase()) {
case "android": case "android":
if (Platform.isAndroid) popup = true; if (Platform.isAndroid) popup = true;
@ -100,6 +115,9 @@ class NewsView extends StatelessWidget {
case "macos": case "macos":
if (Platform.isMacOS) popup = true; if (Platform.isMacOS) popup = true;
break; break;
case "all":
popup = true;
break;
default: default:
popup = true; popup = true;
} }
@ -108,7 +126,10 @@ class NewsView extends StatelessWidget {
} }
if (popup) { if (popup) {
return showDialog<T?>(context: context, builder: (context) => NewsView(news), barrierDismissible: true); return showDialog<T?>(
context: context,
builder: (context) => NewsView(news),
barrierDismissible: true);
} else { } else {
return Future<T?>.value(null); return Future<T?>.value(null);
} }

View File

@ -28,26 +28,21 @@ class PrivacyView extends StatelessWidget {
), ),
SelectableLinkify( SelectableLinkify(
text: """ text: """
A reFilc egy kliensalkalmazás, segítségével az e-Kréta rendszeréből letöltheted és felhasználóbarát módon megjelenítheted az adataidat. A reFilc (továbbiakban alkalmazás) egy mobilos, asztali és webes kliensalkalmazás, segítségével az e-Kréta rendszeréből letöltheted és felhasználóbarát módon megjelenítheted az adataidat. Tanulmányi adataid csak közvetlenül az alkalmazás és a Kréta-szerverek között közlekednek, titkosított kapcsolaton keresztül.
Tanulmányi adataid csak közvetlenül az alkalmazás és a Kréta-szerverek között közlekednek, titkosított kapcsolaton keresztül.
A reFilc fejlesztői és üzemeltetői a tanulmányi adataidat semmilyen célból nem másolják, nem tárolják és harmadik félnek nem továbbítják. Ezeket így az e-Kréta Informatikai Zrt. kezeli, az ő tájékoztatójukat itt találod: https://tudasbazis.ekreta.hu/pages/viewpage.action?pageId=4065038. A reFilc fejlesztői és/vagy üzemeltetői, valamint az alkalmazás a tanulmányi és személyes adataidat semmilyen célból és semmilyen körülmények között nem másolják, nem tárolják és harmadik félnek nem továbbítják. Ezeket így az Educational Development Informatikai Zrt. kezeli, az Ő adatkezeléssel kapcsolatos tájékoztatójukat itt találod: https://tudasbazis.ekreta.hu/pages/viewpage.action?pageId=4065038
Azok törlésével vagy módosítával kapcsolatban keresd az osztályfőnöködet vagy az iskolád rendszergazdáját.
Az alkalmazás névtelen használati statisztikákat gyűjt, ezek alapján tudjuk meghatározni a felhasználók és a telepítések számát. Ezt a beállításokban kikapcsolhatod. Azok törlésével vagy módosítával kapcsolatban keresd az osztályfőnöködet vagy az iskolád rendszergazdáját.
Kérünk, hogy ha csak teheted, hagyd ezt a funkciót bekapcsolva.
Amikor az alkalmazás hibába ütközik, lehetőség van hibajelentés küldésére. Az alkalmazás névtelen használati statisztikákat gyűjt, ezek alapján tudjuk meghatározni a felhasználók és a telepítések számát, valamint az eszközük platformját. Ezt a beállításokban kikapcsolhatod. Kérünk, hogy ha csak teheted, hagyd ezt a funkciót bekapcsolva, hogy pontosabb információnk legyen a felhasználóink platform-megoszlásáról.
Ez személyes- vagy tanulmányi adatokat nem tartalmaz, viszont részletes információval szolgál a hibáról és eszközödről.
A küldés előtt megjelenő képernyőn a te felelősséged átnézni a továbbításra kerülő adatsort.
A hibajelentéseket a reFilc fejlesztői felületén és egy privát Discord szobában tároljuk, ezekhez csak az app fejlesztői férnek hozzá.
Az alkalmazás belépéskor a GitHub API segítségével ellenőrzi, hogy elérhető-e új verzió, és kérésre innen is tölti le a telepítőt.
Ha az adataiddal kapcsolatban bármilyen kérdésed van (törlés, módosítás, adathordozás), keress minket a filcnaplo@filcnaplo.hu címen. Amikor az alkalmazás hibába ütközik, lehetőség van hibajelentés küldésére. Ez személyes- és/vagy tanulmányi adatokat nem tartalmaz, viszont részletes információval szolgál a hibáról, annak okáról és eszközödről. A küldés előtt megjelenő képernyőn a te felelősséged átnézni a továbbításra kerülő adatsort. A hibajelentéseket a reFilc fejlesztői felületén és egy privát Discord szobában tároljuk, ezekhez csak az app fejlesztői férnek hozzá.
Az alkalmazás használatával jelzed, hogy ezt a tájékoztatót tudomásul vetted. Az alkalmazás (az alábbi platformokon: Android, Linux, Windows) minden egyes indításakor a reFilc API, valamint a Github API segítségével ellenőrzi, hogy elérhető-e új verzió, és kérésre innen letölti és telepíti a frissítést.
Utolsó módosítás: 2023. 05. 28. Amennyiben az adataiddal kapcsolatban bármilyen kérdésed van (megtekintés, törlés, módosítás, adathordozás), keress minket a social@refilc.hu e-mail címen, vagy Discord szerverünkön!
A kliensalkalmazás bármely eszközön és platformon történő használatával tudomásul vetted és elfogadod a jelen adatkezelési tájékoztatót. A reFilc csapata fenntartja a jogot a tájékoztató módosítására és a módosításokról nem köteles értesíteni a felhasználóit!
""", """,
onOpen: (link) => launch(link.url, onOpen: (link) => launch(link.url,
customTabsOption: CustomTabsOption( customTabsOption: CustomTabsOption(

View File

@ -15,6 +15,7 @@ import 'package:filcnaplo/models/user.dart';
import 'package:filcnaplo/theme/colors/colors.dart'; import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo_kreta_api/client/client.dart'; import 'package:filcnaplo_kreta_api/client/client.dart';
import 'package:filcnaplo_mobile_ui/common/action_button.dart'; import 'package:filcnaplo_mobile_ui/common/action_button.dart';
import 'package:filcnaplo_mobile_ui/common/beta_chip.dart';
import 'package:filcnaplo_mobile_ui/common/bottom_sheet_menu/bottom_sheet_menu.dart'; import 'package:filcnaplo_mobile_ui/common/bottom_sheet_menu/bottom_sheet_menu.dart';
import 'package:filcnaplo_mobile_ui/common/bottom_sheet_menu/bottom_sheet_menu_item.dart'; import 'package:filcnaplo_mobile_ui/common/bottom_sheet_menu/bottom_sheet_menu_item.dart';
import 'package:filcnaplo_mobile_ui/common/panel/panel.dart'; import 'package:filcnaplo_mobile_ui/common/panel/panel.dart';
@ -41,6 +42,7 @@ import 'package:filcnaplo_premium/ui/mobile/settings/nickname.dart';
import 'package:filcnaplo_premium/ui/mobile/settings/profile_pic.dart'; import 'package:filcnaplo_premium/ui/mobile/settings/profile_pic.dart';
import 'package:filcnaplo_premium/ui/mobile/settings/icon_pack.dart'; import 'package:filcnaplo_premium/ui/mobile/settings/icon_pack.dart';
import 'package:filcnaplo_premium/ui/mobile/settings/modify_subject_names.dart'; import 'package:filcnaplo_premium/ui/mobile/settings/modify_subject_names.dart';
import 'package:filcnaplo_premium/ui/mobile/settings/modify_teacher_names.dart';
class SettingsScreen extends StatefulWidget { class SettingsScreen extends StatefulWidget {
const SettingsScreen({Key? key}) : super(key: key); const SettingsScreen({Key? key}) : super(key: key);
@ -156,7 +158,8 @@ class _SettingsScreenState extends State<SettingsScreen>
void initState() { void initState() {
super.initState(); super.initState();
Future.delayed(Duration.zero, () { Future.delayed(Duration.zero, () {
futureRelease = Provider.of<UpdateProvider>(context, listen: false).installedVersion(); futureRelease = Provider.of<UpdateProvider>(context, listen: false)
.installedVersion();
}); });
_hideContainersController = AnimationController( _hideContainersController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 200)); vsync: this, duration: const Duration(milliseconds: 200));
@ -383,7 +386,10 @@ class _SettingsScreenState extends State<SettingsScreen>
}, },
title: Text("language".i18n), title: Text("language".i18n),
leading: const Icon(FeatherIcons.globe), leading: const Icon(FeatherIcons.globe),
trailing: Text(languageText), trailing: Text(
languageText,
style: const TextStyle(fontSize: 14.0),
),
), ),
PanelButton( PanelButton(
onPressed: () { onPressed: () {
@ -392,7 +398,10 @@ class _SettingsScreenState extends State<SettingsScreen>
}, },
title: Text("startpage".i18n), title: Text("startpage".i18n),
leading: const Icon(FeatherIcons.play), leading: const Icon(FeatherIcons.play),
trailing: Text(startPageTitle.capital()), trailing: Text(
startPageTitle.capital(),
style: const TextStyle(fontSize: 14.0),
),
), ),
PanelButton( PanelButton(
onPressed: () { onPressed: () {
@ -401,8 +410,10 @@ class _SettingsScreenState extends State<SettingsScreen>
}, },
title: Text("rounding".i18n), title: Text("rounding".i18n),
leading: const Icon(FeatherIcons.gitCommit), leading: const Icon(FeatherIcons.gitCommit),
trailing: trailing: Text(
Text((settings.rounding / 10).toStringAsFixed(1)), (settings.rounding / 10).toStringAsFixed(1),
style: const TextStyle(fontSize: 14.0),
),
), ),
PanelButton( PanelButton(
onPressed: () { onPressed: () {
@ -411,7 +422,10 @@ class _SettingsScreenState extends State<SettingsScreen>
}, },
title: Text("vibrate".i18n), title: Text("vibrate".i18n),
leading: const Icon(FeatherIcons.radio), leading: const Icon(FeatherIcons.radio),
trailing: Text(vibrateTitle), trailing: Text(
vibrateTitle,
style: const TextStyle(fontSize: 14.0),
),
), ),
PanelButton( PanelButton(
padding: const EdgeInsets.only(left: 14.0), padding: const EdgeInsets.only(left: 14.0),
@ -445,9 +459,14 @@ class _SettingsScreenState extends State<SettingsScreen>
contentPadding: const EdgeInsets.only(left: 12.0), contentPadding: const EdgeInsets.only(left: 12.0),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0)), borderRadius: BorderRadius.circular(12.0)),
title: Row(children: [ title: Row(
children: [
Icon(FeatherIcons.messageSquare, Icon(FeatherIcons.messageSquare,
color: settings.notificationsEnabled ? Theme.of(context).colorScheme.secondary : AppColors.of(context).text.withOpacity(.25)), color: settings.notificationsEnabled
? Theme.of(context).colorScheme.secondary
: AppColors.of(context)
.text
.withOpacity(.25)),
const SizedBox(width: 14.0), const SizedBox(width: 14.0),
Text( Text(
"notifications".i18n, "notifications".i18n,
@ -461,34 +480,11 @@ class _SettingsScreenState extends State<SettingsScreen>
const SizedBox( const SizedBox(
width: 5, width: 5,
), ),
SizedBox( BetaChip(
height: 30, disabled: !settings.notificationsEnabled,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
child: Padding(
padding:
const EdgeInsets.only(left: 10, right: 10),
child: Center(
child: Text("BETA",
style: TextStyle(
fontSize: 9.1,
color: AppColors.of(context)
.text
.withOpacity(
settings.notificationsEnabled
? 1.0
: .5),
fontWeight: FontWeight.w600,
overflow: TextOverflow.ellipsis))),
), ),
decoration: BoxDecoration( ],
color: settings.notificationsEnabled
? Theme.of(context).colorScheme.secondary
: AppColors.of(context).text.withOpacity(.25),
borderRadius: BorderRadius.circular(40)),
), ),
)
]),
onChanged: (value) => onChanged: (value) =>
settings.update(notificationsEnabled: value), settings.update(notificationsEnabled: value),
), ),
@ -636,7 +632,10 @@ class _SettingsScreenState extends State<SettingsScreen>
}, },
title: Text("theme".i18n), title: Text("theme".i18n),
leading: const Icon(FeatherIcons.sun), leading: const Icon(FeatherIcons.sun),
trailing: Text(themeModeText), trailing: Text(
themeModeText,
style: const TextStyle(fontSize: 14.0),
),
), ),
PanelButton( PanelButton(
onPressed: () async { onPressed: () async {
@ -805,6 +804,9 @@ class _SettingsScreenState extends State<SettingsScreen>
MenuRenamedSubjects( MenuRenamedSubjects(
settings: settings, settings: settings,
), ),
MenuRenamedTeachers(
settings: settings,
),
], ],
), ),
), ),
@ -817,6 +819,19 @@ class _SettingsScreenState extends State<SettingsScreen>
child: Panel( child: Panel(
title: Text("about".i18n), title: Text("about".i18n),
child: Column(children: [ child: Column(children: [
PanelButton(
leading: const Icon(FeatherIcons.mail),
title: Text("news".i18n),
onPressed: () => _openNews(context),
),
PanelButton(
leading: const Icon(FeatherIcons.lock),
title: Text("privacy".i18n),
// onPressed: () => launchUrl(
// Uri.parse("https://refilc.hu/privacy-policy"),
// mode: LaunchMode.inAppWebView),
onPressed: () => _openPrivacy(context),
),
PanelButton( PanelButton(
leading: const Icon(FeatherIcons.atSign), leading: const Icon(FeatherIcons.atSign),
title: const Text("Discord"), title: const Text("Discord"),
@ -838,16 +853,6 @@ class _SettingsScreenState extends State<SettingsScreen>
Uri.parse("https://github.com/refilc"), Uri.parse("https://github.com/refilc"),
mode: LaunchMode.externalApplication), mode: LaunchMode.externalApplication),
), ),
PanelButton(
leading: const Icon(FeatherIcons.mail),
title: Text("news".i18n),
onPressed: () => _openNews(context),
),
PanelButton(
leading: const Icon(FeatherIcons.lock),
title: Text("privacy".i18n),
onPressed: () => _openPrivacy(context),
),
PanelButton( PanelButton(
leading: const Icon(FeatherIcons.award), leading: const Icon(FeatherIcons.award),
title: Text("licenses".i18n), title: Text("licenses".i18n),
@ -923,7 +928,8 @@ class _SettingsScreenState extends State<SettingsScreen>
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0)), borderRadius: BorderRadius.circular(12.0)),
title: Text("devmode".i18n, title: Text("devmode".i18n,
style: const TextStyle(fontWeight: FontWeight.w500)), style:
const TextStyle(fontWeight: FontWeight.w500)),
onChanged: (v) => onChanged: (v) =>
settings.update(developerMode: false), settings.update(developerMode: false),
value: settings.developerMode, value: settings.developerMode,
@ -998,8 +1004,8 @@ class _SettingsScreenState extends State<SettingsScreen>
if (devmodeCountdown > 0) { if (devmodeCountdown > 0) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
content: Text( content:
"devmoretaps".i18n.fill([devmodeCountdown])), Text("devmoretaps".i18n.fill([devmodeCountdown])),
)); ));
setState(() => devmodeCountdown--); setState(() => devmodeCountdown--);

View File

@ -163,7 +163,7 @@ extension SettingsLocalization on String {
"theme": "Thema", "theme": "Thema",
"color": "Farbe", "color": "Farbe",
"grade_colors": "Grad Farben", "grade_colors": "Grad Farben",
"notifications": "Benachrichtigungen", "notifications": "Mitteilung",
"news": "Nachrichten", "news": "Nachrichten",
"extras": "Extras", "extras": "Extras",
"about": "Informationen", "about": "Informationen",

View File

@ -1,52 +0,0 @@
name: filcnaplo_mobile_ui
publish_to: "none"
environment:
sdk: ">=2.17.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
# Filcnaplo main dep
filcnaplo:
path: ../filcnaplo/
filcnaplo_kreta_api:
path: ../filcnaplo_kreta_api/
filcnaplo_premium:
path: ../filcnaplo_premium/
flutter_feather_icons: ^2.0.0+1
provider: ^5.0.0
fl_chart: ^0.45.1
url_launcher: ^6.0.9
flutter_material_color_picker: ^1.1.0+2
photo_view: ^0.14.0
flutter_linkify: ^5.0.2
flutter_custom_tabs: ^1.0.3
flutter_markdown: ^0.6.5
animations: ^2.0.1
animated_list_plus: ^0.5.0
confetti: ^0.6.0
live_activities: ^1.0.0
animated_flip_counter: ^0.2.5
lottie: ^1.4.3
rive: ^0.9.1
animated_background: ^2.0.0
home_widget: ^0.1.6
dropdown_button2: ^1.8.9
flutter_svg: ^1.1.6
background_fetch: ^1.1.5
wtf_sliding_sheet: ^1.0.0
package_info_plus: ^4.0.2
dotted_border: ^2.0.0+3
screenshot: ^2.1.0
image_gallery_saver: ^2.0.2
rounded_expansion_tile: ^0.0.13
dev_dependencies:
flutter_lints: ^1.0.0
flutter:
uses-material-design: true

View File

@ -18,6 +18,7 @@ class PremiumScopes {
/// Modify subject names /// Modify subject names
static const renameSubjects = "filc.premium.RENAME_SUBJECTS"; static const renameSubjects = "filc.premium.RENAME_SUBJECTS";
static const renameTeachers = "filc.premium.RENAME_TEACHERS";
/// Tinta /// Tinta

View File

@ -1,8 +1,15 @@
import 'package:filcnaplo/models/settings.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
class GoalInput extends StatelessWidget { class GoalInput extends StatelessWidget {
const GoalInput({Key? key, required this.currentAverage, required this.value, required this.onChanged}) : super(key: key); const GoalInput(
{Key? key,
required this.currentAverage,
required this.value,
required this.onChanged})
: super(key: key);
final double currentAverage; final double currentAverage;
final double value; final double value;
@ -24,6 +31,8 @@ class GoalInput extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
SettingsProvider settings = Provider.of<SettingsProvider>(context);
List<int> presets = [2, 3, 4, 5]; List<int> presets = [2, 3, 4, 5];
presets = presets.where((e) => gradeToAvg(e) > currentAverage).toList(); presets = presets.where((e) => gradeToAvg(e) > currentAverage).toList();
@ -44,7 +53,8 @@ class GoalInput extends StatelessWidget {
child: Padding( child: Padding(
padding: const EdgeInsets.only(right: 20.0), padding: const EdgeInsets.only(right: 20.0),
child: CustomPaint( child: CustomPaint(
painter: GoalSliderPainter(value: (value - 1) / 4), painter: GoalSliderPainter(
value: (value - 1) / 4, settings: settings),
), ),
), ),
), ),
@ -61,8 +71,9 @@ class GoalInput extends StatelessWidget {
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(99.0), borderRadius: BorderRadius.circular(99.0),
color: gradeColor(e).withOpacity(selected ? 1.0 : 0.2), color:
border: Border.all(color: gradeColor(e), width: 4), gradeColor(e, settings).withOpacity(selected ? 1.0 : 0.2),
border: Border.all(color: gradeColor(e, settings), width: 4),
), ),
child: Material( child: Material(
type: MaterialType.transparency, type: MaterialType.transparency,
@ -70,11 +81,13 @@ class GoalInput extends StatelessWidget {
borderRadius: BorderRadius.circular(99.0), borderRadius: BorderRadius.circular(99.0),
onTap: () => setValue(gradeToAvg(e)), onTap: () => setValue(gradeToAvg(e)),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 24.0), padding: const EdgeInsets.symmetric(
vertical: 2.0, horizontal: 24.0),
child: Text( child: Text(
e.toString(), e.toString(),
style: TextStyle( style: TextStyle(
color: selected ? Colors.white : gradeColor(e), color:
selected ? Colors.white : gradeColor(e, settings),
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 24.0, fontSize: 24.0,
), ),
@ -93,8 +106,9 @@ class GoalInput extends StatelessWidget {
class GoalSliderPainter extends CustomPainter { class GoalSliderPainter extends CustomPainter {
final double value; final double value;
final SettingsProvider settings;
GoalSliderPainter({required this.value}); GoalSliderPainter({required this.value, required this.settings});
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
@ -115,21 +129,24 @@ class GoalSliderPainter extends CustomPainter {
const Radius.circular(99.0), const Radius.circular(99.0),
), ),
Paint() Paint()
..shader = const LinearGradient(colors: [ ..shader = LinearGradient(colors: [
Color(0xffFF3B30), settings.gradeColors[0],
Color(0xffFF9F0A), settings.gradeColors[1],
Color(0xffFFD60A), settings.gradeColors[2],
Color(0xff34C759), settings.gradeColors[3],
Color(0xff247665), settings.gradeColors[4],
]).createShader(rect), ]).createShader(rect),
); );
canvas.drawOval( canvas.drawOval(
Rect.fromCircle(center: Offset(size.width * value, size.height / 2), radius: radius - cpadding), Rect.fromCircle(
center: Offset(size.width * value, size.height / 2),
radius: radius - cpadding),
Paint()..color = Colors.white, Paint()..color = Colors.white,
); );
for (int i = 1; i < 4; i++) { for (int i = 1; i < 4; i++) {
canvas.drawOval( canvas.drawOval(
Rect.fromCircle(center: Offset(size.width / 4 * i, size.height / 2), radius: 4), Rect.fromCircle(
center: Offset(size.width / 4 * i, size.height / 2), radius: 4),
Paint()..color = Colors.white.withOpacity(.5), Paint()..color = Colors.white.withOpacity(.5),
); );
} }
@ -145,12 +162,19 @@ double gradeToAvg(int grade) {
return grade - 0.5; return grade - 0.5;
} }
Color gradeColor(int grade) { Color gradeColor(int grade, SettingsProvider settings) {
// return [
// const Color(0xffFF3B30),
// const Color(0xffFF9F0A),
// const Color(0xffFFD60A),
// const Color(0xff34C759),
// const Color(0xff247665),
// ].elementAt(grade.clamp(1, 5) - 1);
return [ return [
const Color(0xffFF3B30), settings.gradeColors[0],
const Color(0xffFF9F0A), settings.gradeColors[1],
const Color(0xffFFD60A), settings.gradeColors[2],
const Color(0xff34C759), settings.gradeColors[3],
const Color(0xff247665), settings.gradeColors[4],
].elementAt(grade.clamp(1, 5) - 1); ].elementAt(grade.clamp(1, 5) - 1);
} }

View File

@ -13,6 +13,7 @@ import 'dart:math';
import 'package:filcnaplo_kreta_api/models/category.dart'; import 'package:filcnaplo_kreta_api/models/category.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart'; import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo_kreta_api/models/subject.dart'; import 'package:filcnaplo_kreta_api/models/subject.dart';
import 'package:filcnaplo_kreta_api/models/teacher.dart';
import 'package:flutter/foundation.dart' show listEquals; import 'package:flutter/foundation.dart' show listEquals;
/// Generate list of grades that achieve the wanted goal. /// Generate list of grades that achieve the wanted goal.
@ -46,7 +47,8 @@ class GoalPlanner {
for (int i = g.max; i >= 0; i--) { for (int i = g.max; i >= 0; i--) {
int newGradeToAdd = g.gradeToAdd - 1; int newGradeToAdd = g.gradeToAdd - 1;
List<int> newPlan = GoalPlannerHelper._addToList<int>(g.plan, g.gradeToAdd, i); List<int> newPlan =
GoalPlannerHelper._addToList<int>(g.plan, g.gradeToAdd, i);
Avg newAvg = GoalPlannerHelper._addToAvg(g.currentAvg, g.gradeToAdd, i); Avg newAvg = GoalPlannerHelper._addToAvg(g.currentAvg, g.gradeToAdd, i);
int newN = GoalPlannerHelper.howManyNeeded( int newN = GoalPlannerHelper.howManyNeeded(
@ -57,7 +59,7 @@ class GoalPlanner {
id: '', id: '',
date: DateTime(0), date: DateTime(0),
value: GradeValue(e, '', '', 100), value: GradeValue(e, '', '', 100),
teacher: '', teacher: Teacher.fromString(''),
description: '', description: '',
form: '', form: '',
groupId: '', groupId: '',
@ -83,7 +85,8 @@ class GoalPlanner {
grades, grades,
goal, goal,
), ),
Avg(GoalPlannerHelper.averageEvals(grades), GoalPlannerHelper.weightSum(grades)), Avg(GoalPlannerHelper.averageEvals(grades),
GoalPlannerHelper.weightSum(grades)),
[], [],
), ),
); );
@ -92,7 +95,9 @@ class GoalPlanner {
for (var e in plans) { for (var e in plans) {
e.sum = e.plan.fold(0, (int a, b) => a + b); e.sum = e.plan.fold(0, (int a, b) => a + b);
e.avg = e.sum / e.plan.length; e.avg = e.sum / e.plan.length;
e.sigma = sqrt(e.plan.map((i) => pow(i - e.avg, 2)).fold(0, (num a, b) => a + b) / e.plan.length); e.sigma = sqrt(
e.plan.map((i) => pow(i - e.avg, 2)).fold(0, (num a, b) => a + b) /
e.plan.length);
} }
// filter without aggression // filter without aggression
@ -141,7 +146,8 @@ class Plan {
} }
class GoalPlannerHelper { class GoalPlannerHelper {
static Avg _addToAvg(Avg base, int grade, int n) => Avg((base.avg * base.n + grade * n) / (base.n + n), base.n + n); static Avg _addToAvg(Avg base, int grade, int n) =>
Avg((base.avg * base.n + grade * n) / (base.n + n), base.n + n);
static List<T> _addToList<T>(List<T> l, T e, int n) { static List<T> _addToList<T>(List<T> l, T e, int n) {
if (n == 0) return l; if (n == 0) return l;
@ -158,15 +164,20 @@ class GoalPlannerHelper {
if (avg >= goal) return 0; if (avg >= goal) return 0;
if (grade * 1.0 == goal) return -1; if (grade * 1.0 == goal) return -1;
int candidate = (wsum * (avg - goal) / (goal - grade)).floor(); int candidate = (wsum * (avg - goal) / (goal - grade)).floor();
return (candidate * grade + avg * wsum) / (candidate + wsum) < goal ? candidate + 1 : candidate; return (candidate * grade + avg * wsum) / (candidate + wsum) < goal
? candidate + 1
: candidate;
} }
static double averageEvals(List<Grade> grades, {bool finalAvg = false}) { static double averageEvals(List<Grade> grades, {bool finalAvg = false}) {
double average = double average = grades
grades.map((e) => e.value.value * e.value.weight / 100.0).fold(0.0, (double a, double b) => a + b) / weightSum(grades, finalAvg: finalAvg); .map((e) => e.value.value * e.value.weight / 100.0)
.fold(0.0, (double a, double b) => a + b) /
weightSum(grades, finalAvg: finalAvg);
return average.isNaN ? 0.0 : average; return average.isNaN ? 0.0 : average;
} }
static double weightSum(List<Grade> grades, {bool finalAvg = false}) => static double weightSum(List<Grade> grades, {bool finalAvg = false}) => grades
grades.map((e) => finalAvg ? 1 : e.value.weight / 100).fold(0, (a, b) => a + b); .map((e) => finalAvg ? 1 : e.value.weight / 100)
.fold(0, (a, b) => a + b);
} }

View File

@ -0,0 +1,39 @@
import 'package:i18n_extension/i18n_extension.dart';
extension Localization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"goal_planner_title": "Goal Planning",
"set_a_goal": "Your Goal",
"select_subject": "Subject",
"pick_route": "Pick a Route",
"track_it": "Track it!",
"recommended": "Recommended",
"fastest": "Fastest",
},
"hu_hu": {
"goal_planner_title": "Cél követés",
"set_a_goal": "Kitűzött cél",
"select_subject": "Tantárgy",
"pick_route": "Válassz egy utat",
"track_it": "Követés!",
"recommended": "Ajánlott",
"fastest": "Leggyorsabb",
},
"de_de": {
"goal_planner_title": "Zielplanung",
"set_a_goal": "Dein Ziel",
"select_subject": "Thema",
"pick_route": "Wähle einen Weg",
"track_it": "Verfolge es!",
"recommended": "Empfohlen",
"fastest": "Am schnellsten",
},
};
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,5 +1,7 @@
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_input.dart'; import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_input.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class GradeDisplay extends StatelessWidget { class GradeDisplay extends StatelessWidget {
const GradeDisplay({Key? key, required this.grade}) : super(key: key); const GradeDisplay({Key? key, required this.grade}) : super(key: key);
@ -8,12 +10,14 @@ class GradeDisplay extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
SettingsProvider settings = Provider.of<SettingsProvider>(context);
return Container( return Container(
width: 36, width: 36,
height: 36, height: 36,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: gradeColor(grade).withOpacity(.3), color: gradeColor(grade, settings).withOpacity(.3),
), ),
child: Center( child: Center(
child: Text( child: Text(
@ -21,7 +25,7 @@ class GradeDisplay extends StatelessWidget {
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 22.0, fontSize: 22.0,
color: gradeColor(grade), color: gradeColor(grade, settings),
), ),
), ),
), ),

View File

@ -1,11 +1,18 @@
import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_planner.dart'; import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_planner.dart';
import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_planner_screen.i18n.dart';
import 'package:filcnaplo_premium/ui/mobile/goal_planner/grade_display.dart'; import 'package:filcnaplo_premium/ui/mobile/goal_planner/grade_display.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
enum RouteMark { recommended, fastest } enum RouteMark { recommended, fastest }
class RouteOption extends StatelessWidget { class RouteOption extends StatelessWidget {
const RouteOption({Key? key, required this.plan, this.mark, this.selected = false, required this.onSelected}) : super(key: key); const RouteOption(
{Key? key,
required this.plan,
this.mark,
this.selected = false,
required this.onSelected})
: super(key: key);
final Plan plan; final Plan plan;
final RouteMark? mark; final RouteMark? mark;
@ -17,20 +24,20 @@ class RouteOption extends StatelessWidget {
switch (mark!) { switch (mark!) {
case RouteMark.recommended: case RouteMark.recommended:
return const Text("Recommended", style: style); return Text("recommended".i18n, style: style);
case RouteMark.fastest: case RouteMark.fastest:
return const Text("Fastest", style: style); return Text("fastest".i18n, style: style);
} }
} }
Color markColor() { Color markColor(BuildContext context) {
switch (mark) { switch (mark) {
case RouteMark.recommended: case RouteMark.recommended:
return const Color(0xff6a63d4); return const Color.fromARGB(255, 104, 93, 255);
case RouteMark.fastest: case RouteMark.fastest:
return const Color(0xffe9d524); return const Color.fromARGB(255, 255, 91, 146);
default: default:
return Colors.teal; return Theme.of(context).colorScheme.primary;
} }
} }
@ -58,14 +65,16 @@ class RouteOption extends StatelessWidget {
], ],
)); ));
} else { } else {
gradeWidgets.addAll(List.generate(count, (_) => GradeDisplay(grade: i))); gradeWidgets
.addAll(List.generate(count, (_) => GradeDisplay(grade: i)));
} }
if (count > 0) { if (count > 0) {
gradeWidgets.add(SizedBox( gradeWidgets.add(SizedBox(
height: 36.0, height: 36.0,
width: 32.0, width: 32.0,
child: Center(child: Icon(Icons.add, color: Colors.black.withOpacity(.5))), child: Center(
child: Icon(Icons.add, color: Colors.black.withOpacity(.5))),
)); ));
} }
} }
@ -77,19 +86,23 @@ class RouteOption extends StatelessWidget {
child: SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
child: Card( child: Card(
surfaceTintColor: selected ? markColor().withOpacity(.2) : Colors.white, surfaceTintColor:
selected ? markColor(context).withOpacity(.2) : Colors.white,
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
elevation: 5, elevation: 5,
shadowColor: Colors.transparent, shadowColor: Colors.transparent,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.0), borderRadius: BorderRadius.circular(16.0),
side: selected ? BorderSide(color: markColor(), width: 4.0) : BorderSide.none, side: selected
? BorderSide(color: markColor(context), width: 4.0)
: BorderSide.none,
), ),
child: InkWell( child: InkWell(
borderRadius: BorderRadius.circular(16.0), borderRadius: BorderRadius.circular(16.0),
onTap: onSelected, onTap: onSelected,
child: Padding( child: Padding(
padding: const EdgeInsets.only(top: 16.0, bottom: 16.0, left: 20.0, right: 12.0), padding: const EdgeInsets.only(
top: 16.0, bottom: 16.0, left: 20.0, right: 12.0),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -98,12 +111,14 @@ class RouteOption extends StatelessWidget {
Chip( Chip(
label: markLabel(), label: markLabel(),
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
backgroundColor: selected ? markColor() : Colors.transparent, backgroundColor:
selected ? markColor(context) : Colors.transparent,
labelPadding: const EdgeInsets.symmetric(horizontal: 8.0), labelPadding: const EdgeInsets.symmetric(horizontal: 8.0),
labelStyle: TextStyle(color: selected ? Colors.white : null), labelStyle:
TextStyle(color: selected ? Colors.white : null),
shape: StadiumBorder( shape: StadiumBorder(
side: BorderSide( side: BorderSide(
color: markColor(), color: markColor(context),
width: 3.0, width: 3.0,
), ),
), ),

View File

@ -1,8 +1,16 @@
import 'package:filcnaplo/helpers/subject.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart'; import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo_kreta_api/models/subject.dart';
import 'package:filcnaplo_kreta_api/providers/grade_provider.dart';
import 'package:filcnaplo_mobile_ui/pages/grades/calculator/grade_calculator_provider.dart';
import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_input.dart'; import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_input.dart';
import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_planner.dart'; import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_planner.dart';
import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_planner_screen.i18n.dart';
import 'package:filcnaplo_premium/ui/mobile/goal_planner/route_option.dart'; import 'package:filcnaplo_premium/ui/mobile/goal_planner/route_option.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:filcnaplo_mobile_ui/common/beta_chip.dart';
enum PlanResult { enum PlanResult {
available, // There are possible solutions available, // There are possible solutions
@ -12,13 +20,25 @@ enum PlanResult {
} }
class GoalPlannerTest extends StatefulWidget { class GoalPlannerTest extends StatefulWidget {
const GoalPlannerTest({Key? key}) : super(key: key); final Subject subject;
const GoalPlannerTest({Key? key, required this.subject}) : super(key: key);
@override @override
State<GoalPlannerTest> createState() => _GoalPlannerTestState(); State<GoalPlannerTest> createState() => _GoalPlannerTestState();
} }
class _GoalPlannerTestState extends State<GoalPlannerTest> { class _GoalPlannerTestState extends State<GoalPlannerTest> {
late GradeProvider gradeProvider;
late GradeCalculatorProvider calculatorProvider;
late SettingsProvider settingsProvider;
bool gradeCalcMode = false;
List<Grade> getSubjectGrades(Subject subject) => !gradeCalcMode
? gradeProvider.grades.where((e) => e.subject == subject).toList()
: calculatorProvider.grades.where((e) => e.subject == subject).toList();
double goalValue = 4.0; double goalValue = 4.0;
List<Grade> grades = []; List<Grade> grades = [];
@ -39,11 +59,14 @@ class _GoalPlannerTestState extends State<GoalPlannerTest> {
final planner = GoalPlanner(goalValue, grades); final planner = GoalPlanner(goalValue, grades);
final plans = planner.solve(); final plans = planner.solve();
plans.sort((a, b) => (a.avg - (2 * goalValue + 5) / 3).abs().compareTo(b.avg - (2 * goalValue + 5) / 3)); plans.sort((a, b) => (a.avg - (2 * goalValue + 5) / 3)
.abs()
.compareTo(b.avg - (2 * goalValue + 5) / 3));
try { try {
final singleSolution = plans.every((e) => e.sigma == 0); final singleSolution = plans.every((e) => e.sigma == 0);
recommended = plans.where((e) => singleSolution ? true : e.sigma > 0).first; recommended =
plans.where((e) => singleSolution ? true : e.sigma > 0).first;
plans.removeWhere((e) => e == recommended); plans.removeWhere((e) => e == recommended);
} catch (_) {} } catch (_) {}
@ -78,8 +101,18 @@ class _GoalPlannerTestState extends State<GoalPlannerTest> {
return PlanResult.available; return PlanResult.available;
} }
void getGrades() {
grades = getSubjectGrades(widget.subject).toList();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
gradeProvider = Provider.of<GradeProvider>(context);
calculatorProvider = Provider.of<GradeCalculatorProvider>(context);
settingsProvider = Provider.of<SettingsProvider>(context);
getGrades();
final currentAvg = GoalPlannerHelper.averageEvals(grades); final currentAvg = GoalPlannerHelper.averageEvals(grades);
final result = getResult(); final result = getResult();
@ -87,16 +120,42 @@ class _GoalPlannerTestState extends State<GoalPlannerTest> {
return Scaffold( return Scaffold(
body: SafeArea( body: SafeArea(
child: ListView( child: ListView(
padding: const EdgeInsets.all(20.0), padding: const EdgeInsets.only(
left: 22.0, right: 22.0, top: 5.0, bottom: 220.0),
children: [ children: [
const Align( Row(
alignment: Alignment.topLeft, mainAxisAlignment: MainAxisAlignment.spaceBetween,
child: BackButton(), children: [
const BackButton(),
Padding(
padding: const EdgeInsets.only(right: 15.0),
child: Row(
children: [
Text(
'goal_planner_title'.i18n,
style: const TextStyle(
fontWeight: FontWeight.w500, fontSize: 18.0),
),
const SizedBox(
width: 5,
),
const BetaChip(),
],
),
),
],
), ),
const SizedBox(height: 12.0), const SizedBox(height: 12.0),
const Text( Row(
"Set a goal", mainAxisAlignment: MainAxisAlignment.spaceBetween,
style: TextStyle( crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"set_a_goal".i18n,
style: const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 20.0, fontSize: 20.0,
), ),
@ -107,13 +166,51 @@ class _GoalPlannerTestState extends State<GoalPlannerTest> {
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
fontSize: 42.0, fontSize: 42.0,
color: gradeColor(goalValue.round()), color: gradeColor(goalValue.round(), settingsProvider),
), ),
), ),
],
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"select_subject".i18n,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20.0,
),
),
const SizedBox(height: 4.0),
Column(
children: [
Icon(
SubjectIcon.resolveVariant(
context: context,
subject: widget.subject,
),
size: 48.0,
),
Text(
(widget.subject.isRenamed
? widget.subject.renamedTo
: widget.subject.name) ??
'',
style: const TextStyle(
fontSize: 17.0,
fontWeight: FontWeight.w500,
),
)
],
)
],
)
],
),
const SizedBox(height: 24.0), const SizedBox(height: 24.0),
const Text( Text(
"Pick a route", "pick_route".i18n,
style: TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 20.0, fontSize: 20.0,
), ),
@ -157,8 +254,9 @@ class _GoalPlannerTestState extends State<GoalPlannerTest> {
child: Container( child: Container(
padding: const EdgeInsets.only(top: 24.0), padding: const EdgeInsets.only(top: 24.0),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color.fromARGB(255, 215, 255, 242), color: Colors.white,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24.0)), borderRadius:
const BorderRadius.vertical(top: Radius.circular(24.0)),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(.1), color: Colors.black.withOpacity(.1),
@ -183,13 +281,16 @@ class _GoalPlannerTestState extends State<GoalPlannerTest> {
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: RawMaterialButton( child: RawMaterialButton(
onPressed: () {}, onPressed: () {
fillColor: const Color(0xff01342D), ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Hamarosan...")));
},
fillColor: Theme.of(context).colorScheme.primary,
shape: const StadiumBorder(), shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(vertical: 8.0), padding: const EdgeInsets.symmetric(vertical: 8.0),
child: const Text( child: Text(
"Track it!", "track_it".i18n,
style: TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 20.0, fontSize: 20.0,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,

View File

@ -8,6 +8,7 @@ enum PremiumFeature {
profile, profile,
iconpack, iconpack,
subjectrename, subjectrename,
teacherrename,
weeklytimetable, weeklytimetable,
goalplanner, goalplanner,
widget, widget,

View File

@ -19,8 +19,10 @@ class PremiumIconPackSelector extends StatelessWidget {
return PanelButton( return PanelButton(
onPressed: () { onPressed: () {
if (!Provider.of<PremiumProvider>(context, listen: false).hasScope(PremiumScopes.customIcons)) { if (!Provider.of<PremiumProvider>(context, listen: false)
PremiumLockedFeatureUpsell.show(context: context, feature: PremiumFeature.iconpack); .hasScope(PremiumScopes.customIcons)) {
PremiumLockedFeatureUpsell.show(
context: context, feature: PremiumFeature.iconpack);
return; return;
} }
@ -28,7 +30,10 @@ class PremiumIconPackSelector extends StatelessWidget {
}, },
title: Text("icon_pack".i18n), title: Text("icon_pack".i18n),
leading: const Icon(FeatherIcons.grid), leading: const Icon(FeatherIcons.grid),
trailing: Text(settings.iconPack.name.capital()), trailing: Text(
settings.iconPack.name.capital(),
style: const TextStyle(fontSize: 14.0),
),
); );
} }
} }

View File

@ -4,6 +4,7 @@ extension SettingsLocalization on String {
static final _t = Translations.byLocale("hu_hu") + static final _t = Translations.byLocale("hu_hu") +
{ {
"en_en": { "en_en": {
// subject rename
"renamed_subjects": "Renamed Subjects", "renamed_subjects": "Renamed Subjects",
"rename_subjects": "Rename Subjects", "rename_subjects": "Rename Subjects",
"rename_subject": "Rename Subject", "rename_subject": "Rename Subject",
@ -13,21 +14,37 @@ extension SettingsLocalization on String {
"cancel": "Cancel", "cancel": "Cancel",
"done": "Done", "done": "Done",
"rename_new_subject": "Rename New Subject", "rename_new_subject": "Rename New Subject",
"italics_toggle": "Toggle Italics", "italics_toggle": "Italic Font",
// teacher rename
"renamed_teachers": "Renamed Teachers",
"rename_teachers": "Rename Teachers",
"rename_teacher": "Rename Teacher",
"select_teacher": "Select Teacher",
"modify_teachers": "Modify Teachers",
"rename_new_teacher": "Rename New Teacher",
}, },
"hu_hu": { "hu_hu": {
// subject rename
"renamed_subjects": "Átnevezett Tantárgyaid", "renamed_subjects": "Átnevezett Tantárgyaid",
"rename_subjects": "Tantárgyak átnevezése", "rename_subjects": "Tantárgyak átnevezése",
"rename_subject": "Tantárgy átnevezése", "rename_subject": "Tantárgy átnevezése",
"select_subject": "Válassz tantárgyat", "select_subject": "Válassz tantárgyat",
"modified_name": "Módosított név", "modified_name": "Módosított név",
"modify_subjects": "Tantárgyak átnevezése", "modify_subjects": "Tantárgyak módosítása",
"cancel": "Mégse", "cancel": "Mégse",
"done": "Kész", "done": "Kész",
"rename_new_subject": "Új Tantárgy átnevezése", "rename_new_subject": "Új tantárgy átnevezése",
"italics_toggle": "Dőlt betűs megjelenítés", "italics_toggle": "Dőlt betűs megjelenítés",
// teacher rename
"renamed_teachers": "Átnevezett tanáraid",
"rename_teachers": "Tanárok átnevezése",
"rename_teacher": "Tanár átnevezése",
"select_teacher": "Válassz tanárt",
"modify_teachers": "Tanárok módosítása",
"rename_new_teacher": "Új tanár átnevezése",
}, },
"de_de": { "de_de": {
// subject rename
"renamed_subjects": "Umbenannte Fächer", "renamed_subjects": "Umbenannte Fächer",
"rename_subjects": "Fächer umbenennen", "rename_subjects": "Fächer umbenennen",
"rename_subject": "Fach umbenennen", "rename_subject": "Fach umbenennen",
@ -38,6 +55,13 @@ extension SettingsLocalization on String {
"done": "Erledigt", "done": "Erledigt",
"rename_new_subject": "Neues Fach umbenennen", "rename_new_subject": "Neues Fach umbenennen",
"italics_toggle": "Kursivschrift umschalten", "italics_toggle": "Kursivschrift umschalten",
// teacher rename
"renamed_teachers": "Renamed Teachers",
"rename_teachers": "Rename Teachers",
"rename_teacher": "Rename Teacher",
"select_teacher": "Select Teacher",
"modify_teachers": "Modify Teachers",
"rename_new_teacher": "Rename New Teacher",
}, },
}; };

View File

@ -19,7 +19,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'modify_subject_names.i18n.dart'; import 'modify_names.i18n.dart';
class MenuRenamedSubjects extends StatelessWidget { class MenuRenamedSubjects extends StatelessWidget {
const MenuRenamedSubjects({Key? key, required this.settings}) const MenuRenamedSubjects({Key? key, required this.settings})

View File

@ -0,0 +1,437 @@
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo_kreta_api/models/teacher.dart';
import 'package:filcnaplo_kreta_api/providers/absence_provider.dart';
import 'package:filcnaplo_kreta_api/providers/grade_provider.dart';
import 'package:filcnaplo_kreta_api/providers/timetable_provider.dart';
import 'package:filcnaplo_mobile_ui/common/panel/panel.dart';
import 'package:filcnaplo_mobile_ui/common/panel/panel_button.dart';
import 'package:filcnaplo_premium/models/premium_scopes.dart';
import 'package:filcnaplo_premium/providers/premium_provider.dart';
import 'package:filcnaplo_premium/ui/mobile/premium/upsell.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:provider/provider.dart';
import 'modify_names.i18n.dart';
class MenuRenamedTeachers extends StatelessWidget {
const MenuRenamedTeachers({Key? key, required this.settings})
: super(key: key);
final SettingsProvider settings;
@override
Widget build(BuildContext context) {
return PanelButton(
padding: const EdgeInsets.only(left: 14.0),
onPressed: () {
if (!Provider.of<PremiumProvider>(context, listen: false)
.hasScope(PremiumScopes.renameTeachers)) {
PremiumLockedFeatureUpsell.show(
context: context, feature: PremiumFeature.teacherrename);
return;
}
Navigator.of(context, rootNavigator: true).push(
CupertinoPageRoute(builder: (context) => const ModifyTeacherNames()),
);
},
title: Text(
"rename_teachers".i18n,
style: TextStyle(
color: AppColors.of(context)
.text
.withOpacity(settings.renamedTeachersEnabled ? 1.0 : .5)),
),
leading: settings.renamedTeachersEnabled
? const Icon(FeatherIcons.users)
: Icon(FeatherIcons.users,
color: AppColors.of(context).text.withOpacity(.25)),
trailingDivider: true,
trailing: Switch(
onChanged: (v) async {
if (!Provider.of<PremiumProvider>(context, listen: false)
.hasScope(PremiumScopes.renameTeachers)) {
PremiumLockedFeatureUpsell.show(
context: context, feature: PremiumFeature.teacherrename);
return;
}
settings.update(renamedTeachersEnabled: v);
await Provider.of<GradeProvider>(context, listen: false)
.convertBySettings();
await Provider.of<TimetableProvider>(context, listen: false)
.convertBySettings();
await Provider.of<AbsenceProvider>(context, listen: false)
.convertBySettings();
},
value: settings.renamedTeachersEnabled,
activeColor: Theme.of(context).colorScheme.secondary,
),
);
}
}
class ModifyTeacherNames extends StatefulWidget {
const ModifyTeacherNames({Key? key}) : super(key: key);
@override
State<ModifyTeacherNames> createState() => _ModifyTeacherNamesState();
}
class _ModifyTeacherNamesState extends State<ModifyTeacherNames> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final _teacherName = TextEditingController();
String? selectedTeacherId;
late List<Teacher> teachers;
late UserProvider user;
late DatabaseProvider dbProvider;
late SettingsProvider settings;
@override
void initState() {
super.initState();
teachers = (Provider.of<GradeProvider>(context, listen: false)
.grades
.map((e) => e.teacher)
.toSet()
.toList()
..sort((a, b) => a.name.compareTo(b.name)));
print(teachers.map((e) => e.name));
user = Provider.of<UserProvider>(context, listen: false);
dbProvider = Provider.of<DatabaseProvider>(context, listen: false);
}
Future<Map<String, String>> fetchRenamedTeachers() async {
return await dbProvider.userQuery.renamedTeachers(userId: user.id!);
}
void showRenameDialog() {
showDialog(
context: context,
builder: (context) => StatefulBuilder(builder: (context, setS) {
return AlertDialog(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(14.0))),
title: Text("rename_teacher".i18n),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
DropdownButton2(
items: teachers
.map((item) => DropdownMenuItem<String>(
value: item.id,
child: Text(
item.name,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: AppColors.of(context).text,
),
overflow: TextOverflow.ellipsis,
),
))
.toList(),
onChanged: (String? v) async {
final renamedSubs = await fetchRenamedTeachers();
setS(() {
selectedTeacherId = v;
if (renamedSubs.containsKey(selectedTeacherId)) {
_teacherName.text = renamedSubs[selectedTeacherId]!;
} else {
_teacherName.text = "";
}
});
},
iconSize: 14,
iconEnabledColor: AppColors.of(context).text,
iconDisabledColor: AppColors.of(context).text,
underline: const SizedBox(),
itemHeight: 40,
itemPadding: const EdgeInsets.only(left: 14, right: 14),
buttonWidth: 50,
dropdownWidth: 300,
dropdownPadding: null,
buttonDecoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
),
dropdownDecoration: BoxDecoration(
borderRadius: BorderRadius.circular(14),
),
dropdownElevation: 8,
scrollbarRadius: const Radius.circular(40),
scrollbarThickness: 6,
scrollbarAlwaysShow: true,
offset: const Offset(-10, -10),
buttonSplashColor: Colors.transparent,
customButton: Container(
width: double.infinity,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey, width: 2),
borderRadius: BorderRadius.circular(12.0),
),
padding: const EdgeInsets.symmetric(
vertical: 12.0, horizontal: 8.0),
child: Text(
selectedTeacherId == null
? "select_teacher".i18n
: teachers
.firstWhere(
(element) => element.id == selectedTeacherId)
.name,
style: Theme.of(context).textTheme.titleSmall!.copyWith(
fontWeight: FontWeight.w700,
color: AppColors.of(context).text.withOpacity(0.75)),
overflow: TextOverflow.ellipsis,
maxLines: 2,
textAlign: TextAlign.center,
),
),
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Icon(FeatherIcons.arrowDown, size: 32),
),
TextField(
controller: _teacherName,
decoration: InputDecoration(
border: OutlineInputBorder(
borderSide:
const BorderSide(color: Colors.grey, width: 1.5),
borderRadius: BorderRadius.circular(12.0),
),
focusedBorder: OutlineInputBorder(
borderSide:
const BorderSide(color: Colors.grey, width: 1.5),
borderRadius: BorderRadius.circular(12.0),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0),
hintText: "modified_name".i18n,
suffixIcon: IconButton(
icon: const Icon(
FeatherIcons.x,
color: Colors.grey,
),
onPressed: () {
setState(() {
_teacherName.text = "";
});
},
),
),
),
],
),
actions: [
TextButton(
child: Text(
"cancel".i18n,
style: const TextStyle(fontWeight: FontWeight.w500),
),
onPressed: () {
Navigator.of(context).maybePop();
},
),
TextButton(
child: Text(
"done".i18n,
style: const TextStyle(fontWeight: FontWeight.w500),
),
onPressed: () async {
if (selectedTeacherId != null) {
final renamedSubs = await fetchRenamedTeachers();
renamedSubs[selectedTeacherId!] = _teacherName.text;
await dbProvider.userStore
.storeRenamedTeachers(renamedSubs, userId: user.id!);
await Provider.of<GradeProvider>(context, listen: false)
.convertBySettings();
await Provider.of<TimetableProvider>(context, listen: false)
.convertBySettings();
await Provider.of<AbsenceProvider>(context, listen: false)
.convertBySettings();
}
Navigator.of(context).pop(true);
setState(() {});
},
),
],
);
}),
).then((val) {
_teacherName.text = "";
selectedTeacherId = null;
});
}
@override
Widget build(BuildContext context) {
settings = Provider.of<SettingsProvider>(context);
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
surfaceTintColor: Theme.of(context).scaffoldBackgroundColor,
leading: BackButton(color: AppColors.of(context).text),
title: Text(
"modify_teachers".i18n,
style: TextStyle(color: AppColors.of(context).text),
),
),
body: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Panel(
child: SwitchListTile(
title: Text("italics_toggle".i18n),
onChanged: (value) =>
settings.update(renamedTeachersItalics: value),
value: settings.renamedTeachersItalics,
),
),
const SizedBox(
height: 20,
),
InkWell(
onTap: showRenameDialog,
borderRadius: BorderRadius.circular(12.0),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey, width: 2),
borderRadius: BorderRadius.circular(12.0),
),
padding: const EdgeInsets.symmetric(
vertical: 18.0, horizontal: 12.0),
child: Center(
child: Text(
"rename_new_teacher".i18n,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 18,
color: AppColors.of(context).text.withOpacity(.85),
),
),
),
),
),
const SizedBox(
height: 30,
),
FutureBuilder<Map<String, String>>(
future: fetchRenamedTeachers(),
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return Container();
}
return Panel(
title: Text("renamed_teachers".i18n),
child: Column(
children: snapshot.data!.keys.map(
(key) {
Teacher? teacher = teachers
.firstWhere((element) => key == element.id);
String renameTo = snapshot.data![key]!;
return RenamedTeacherItem(
teacher: teacher,
renamedTo: renameTo,
modifyCallback: () {
setState(() {
selectedTeacherId = teacher.id;
_teacherName.text = renameTo;
});
showRenameDialog();
},
removeCallback: () {
setState(() {
Map<String, String> subs =
Map.from(snapshot.data!);
subs.remove(key);
dbProvider.userStore.storeRenamedTeachers(
subs,
userId: user.id!);
});
},
);
},
).toList(),
),
);
},
),
],
),
),
));
}
}
class RenamedTeacherItem extends StatelessWidget {
const RenamedTeacherItem({
Key? key,
required this.teacher,
required this.renamedTo,
required this.modifyCallback,
required this.removeCallback,
}) : super(key: key);
final Teacher teacher;
final String renamedTo;
final void Function() modifyCallback;
final void Function() removeCallback;
@override
Widget build(BuildContext context) {
return ListTile(
minLeadingWidth: 32.0,
dense: true,
contentPadding:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 6.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
visualDensity: VisualDensity.compact,
onTap: () {},
leading: Icon(FeatherIcons.user,
color: AppColors.of(context).text.withOpacity(.75)),
title: InkWell(
onTap: modifyCallback,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
teacher.name.capital(),
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14,
color: AppColors.of(context).text.withOpacity(.75)),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
renamedTo,
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 16),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
trailing: InkWell(
onTap: removeCallback,
child: Icon(FeatherIcons.trash,
color: AppColors.of(context).red.withOpacity(.75)),
),
);
}
}