changed everything from filcnaplo to refilc finally

This commit is contained in:
Kima
2024-02-24 20:12:25 +01:00
parent 0d1c7b7143
commit 1171e3aaaf
655 changed files with 38728 additions and 44967 deletions

357
refilc/lib/api/client.dart Normal file
View File

@@ -0,0 +1,357 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:refilc/models/ad.dart';
import 'package:refilc/models/config.dart';
import 'package:refilc/models/news.dart';
import 'package:refilc/models/release.dart';
import 'package:refilc/models/settings.dart';
import 'package:refilc/models/shared_theme.dart';
import 'package:refilc/models/supporter.dart';
import 'package:refilc_kreta_api/models/school.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:connectivity_plus/connectivity_plus.dart';
class FilcAPI {
// API base
static const baseUrl = "https://api.refilc.hu";
// Public API
static const schoolList = "$baseUrl/v1/public/school-list";
static const news = "$baseUrl/v1/public/news";
static const supporters = "$baseUrl/v1/public/supporters";
// Private API
static const ads = "$baseUrl/v1/private/ads";
static const config = "$baseUrl/v1/private/config";
static const reportApi = "$baseUrl/v1/private/crash-report";
static const rfPlus = "$baseUrl/v2/rf-plus";
static const plusAuthLogin = "$rfPlus/auth/login";
static const plusAuthCallback = "$rfPlus/auth/callback";
static const plusActivation = "$rfPlus/activate";
static const plusScopes = "$rfPlus/scopes";
// Updates
static const repo = "refilc/naplo";
static const releases = "https://api.github.com/repos/$repo/releases";
// Share API
static const themeShare = "$baseUrl/v2/shared/theme/add";
static const themeGet = "$baseUrl/v2/shared/theme/get";
static const allThemes = "$themeGet/all";
static const themeByID = "$themeGet/";
static const gradeColorsShare = "$baseUrl/v2/shared/grade-colors/add";
static const gradeColorsGet = "$baseUrl/v2/shared/grade-colors/get";
static const allGradeColors = "$gradeColorsGet/all";
static const gradeColorsByID = "$gradeColorsGet/";
static Future<bool> checkConnectivity() async =>
(await Connectivity().checkConnectivity()) != ConnectivityResult.none;
static Future<List<School>?> getSchools() async {
try {
http.Response res = await http.get(Uri.parse(schoolList));
if (res.statusCode == 200) {
List<School> schools = (jsonDecode(res.body) as List)
.cast<Map>()
.map((json) => School.fromJson(json))
.toList();
schools.add(School(
city: "Stockholm",
instituteCode: "refilc-test-sweden",
name: "reFilc Test SE - Leo Ekström High School",
));
schools.add(School(
city: "Madrid",
instituteCode: "refilc-test-spain",
name: "reFilc Test ES - Emilio Obrero University",
));
return schools;
} else {
throw "HTTP ${res.statusCode}: ${res.body}";
}
} on Exception catch (error, stacktrace) {
log("ERROR: FilcAPI.getSchools: $error $stacktrace");
}
return null;
}
static Future<Config?> getConfig(SettingsProvider settings) async {
final userAgent = SettingsProvider.defaultSettings().config.userAgent;
Map<String, String> headers = {
"x-filc-id": settings.xFilcId,
"user-agent": userAgent,
// platform things
"rf-platform": Platform.operatingSystem,
"rf-platform-version": Platform.operatingSystemVersion,
"rf-app-version":
const String.fromEnvironment("APPVER", defaultValue: "?"),
"rf-uinid": settings.xFilcId,
};
log("[CONFIG] x-filc-id: \"${settings.xFilcId}\"");
log("[CONFIG] user-agent: \"$userAgent\"");
try {
http.Response res = await http.get(Uri.parse(config), headers: headers);
if (res.statusCode == 200) {
if (kDebugMode) {
print(jsonDecode(res.body));
}
return Config.fromJson(jsonDecode(res.body));
} else if (res.statusCode == 429) {
res = await http.get(Uri.parse(config));
if (res.statusCode == 200) return Config.fromJson(jsonDecode(res.body));
}
throw "HTTP ${res.statusCode}: ${res.body}";
} on Exception catch (error, stacktrace) {
log("ERROR: FilcAPI.getConfig: $error $stacktrace");
}
return null;
}
static Future<List<News>?> getNews() async {
try {
http.Response res = await http.get(Uri.parse(news));
if (res.statusCode == 200) {
return (jsonDecode(res.body) as List)
.cast<Map>()
.map((e) => News.fromJson(e))
.toList();
} else {
throw "HTTP ${res.statusCode}: ${res.body}";
}
} on Exception catch (error, stacktrace) {
log("ERROR: FilcAPI.getNews: $error $stacktrace");
}
return null;
}
static Future<Supporters?> getSupporters() async {
try {
http.Response res = await http.get(Uri.parse(supporters));
if (res.statusCode == 200) {
return Supporters.fromJson(jsonDecode(res.body));
} else {
throw "HTTP ${res.statusCode}: ${res.body}";
}
} on Exception catch (error, stacktrace) {
log("ERROR: FilcAPI.getSupporters: $error $stacktrace");
}
return null;
}
static Future<List<Ad>?> getAds() async {
try {
http.Response res = await http.get(Uri.parse(ads));
if (res.statusCode == 200) {
return (jsonDecode(res.body) as List)
.cast<Map>()
.map((e) => Ad.fromJson(e))
.toList();
} else {
throw "HTTP ${res.statusCode}: ${res.body}";
}
} on Exception catch (error, stacktrace) {
log("ERROR: FilcAPI.getAds: $error $stacktrace");
}
return null;
}
static Future<List<Release>?> getReleases() async {
try {
http.Response res = await http.get(Uri.parse(releases));
if (res.statusCode == 200) {
return (jsonDecode(res.body) as List)
.cast<Map>()
.map((e) => Release.fromJson(e))
.toList();
} else {
throw "HTTP ${res.statusCode}: ${res.body}";
}
} on Exception catch (error, stacktrace) {
log("ERROR: FilcAPI.getReleases: $error $stacktrace");
}
return null;
}
static Future<http.StreamedResponse?> downloadRelease(
ReleaseDownload release) {
try {
var client = http.Client();
var request = http.Request('GET', Uri.parse(release.url));
return client.send(request);
} on Exception catch (error, stacktrace) {
log("ERROR: FilcAPI.downloadRelease: $error $stacktrace");
return Future.value(null);
}
}
static Future<void> sendReport(ErrorReport report) async {
try {
Map body = {
"os": report.os,
"version": report.version,
"error": report.error,
"stack_trace": report.stack,
};
var client = http.Client();
http.Response res = await client.post(
Uri.parse(reportApi),
body: body,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
);
if (res.statusCode != 200) {
throw "HTTP ${res.statusCode}: ${res.body}";
}
} on Exception catch (error, stacktrace) {
log("ERROR: FilcAPI.sendReport: $error $stacktrace");
}
}
// sharing
static Future<void> addSharedTheme(SharedTheme theme) async {
try {
theme.json.remove('json');
theme.json['is_public'] = theme.isPublic.toString();
theme.json['background_color'] = theme.backgroundColor.value.toString();
theme.json['panels_color'] = theme.panelsColor.value.toString();
theme.json['accent_color'] = theme.accentColor.value.toString();
theme.json['icon_color'] = theme.iconColor.value.toString();
theme.json['shadow_effect'] = theme.shadowEffect.toString();
// set theme mode or remove if unneccessary
switch (theme.themeMode) {
case ThemeMode.dark:
theme.json['theme_mode'] = 'dark';
break;
case ThemeMode.light:
theme.json['theme_mode'] = 'light';
break;
default:
theme.json.remove('theme_mode');
break;
}
// set linked grade colors
theme.json['grade_colors_id'] = theme.gradeColors.id;
http.Response res = await http.post(
Uri.parse(themeShare),
body: theme.json,
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
);
if (res.statusCode != 201) {
throw "HTTP ${res.statusCode}: ${res.body}";
}
log('Shared theme successfully with ID: ${theme.id}');
} on Exception catch (error, stacktrace) {
log("ERROR: FilcAPI.addSharedTheme: $error $stacktrace");
}
}
static Future<Map?> getSharedTheme(String id) async {
try {
http.Response res = await http.get(Uri.parse(themeByID + id));
if (res.statusCode == 200) {
return (jsonDecode(res.body) as Map);
} else {
throw "HTTP ${res.statusCode}: ${res.body}";
}
} on Exception catch (error, stacktrace) {
log("ERROR: FilcAPI.getSharedTheme: $error $stacktrace");
}
return null;
}
static Future<List?> getAllSharedThemes(int count) async {
try {
http.Response res = await http.get(Uri.parse(allThemes));
if (res.statusCode == 200) {
return (jsonDecode(res.body) as List);
} else {
throw "HTTP ${res.statusCode}: ${res.body}";
}
} on Exception catch (error, stacktrace) {
log("ERROR: FilcAPI.getAllSharedThemes: $error $stacktrace");
}
return null;
}
static Future<void> addSharedGradeColors(
SharedGradeColors gradeColors) async {
try {
gradeColors.json.remove('json');
gradeColors.json['is_public'] = gradeColors.isPublic.toString();
gradeColors.json['five_color'] = gradeColors.fiveColor.value.toString();
gradeColors.json['four_color'] = gradeColors.fourColor.value.toString();
gradeColors.json['three_color'] = gradeColors.threeColor.value.toString();
gradeColors.json['two_color'] = gradeColors.twoColor.value.toString();
gradeColors.json['one_color'] = gradeColors.oneColor.value.toString();
http.Response res = await http.post(
Uri.parse(gradeColorsShare),
body: gradeColors.json,
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
);
if (res.statusCode != 201) {
throw "HTTP ${res.statusCode}: ${res.body}";
}
log('Shared grade colors successfully with ID: ${gradeColors.id}');
} on Exception catch (error, stacktrace) {
log("ERROR: FilcAPI.addSharedGradeColors: $error $stacktrace");
}
}
static Future<Map?> getSharedGradeColors(String id) async {
try {
http.Response res = await http.get(Uri.parse(gradeColorsByID + id));
if (res.statusCode == 200) {
return (jsonDecode(res.body) as Map);
} else {
throw "HTTP ${res.statusCode}: ${res.body}";
}
} on Exception catch (error, stacktrace) {
log("ERROR: FilcAPI.getSharedGradeColors: $error $stacktrace");
}
return null;
}
}
class ErrorReport {
String stack;
String os;
String version;
String error;
ErrorReport({
required this.stack,
required this.os,
required this.version,
required this.error,
});
}

191
refilc/lib/api/login.dart Normal file
View File

@@ -0,0 +1,191 @@
// ignore_for_file: avoid_print, use_build_context_synchronously
import 'package:refilc/utils/jwt.dart';
import 'package:refilc_kreta_api/models/school.dart';
import 'package:refilc_kreta_api/providers/absence_provider.dart';
import 'package:refilc_kreta_api/providers/event_provider.dart';
import 'package:refilc_kreta_api/providers/exam_provider.dart';
import 'package:refilc_kreta_api/providers/grade_provider.dart';
import 'package:refilc_kreta_api/providers/homework_provider.dart';
import 'package:refilc_kreta_api/providers/message_provider.dart';
import 'package:refilc_kreta_api/providers/note_provider.dart';
import 'package:refilc_kreta_api/providers/timetable_provider.dart';
import 'package:refilc/api/providers/user_provider.dart';
import 'package:refilc/api/providers/database_provider.dart';
import 'package:refilc/models/settings.dart';
import 'package:refilc/models/user.dart';
import 'package:refilc_kreta_api/client/api.dart';
import 'package:refilc_kreta_api/client/client.dart';
import 'package:refilc_kreta_api/models/student.dart';
import 'package:refilc_kreta_api/models/week.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:refilc/api/nonce.dart';
import 'package:uuid/uuid.dart';
enum LoginState {
missingFields,
invalidGrant,
failed,
normal,
inProgress,
success,
}
Nonce getNonce(String nonce, String username, String instituteCode) {
Nonce nonceEncoder = Nonce(
key: [98, 97, 83, 115, 120, 79, 119, 108, 85, 49, 106, 77], nonce: nonce);
nonceEncoder
.encode(instituteCode.toUpperCase() + nonce + username.toUpperCase());
return nonceEncoder;
}
Future loginAPI({
required String username,
required String password,
required String instituteCode,
required BuildContext context,
void Function(User)? onLogin,
void Function()? onSuccess,
}) async {
Future testLogin(School school) async {
var user = User(
username: username,
password: password,
instituteCode: instituteCode,
name: 'Teszt Lajos',
student: Student(
birth: DateTime.now(),
id: const Uuid().v4(),
name: 'Teszt Lajos',
school: school,
yearId: '1',
parents: ['Teszt András', 'Teszt Linda'],
json: {"a": "b"},
address: '1117 Budapest, Gábor Dénes utca 4.',
),
role: Role.parent,
);
if (onLogin != null) onLogin(user);
// store test user in db
await Provider.of<DatabaseProvider>(context, listen: false)
.store
.storeUser(user);
Provider.of<UserProvider>(context, listen: false).addUser(user);
Provider.of<UserProvider>(context, listen: false).setUser(user.id);
if (onSuccess != null) onSuccess();
return LoginState.success;
}
// if institute matches one of test things do test login
if (instituteCode == 'refilc-test-sweden') {
School school = School(
city: "Stockholm",
instituteCode: "refilc-test-sweden",
name: "reFilc Test SE - Leo Ekström High School",
);
await testLogin(school);
} else if (instituteCode == 'refilc-test-spain') {
School school = School(
city: "Madrid",
instituteCode: "refilc-test-spain",
name: "reFilc Test ES - Emilio Obrero University",
);
await testLogin(school);
} else {
// normal login from here
Provider.of<KretaClient>(context, listen: false).userAgent =
Provider.of<SettingsProvider>(context, listen: false).config.userAgent;
Map<String, String> headers = {
"content-type": "application/x-www-form-urlencoded",
};
String nonceStr = await Provider.of<KretaClient>(context, listen: false)
.getAPI(KretaAPI.nonce, json: false);
Nonce nonce = getNonce(nonceStr, username, instituteCode);
headers.addAll(nonce.header());
Map? res = await Provider.of<KretaClient>(context, listen: false)
.postAPI(KretaAPI.login,
headers: headers,
body: User.loginBody(
username: username,
password: password,
instituteCode: instituteCode,
));
if (res != null) {
if (res.containsKey("error")) {
if (res["error"] == "invalid_grant") {
return LoginState.invalidGrant;
}
} else {
if (res.containsKey("access_token")) {
try {
Provider.of<KretaClient>(context, listen: false).accessToken =
res["access_token"];
Map? studentJson =
await Provider.of<KretaClient>(context, listen: false)
.getAPI(KretaAPI.student(instituteCode));
Student student = Student.fromJson(studentJson!);
var user = User(
username: username,
password: password,
instituteCode: instituteCode,
name: student.name,
student: student,
role: JwtUtils.getRoleFromJWT(res["access_token"])!,
);
if (onLogin != null) onLogin(user);
// Store User in the database
await Provider.of<DatabaseProvider>(context, listen: false)
.store
.storeUser(user);
Provider.of<UserProvider>(context, listen: false).addUser(user);
Provider.of<UserProvider>(context, listen: false).setUser(user.id);
// Get user data
try {
await Future.wait([
Provider.of<GradeProvider>(context, listen: false).fetch(),
Provider.of<TimetableProvider>(context, listen: false)
.fetch(week: Week.current()),
Provider.of<ExamProvider>(context, listen: false).fetch(),
Provider.of<HomeworkProvider>(context, listen: false).fetch(),
Provider.of<MessageProvider>(context, listen: false).fetchAll(),
Provider.of<MessageProvider>(context, listen: false)
.fetchAllRecipients(),
Provider.of<NoteProvider>(context, listen: false).fetch(),
Provider.of<EventProvider>(context, listen: false).fetch(),
Provider.of<AbsenceProvider>(context, listen: false).fetch(),
]);
} catch (error) {
print("WARNING: failed to fetch user data: $error");
}
if (onSuccess != null) onSuccess();
return LoginState.success;
} catch (error) {
print("ERROR: loginAPI: $error");
// maybe check debug mode
// ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("ERROR: $error")));
return LoginState.failed;
}
}
}
}
}
return LoginState.failed;
}

25
refilc/lib/api/nonce.dart Normal file
View File

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

View File

@@ -0,0 +1,29 @@
import 'package:refilc/api/client.dart';
import 'package:refilc/models/ad.dart';
import 'package:flutter/material.dart';
class AdProvider extends ChangeNotifier {
// private
late List<Ad> _ads;
bool get available => _ads.isNotEmpty;
// public
List<Ad> get ads => _ads;
AdProvider({
List<Ad> initialAds = const [],
required BuildContext context,
}) {
_ads = List.castFrom(initialAds);
}
Future<void> fetch() async {
_ads = await FilcAPI.getAds() ?? [];
_ads.sort((a, b) => -a.date.compareTo(b.date));
// check for new ads
if (_ads.isNotEmpty) {
notifyListeners();
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,109 @@
// ignore_for_file: use_build_context_synchronously
import 'package:refilc/api/client.dart';
import 'package:refilc/models/news.dart';
import 'package:refilc/models/settings.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class NewsProvider extends ChangeNotifier {
// Private
late List<News> _news;
//late int _state;
late int _fresh;
bool show = false;
late BuildContext _context;
// Public
List<News> get news => _news;
int get state => _fresh - 1;
NewsProvider({
List<News> initialNews = const [],
required BuildContext context,
}) {
_news = List.castFrom(initialNews);
_context = context;
}
Future<void> restore() async {
// Load news state from the database
var seen_ = Provider.of<SettingsProvider>(_context, listen: false).seenNews;
if (seen_.isEmpty) {
var news_ = await FilcAPI.getNews();
if (news_ != null) {
_news = news_;
show = true;
}
}
//_state = seen_;
// Provider.of<SettingsProvider>(_context, listen: false)
// .update(seenNewsId: news_.id);
}
Future<void> fetch() async {
var news_ = await FilcAPI.getNews();
if (news_ == null) return;
show = false;
_news = news_;
for (var news in news_) {
if (news.expireDate.isAfter(DateTime.now()) &&
Provider.of<SettingsProvider>(_context, listen: false)
.seenNews
.contains(news.id) ==
false) {
show = true;
Provider.of<SettingsProvider>(_context, listen: false)
.update(seenNewsId: news.id);
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 release() {
// if (_fresh == 0) return;
// _fresh--;
// //_state++;
// // Provider.of<SettingsProvider>(_context, listen: false)
// // .update(seenNewsId: _state);
// if (_fresh > 0) {
// show = true;
// } else {
// show = false;
// }
// notifyListeners();
}
}

View File

@@ -0,0 +1,53 @@
import 'package:refilc/api/providers/user_provider.dart';
import 'package:refilc/api/providers/database_provider.dart';
import 'package:refilc/models/self_note.dart';
import 'package:refilc/models/user.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class SelfNoteProvider with ChangeNotifier {
late List<SelfNote> _notes;
late BuildContext _context;
List<SelfNote> get notes => _notes;
SelfNoteProvider({
List<SelfNote> initialNotes = const [],
required BuildContext context,
}) {
_notes = List.castFrom(initialNotes);
_context = context;
if (_notes.isEmpty) restore();
}
// restore self notes from db
Future<void> restore() async {
String? userId = Provider.of<UserProvider>(_context, listen: false).id;
// load self notes from db
if (userId != null) {
var dbNotes = await Provider.of<DatabaseProvider>(_context, listen: false)
.userQuery
.getSelfNotes(userId: userId);
_notes = dbNotes;
notifyListeners();
}
}
// fetches fresh data from api (not needed, cuz no api for that)
// Future<void> fetch() async {
// }
// store self notes in db
Future<void> store(List<SelfNote> notes) async {
User? user = Provider.of<UserProvider>(_context, listen: false).user;
if (user == null) throw "Cannot store Self Notes for User null";
String userId = user.id;
await Provider.of<DatabaseProvider>(_context, listen: false)
.userStore
.storeSelfNotes(notes, userId: userId);
_notes = notes;
notifyListeners();
}
}

View File

@@ -0,0 +1,126 @@
import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/widgets.dart';
import 'package:http/http.dart' as http;
enum Status { network, maintenance, syncing, apiError }
class StatusProvider extends ChangeNotifier {
final List<Status> _stack = [];
double _progress = 0.0;
ConnectivityResult _networkType = ConnectivityResult.none;
ConnectivityResult get networkType => _networkType;
StatusProvider() {
_handleNetworkChanges();
_handleDNSFailure();
Connectivity().checkConnectivity().then((value) => _networkType = value);
}
Status? getStatus() => _stack.isNotEmpty ? _stack[0] : null;
// Status progress from 0.0 to 1.0
double get progress => _progress;
void _handleNetworkChanges() {
Connectivity().onConnectivityChanged.listen((event) {
_networkType = event;
if (event == ConnectivityResult.none) {
if (!_stack.contains(Status.network)) {
_stack.remove(Status.apiError);
_stack.insert(0, Status.network);
notifyListeners();
}
} else {
if (_stack.contains(Status.network)) {
_stack.remove(Status.network);
notifyListeners();
}
}
});
}
void _handleDNSFailure() {
try {
InternetAddress.lookup('api.refilc.hu').then((status) {
if (status.isEmpty) {
if (!_stack.contains(Status.network)) {
_stack.remove(Status.apiError);
_stack.insert(0, Status.network);
notifyListeners();
}
} else {
if (_stack.contains(Status.network)) {
_stack.remove(Status.network);
notifyListeners();
}
}
});
} on SocketException catch (_) {
if (!_stack.contains(Status.network)) {
_stack.remove(Status.apiError);
_stack.insert(0, Status.network);
notifyListeners();
}
}
}
void triggerRequest(http.Response res) {
if (res.headers.containsKey("x-maintenance-mode") ||
res.statusCode == 503) {
if (!_stack.contains(Status.maintenance)) {
_stack.insert(0, Status.maintenance);
notifyListeners();
}
} else {
if (_stack.contains(Status.maintenance)) {
_stack.remove(Status.maintenance);
notifyListeners();
}
}
if (res.body == 'invalid_grant' ||
res.body.replaceAll(' ', '') == '' ||
res.statusCode == 400) {
if (!_stack.contains(Status.apiError) &&
!_stack.contains(Status.network)) {
if (res.statusCode == 401) return;
_stack.insert(0, Status.apiError);
notifyListeners();
}
} else {
if (_stack.contains(Status.apiError) &&
res.request?.url.path != '/nonce') {
_stack.remove(Status.apiError);
notifyListeners();
}
}
}
void triggerSync({required int current, required int max}) {
double prev = _progress;
if (!_stack.contains(Status.syncing)) {
_stack.add(Status.syncing);
_progress = 0.0;
notifyListeners();
}
if (max == 0) {
_progress = 0.0;
} else {
_progress = current / max;
}
if (_progress == 1.0) {
notifyListeners();
// Wait for animation
Future.delayed(const Duration(milliseconds: 250), () {
_stack.remove(Status.syncing);
notifyListeners();
});
} else if (progress != prev) {
notifyListeners();
}
}
}

View File

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

View File

@@ -0,0 +1,65 @@
import 'dart:io';
import 'package:refilc/api/client.dart';
import 'package:refilc/models/release.dart';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
class UpdateProvider extends ChangeNotifier {
// Private
late List<Release> _releases;
bool _available = false;
bool get available => _available && _releases.isNotEmpty;
// Public
List<Release> get releases => _releases;
UpdateProvider({
List<Release> initialReleases = const [],
required BuildContext context,
}) {
_releases = List.castFrom(initialReleases);
}
Future<void> fetch() async {
late String currentVersion;
PackageInfo packageInfo = await PackageInfo.fromPlatform();
currentVersion = packageInfo.version;
if (!Platform.isAndroid) return;
_releases = await FilcAPI.getReleases() ?? [];
_releases.sort((a, b) => -a.version.compareTo(b.version));
// Check for new releases
if (_releases.isNotEmpty) {
if (!_releases.first.prerelease) {
_available = _releases.first.version
.compareTo(Version.fromString(currentVersion)) ==
1;
}
// ignore: avoid_print
if (_available) print("INFO: New update: ${releases.first.version}");
notifyListeners();
}
}
Future<Map> installedVersion() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
String appName = packageInfo.appName;
String packageName = packageInfo.packageName;
String version = packageInfo.version;
String buildNumber = packageInfo.buildNumber;
Map<String, String> release = {
"app_name": appName,
"package_name": packageName,
"version": version,
"build_number": buildNumber,
};
return release;
}
}

View File

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