ios live activity

This commit is contained in:
55nknown
2022-10-03 19:37:39 +02:00
parent 75eba2c83f
commit ea33d00f54
20 changed files with 876 additions and 66 deletions

View File

@@ -23,7 +23,7 @@ import 'package:filcnaplo/api/nonce.dart';
enum LoginState { missingFields, invalidGrant, failed, normal, inProgress, success }
Nonce getNonce(BuildContext context, String nonce, String username, String instituteCode) {
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());
@@ -46,7 +46,7 @@ Future loginApi({
String nonceStr = await Provider.of<KretaClient>(context, listen: false).getAPI(KretaAPI.nonce, json: false);
Nonce nonce = getNonce(context, nonceStr, username, instituteCode);
Nonce nonce = getNonce(nonceStr, username, instituteCode);
headers.addAll(nonce.header());
Map? res = await Provider.of<KretaClient>(context, listen: false).postAPI(KretaAPI.login,

View File

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

View File

@@ -3,6 +3,7 @@ import 'dart:math';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:filcnaplo/api/client.dart';
import 'package:filcnaplo/api/providers/live_card_provider.dart';
import 'package:filcnaplo/api/providers/news_provider.dart';
import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/api/providers/status_provider.dart';
@@ -69,30 +70,35 @@ class App extends StatelessWidget {
CorePalette? corePalette;
final status = StatusProvider();
final kreta = KretaClient(user: user, settings: settings, status: status);
final timetable = TimetableProvider(user: user, database: database, kreta: kreta);
return I18n(
initialLocale: Locale(settings.language, settings.language.toUpperCase()),
child: MultiProvider(
providers: [
ChangeNotifierProvider<SettingsProvider>(create: (_) => settings),
ChangeNotifierProvider<UserProvider>(create: (_) => user),
ChangeNotifierProvider<StatusProvider>(create: (context) => StatusProvider()),
Provider<KretaClient>(create: (context) => KretaClient(context: context, userAgent: settings.config.userAgent)),
ChangeNotifierProvider<StatusProvider>(create: (_) => status),
Provider<KretaClient>(create: (_) => kreta),
Provider<DatabaseProvider>(create: (context) => database),
ChangeNotifierProvider<ThemeModeObserver>(create: (context) => ThemeModeObserver(initialTheme: settings.theme)),
ChangeNotifierProvider<NewsProvider>(create: (context) => NewsProvider(context: context)),
ChangeNotifierProvider<UpdateProvider>(create: (context) => UpdateProvider(context: context)),
// User data providers
ChangeNotifierProvider<GradeProvider>(create: (context) => GradeProvider(context: context)),
ChangeNotifierProvider<TimetableProvider>(create: (context) => TimetableProvider(context: context)),
ChangeNotifierProvider<TimetableProvider>(create: (_) => timetable),
ChangeNotifierProvider<ExamProvider>(create: (context) => ExamProvider(context: context)),
ChangeNotifierProvider<HomeworkProvider>(create: (context) => HomeworkProvider(context: context)),
ChangeNotifierProvider<MessageProvider>(create: (context) => MessageProvider(context: context)),
ChangeNotifierProvider<NoteProvider>(create: (context) => NoteProvider(context: context)),
ChangeNotifierProvider<EventProvider>(create: (context) => EventProvider(context: context)),
ChangeNotifierProvider<AbsenceProvider>(create: (context) => AbsenceProvider(context: context)),
ChangeNotifierProvider<GradeCalculatorProvider>(create: (context) => GradeCalculatorProvider(context)),
ChangeNotifierProvider<LiveCardProvider>(create: (context) => LiveCardProvider(lessonProvider: timetable, settingsProvider: settings))
],
child: Consumer<ThemeModeObserver>(
builder: (context, themeMode, child) {
@@ -104,7 +110,7 @@ class App extends StatelessWidget {
builder: (context, child) {
// Limit font size scaling to 1.0
double textScaleFactor = min(MediaQuery.of(context).textScaleFactor, 1.0);
return MediaQuery(
data: MediaQuery.of(context).copyWith(textScaleFactor: textScaleFactor),
child: child ?? Container(),
@@ -127,14 +133,14 @@ class App extends StatelessWidget {
],
localeListResolutionCallback: (locales, supported) {
Locale locale = const Locale('hu', 'HU');
for (var loc in locales ?? []) {
if (supported.contains(loc)) {
locale = loc;
break;
}
}
return locale;
},
onGenerateRoute: (settings) => rootNavigator(settings),

View File

@@ -0,0 +1,119 @@
import 'package:filcnaplo/icons/filc_icons.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo_kreta_api/models/subject.dart';
import 'package:flutter/cupertino.dart';
class SubjectIconData {
final IconData data;
final String name; // for iOS live activities compatibilty
SubjectIconData({
this.data = CupertinoIcons.rectangle_grid_2x2,
this.name = "square.grid.2x2",
});
}
class SubjectIcon {
static SubjectIconData resolve({Subject? subject, String? subjectName}) {
assert(!(subject == null && subjectName == null));
String name = (subject?.name ?? subjectName ?? "").toLowerCase().specialChars().trim();
String category = subject?.category.description.toLowerCase().specialChars() ?? "";
// todo: check for categories
if (RegExp("mate(k|matika)").hasMatch(name) || category == "matematika") {
return SubjectIconData(data: CupertinoIcons.function, name: "function");
} else if (RegExp("magyar nyelv|nyelvtan").hasMatch(name)) {
return SubjectIconData(data: CupertinoIcons.textformat_alt, name: "textformat.alt");
} else if (RegExp("irodalom").hasMatch(name)) {
return SubjectIconData(data: CupertinoIcons.book, name: "book");
} else if (RegExp("tor(i|tenelem)").hasMatch(name)) {
return SubjectIconData(data: CupertinoIcons.compass, name: "safari");
} else if (RegExp("foldrajz").hasMatch(name)) {
return SubjectIconData(data: CupertinoIcons.map, name: "map");
} else if (RegExp("rajz|muvtori|muveszet|vizualis").hasMatch(name)) {
return SubjectIconData(data: CupertinoIcons.paintbrush, name: "paintbrush");
} else if (RegExp("fizika").hasMatch(name)) {
return SubjectIconData(data: CupertinoIcons.lightbulb, name: "lightbulb");
} else if (RegExp("^enek|zene|szolfezs|zongora|korus").hasMatch(name)) {
return SubjectIconData(data: CupertinoIcons.music_note, name: "music.note");
} else if (RegExp("^tes(i|tneveles)|sport").hasMatch(name)) {
return SubjectIconData(data: CupertinoIcons.sportscourt, name: "sportscourt");
} else if (RegExp("kemia").hasMatch(name)) {
return SubjectIconData(data: CupertinoIcons.lab_flask, name: "testtube.2");
} else if (RegExp("biologia").hasMatch(name)) {
return SubjectIconData(data: CupertinoIcons.paw, name: "pawprint");
} else if (RegExp("kornyezet|termeszet ?(tudomany|ismeret)|hon( es nep)?ismeret").hasMatch(name)) {
return SubjectIconData(data: CupertinoIcons.arrow_3_trianglepath, name: "arrow.3.trianglepath");
} else if (RegExp("(hit|erkolcs)tan|vallas|etika").hasMatch(name)) {
return SubjectIconData(data: CupertinoIcons.heart, name: "heart");
} else if (RegExp("penzugy").hasMatch(name)) {
return SubjectIconData(data: CupertinoIcons.money_dollar, name: "dollarsign");
} else if (RegExp("informatika|szoftver|iroda|digitalis").hasMatch(name)) {
return SubjectIconData(data: CupertinoIcons.device_laptop, name: "laptopcomputer");
} else if (RegExp("prog").hasMatch(name)) {
return SubjectIconData(data: CupertinoIcons.chevron_left_slash_chevron_right, name: "chevron.left.forwardslash.chevron.right");
} else if (RegExp("halozat").hasMatch(name)) {
return SubjectIconData(data: CupertinoIcons.antenna_radiowaves_left_right, name: "antenna.radiowaves.left.and.right");
} else if (RegExp("szinhaz").hasMatch(name)) {
return SubjectIconData(data: CupertinoIcons.hifispeaker, name: "hifispeaker");
} else if (RegExp("film|media").hasMatch(name)) {
return SubjectIconData(data: CupertinoIcons.film, name: "film");
} else if (RegExp("elektro(tech)?nika").hasMatch(name)) {
return SubjectIconData(data: CupertinoIcons.bolt, name: "bolt");
} else if (RegExp("gepesz|mernok|ipar").hasMatch(name)) {
return SubjectIconData(data: CupertinoIcons.wrench, name: "wrench");
} else if (RegExp("technika").hasMatch(name)) {
return SubjectIconData(data: CupertinoIcons.hammer, name: "hammer");
} else if (RegExp("tanc").hasMatch(name)) {
return SubjectIconData(data: CupertinoIcons.music_mic, name: "music.mic");
} else if (RegExp("filozofia").hasMatch(name)) {
return SubjectIconData(data: CupertinoIcons.bubble_left, name: "bubble.left");
} else if (RegExp("osztaly(fonoki|kozosseg)").hasMatch(name) || name == "ofo") {
return SubjectIconData(data: CupertinoIcons.group, name: "person.3");
} else if (RegExp("gazdasag").hasMatch(name)) {
return SubjectIconData(data: CupertinoIcons.chart_pie, name: "chart.pie");
} else if (RegExp("szorgalom").hasMatch(name)) {
return SubjectIconData(data: CupertinoIcons.checkmark_seal, name: "checkmark.seal");
} else if (RegExp("magatartas").hasMatch(name)) {
return SubjectIconData(data: CupertinoIcons.smiley, name: "face.smiling");
} else if (RegExp("angol|nemet|francia|olasz|orosz|spanyol|latin|kinai|nyelv").hasMatch(name)) {
return SubjectIconData(data: CupertinoIcons.globe, name: "globe");
} else if (RegExp("linux").hasMatch(name)) {
return SubjectIconData(data: FilcIcons.linux);
}
return SubjectIconData();
}
}
class ShortSubject {
static String resolve({Subject? subject, String? subjectName}) {
assert(!(subject == null && subjectName == null));
String name = (subject?.name ?? subjectName ?? "").toLowerCase().specialChars().trim();
// String category = subject?.category.description.toLowerCase().specialChars() ?? "";
if (RegExp("magyar irodalom").hasMatch(name)) {
return "Irodalom";
} else if (RegExp("magyar nyelv").hasMatch(name)) {
return "Nyelvtan";
} else if (RegExp("matematika").hasMatch(name)) {
return "Matek";
} else if (RegExp("digitalis kultura").hasMatch(name)) {
return "Dig. kult.";
} else if (RegExp("testneveles").hasMatch(name)) {
return "Tesi";
} else if (RegExp("tortenelem").hasMatch(name)) {
return "Töri";
} else if (RegExp("(angol|nemet|francia|olasz|orosz|spanyol|latin|kinai) nyelv").hasMatch(name)) {
return (subject?.name ?? subjectName ?? "?").replaceAll(" nyelv", "");
} else if (RegExp("informatika").hasMatch(name)) {
return "Infó";
} else if (RegExp("osztalyfonoki").hasMatch(name)) {
return "Ofő";
}
return subject?.name.capital() ?? subjectName?.capital() ?? "?";
}
}

View File

@@ -1,46 +0,0 @@
import 'package:filcnaplo/icons/filc_icons.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo_kreta_api/models/subject.dart';
import 'package:flutter/material.dart';
class SubjectIcon {
static IconData? lookup({Subject? subject, String? subjectName}) {
assert(!(subject == null && subjectName == null));
String name = (subject?.name ?? subjectName ?? "").toLowerCase().specialChars().trim();
String category = subject?.category.description.toLowerCase().specialChars() ?? "";
// todo: check for categories
if (RegExp("mate(k|matika)").hasMatch(name) || category == "matematika") return Icons.calculate_outlined;
if (RegExp("magyar nyelv|nyelvtan").hasMatch(name)) return Icons.spellcheck_outlined;
if (RegExp("irodalom").hasMatch(name)) return Icons.menu_book_outlined;
if (RegExp("tor(i|tenelem)").hasMatch(name)) return Icons.hourglass_empty_outlined;
if (RegExp("foldrajz").hasMatch(name)) return Icons.public_outlined;
if (RegExp("rajz|muvtori|muveszet|vizualis").hasMatch(name)) return Icons.palette_outlined;
if (RegExp("fizika").hasMatch(name)) return Icons.emoji_objects_outlined;
if (RegExp("^enek|zene|szolfezs|zongora|korus").hasMatch(name)) return Icons.music_note_outlined;
if (RegExp("^tes(i|tneveles)|sport").hasMatch(name)) return Icons.sports_soccer_outlined;
if (RegExp("kemia").hasMatch(name)) return Icons.science_outlined;
if (RegExp("biologia").hasMatch(name)) return Icons.pets_outlined;
if (RegExp("kornyezet|termeszet(tudomany|ismeret)|hon( es nep)?ismeret").hasMatch(name)) return Icons.eco_outlined;
if (RegExp("(hit|erkolcs)tan|vallas|etika").hasMatch(name)) return Icons.favorite_border_outlined;
if (RegExp("penzugy").hasMatch(name)) return Icons.savings_outlined;
if (RegExp("informatika|szoftver|iroda|digitalis").hasMatch(name)) return Icons.computer_outlined;
if (RegExp("prog").hasMatch(name)) return Icons.code_outlined;
if (RegExp("halozat").hasMatch(name)) return Icons.wifi_tethering_outlined;
if (RegExp("szinhaz").hasMatch(name)) return Icons.theater_comedy_outlined;
if (RegExp("film|media").hasMatch(name)) return Icons.theaters_outlined;
if (RegExp("elektro(tech)?nika").hasMatch(name)) return Icons.electrical_services_outlined;
if (RegExp("gepesz|mernok|ipar").hasMatch(name)) return Icons.precision_manufacturing_outlined;
if (RegExp("technika").hasMatch(name)) return Icons.build_outlined;
if (RegExp("tanc").hasMatch(name)) return Icons.speaker_outlined;
if (RegExp("filozofia").hasMatch(name)) return Icons.psychology_outlined;
if (RegExp("osztaly(fonoki|kozosseg)").hasMatch(name) || name == "ofo") return Icons.groups_outlined;
if (RegExp("gazdasag").hasMatch(name)) return Icons.account_balance_outlined;
if (RegExp("szorgalom").hasMatch(name)) return Icons.verified_outlined;
if (RegExp("magatartas").hasMatch(name)) return Icons.emoji_people_outlined;
if (RegExp("angol|nemet|francia|olasz|orosz|spanyol|latin|kinai|nyelv").hasMatch(name)) return Icons.translate_outlined;
if (RegExp("linux").hasMatch(name)) return FilcIcons.linux;
return Icons.widgets_outlined;
}
}

View File

@@ -1,7 +1,7 @@
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo/helpers/subject_icon.dart';
import 'package:filcnaplo/helpers/subject.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo_mobile_ui/pages/grades/calculator/grade_calculator_provider.dart';
import 'package:filcnaplo_mobile_ui/pages/grades/subject_grades_container.dart';
@@ -79,7 +79,11 @@ class GradeTile extends StatelessWidget {
child: Center(
child: Padding(
padding: leadingPadding,
child: Icon(SubjectIcon.lookup(subject: grade.subject), size: 28.0, color: AppColors.of(context).text.withOpacity(.75)),
child: Icon(
SubjectIcon.resolve(subject: grade.subject).data,
size: 28.0,
color: AppColors.of(context).text.withOpacity(.75),
),
),
),
),