From 32936c2aa5b712013299fba661b118d7474ce268 Mon Sep 17 00:00:00 2001 From: Armand <4831c0@proton.me> Date: Mon, 2 Mar 2026 23:26:59 +0100 Subject: [PATCH 1/6] firka: extract firka_common package with shared widgets (Isar kept separate) - Create firka_common package with core helpers (debug, json, icon), theme, and shared widgets (FirkaCard, FirkaShadow, GradeWidget, GradeSmallCard, ClassIconWidget, FirkaIconWidget, DelayedSpinnerWidget, CounterDigitWidget) - Keep Isar models (GenericCacheModel, TimetableCacheModel, HomeworkCacheModel, DatedCacheEntry, util) in firka and firka_wear - not moved to firka_common - Update firka and firka_wear to depend on firka_common for shared UI only - Add configurable roundGrade thresholds for firka settings - Add package param to FirkaIconWidget for app asset paths --- firka/lib/api/client/kreta_client.dart | 2 +- firka/lib/core/debug_helper.dart | 18 +- firka/lib/core/extensions.dart | 27 +- firka/lib/core/icon_helper.dart | 153 +-------- firka/lib/core/json_helper.dart | 10 +- firka/lib/data/models/token_model.dart | 2 +- firka/lib/ui/components/firka_card.dart | 135 +------- firka/lib/ui/components/firka_shadow.dart | 46 +-- firka/lib/ui/components/grade.dart | 98 +----- firka/lib/ui/components/grade_helpers.dart | 96 +----- .../lib/ui/phone/pages/home/home_grades.dart | 25 +- .../ui/phone/pages/home/home_timetable.dart | 2 + .../phone/pages/home/home_timetable_mo.dart | 2 + .../screens/settings/settings_screen.dart | 24 ++ .../ui/phone/widgets/home_main_welcome.dart | 2 + firka/lib/ui/phone/widgets/homework.dart | 1 + firka/lib/ui/phone/widgets/lesson.dart | 1 + firka/lib/ui/phone/widgets/lesson_big.dart | 2 + firka/lib/ui/shared/class_icon.dart | 36 +- firka/lib/ui/shared/counter_digit.dart | 22 +- firka/lib/ui/shared/delayed_spinner.dart | 46 +-- firka/lib/ui/shared/firka_icon.dart | 43 +-- firka/lib/ui/shared/grade_small_card.dart | 61 +--- firka/lib/ui/theme/style.dart | 310 +----------------- firka/pubspec.yaml | 2 + firka_common/lib/core/debug_helper.dart | 17 + firka_common/lib/core/extensions.dart | 19 ++ firka_common/lib/core/icon_helper.dart | 135 ++++++++ firka_common/lib/core/json_helper.dart | 9 + firka_common/lib/firka_common.dart | 16 + .../lib/ui/components/firka_card.dart | 137 ++++++++ .../lib/ui/components/firka_shadow.dart | 52 +++ firka_common/lib/ui/components/grade.dart | 97 ++++++ .../lib/ui/components/grade_helpers.dart | 100 ++++++ firka_common/lib/ui/shared/class_icon.dart | 35 ++ firka_common/lib/ui/shared/counter_digit.dart | 22 ++ .../lib/ui/shared/delayed_spinner.dart | 48 +++ firka_common/lib/ui/shared/firka_icon.dart | 46 +++ .../lib/ui/shared/grade_small_card.dart | 60 ++++ firka_common/lib/ui/theme/style.dart | 309 +++++++++++++++++ firka_common/pubspec.yaml | 21 ++ firka_wear/lib/core/debug_helper.dart | 18 +- firka_wear/lib/core/extensions.dart | 21 +- firka_wear/lib/core/icon_helper.dart | 153 +-------- firka_wear/lib/core/json_helper.dart | 10 +- .../lib/data/models/homework_cache_model.dart | 2 +- .../data/models/timetable_cache_model.dart | 2 +- firka_wear/lib/ui/components/firka_card.dart | 58 +--- .../lib/ui/components/firka_shadow.dart | 45 +-- firka_wear/lib/ui/components/grade.dart | 88 +---- .../lib/ui/components/grade_helpers.dart | 76 +---- firka_wear/lib/ui/shared/class_icon.dart | 37 +-- firka_wear/lib/ui/shared/counter_digit.dart | 23 +- firka_wear/lib/ui/shared/delayed_spinner.dart | 43 +-- firka_wear/lib/ui/shared/firka_icon.dart | 40 +-- .../lib/ui/shared/grade_small_card.dart | 61 +--- firka_wear/lib/ui/theme/style.dart | 272 +-------------- firka_wear/pubspec.yaml | 2 + 58 files changed, 1221 insertions(+), 2019 deletions(-) create mode 100644 firka_common/lib/core/debug_helper.dart create mode 100644 firka_common/lib/core/extensions.dart create mode 100644 firka_common/lib/core/icon_helper.dart create mode 100644 firka_common/lib/core/json_helper.dart create mode 100644 firka_common/lib/firka_common.dart create mode 100644 firka_common/lib/ui/components/firka_card.dart create mode 100644 firka_common/lib/ui/components/firka_shadow.dart create mode 100644 firka_common/lib/ui/components/grade.dart create mode 100644 firka_common/lib/ui/components/grade_helpers.dart create mode 100644 firka_common/lib/ui/shared/class_icon.dart create mode 100644 firka_common/lib/ui/shared/counter_digit.dart create mode 100644 firka_common/lib/ui/shared/delayed_spinner.dart create mode 100644 firka_common/lib/ui/shared/firka_icon.dart create mode 100644 firka_common/lib/ui/shared/grade_small_card.dart create mode 100644 firka_common/lib/ui/theme/style.dart create mode 100644 firka_common/pubspec.yaml diff --git a/firka/lib/api/client/kreta_client.dart b/firka/lib/api/client/kreta_client.dart index 6b3435d..e76cd23 100644 --- a/firka/lib/api/client/kreta_client.dart +++ b/firka/lib/api/client/kreta_client.dart @@ -12,8 +12,8 @@ import 'package:kreta_api/kreta_api.dart' hide KretaEndpoints; import 'package:firka/app/app_state.dart'; import 'package:firka/core/bloc/reauth_cubit.dart'; import 'package:firka/data/models/token_model.dart'; -import 'package:firka/data/util.dart'; import 'package:firka/core/debug_helper.dart'; +import 'package:firka/data/util.dart'; import 'package:firka/services/active_account_helper.dart'; import 'package:firka/services/watch_sync_helper.dart'; import '../consts.dart'; diff --git a/firka/lib/core/debug_helper.dart b/firka/lib/core/debug_helper.dart index 027d8e8..d374108 100644 --- a/firka/lib/core/debug_helper.dart +++ b/firka/lib/core/debug_helper.dart @@ -1,17 +1 @@ -DateTime? debugFakeTime; -DateTime? debugSetAt; -var debugTimeAdvance = false; - -DateTime timeNow() { - if (debugFakeTime != null) { - if (debugTimeAdvance && debugSetAt != null) { - var diff = DateTime.now().difference(debugSetAt!); - - return debugFakeTime!.add(diff); - } else { - return debugFakeTime!; - } - } else { - return DateTime.now(); - } -} +export 'package:firka_common/core/debug_helper.dart'; diff --git a/firka/lib/core/extensions.dart b/firka/lib/core/extensions.dart index 8eb59a5..e837e7f 100644 --- a/firka/lib/core/extensions.dart +++ b/firka/lib/core/extensions.dart @@ -1,8 +1,11 @@ import 'package:intl/intl.dart'; -import 'package:kreta_api/kreta_api.dart'; -import 'package:firka/core/debug_helper.dart'; import 'package:firka/l10n/app_localizations.dart'; +import 'package:firka_common/core/debug_helper.dart'; +import 'package:firka_common/core/extensions.dart'; +import 'package:kreta_api/kreta_api.dart'; + +export 'package:firka_common/core/extensions.dart'; extension TimetableExtension on Iterable { List getAllSeqs(Lesson reference) { @@ -59,26 +62,6 @@ extension TimetableExtension on Iterable { } } -extension IterableExtensionMap on Iterable> { - Map toMap() { - var map = {}; - for (var item in this) { - map[item.key] = item.value; - } - - return map; - } -} - -extension IterableExtension on Iterable { - T? firstWhereOrNull(bool Function(T element) test) { - for (var element in this) { - if (test(element)) return element; - } - return null; - } -} - extension DurationExtension on Duration { String formatDuration() { String hours = inHours.toString().padLeft(2, '0'); diff --git a/firka/lib/core/icon_helper.dart b/firka/lib/core/icon_helper.dart index 34d5343..dbf6f91 100644 --- a/firka/lib/core/icon_helper.dart +++ b/firka/lib/core/icon_helper.dart @@ -1,152 +1 @@ -import 'dart:typed_data'; - -import 'package:majesticons_flutter/majesticons_flutter.dart'; - -enum ClassIcon { - mathematics, - grammar, - literature, - history, - geography, - art, - physics, - music, - pe, - chemistry, - biology, - env, - religion, - economics, - it, - code, - networking, - theatre, - film, - electricalEngineering, - mechanicalEngineering, - technika, - dance, - philosophy, - ofo, - diligence, - attitude, - language, - linux, - database, - applications, - project, -} - -Map _descriptors = { - ClassIcon.mathematics: RegExp(r'mate(k|matika)'), - ClassIcon.grammar: RegExp(r'magyar nyelv|nyelvtan'), - ClassIcon.literature: RegExp(r'irodalom'), - ClassIcon.history: RegExp(r'tor(i|tenelem)'), - ClassIcon.geography: RegExp(r'foldrajz'), - ClassIcon.art: RegExp(r'rajz|muvtori|muveszet|vizualis'), - ClassIcon.physics: RegExp(r'fizika'), - ClassIcon.music: RegExp(r'^enek|zene|szolfezs|zongora|korus'), - ClassIcon.pe: RegExp(r'^tes(i|tneveles)|sport|edzeselmelet'), - ClassIcon.chemistry: RegExp(r'kemia'), - ClassIcon.biology: RegExp(r'biologia'), - ClassIcon.env: RegExp( - r'kornyezet|termeszet ?(tudomany|ismeret)|hon( es nep)?ismeret', - ), - ClassIcon.religion: RegExp(r'(hit|erkolcs)tan|vallas|etika|bibliaismeret'), - ClassIcon.economics: RegExp(r'penzugy|gazdasag'), - ClassIcon.it: RegExp(r'informatika|szoftver|iroda|digitalis'), - ClassIcon.code: RegExp(r'prog|alkalmazas'), - ClassIcon.networking: RegExp(r'halozat'), - ClassIcon.theatre: RegExp(r'szinhaz'), - ClassIcon.film: RegExp(r'film|media'), - ClassIcon.electricalEngineering: RegExp(r'elektro(tech)?nika'), - ClassIcon.mechanicalEngineering: RegExp(r'gepesz|mernok|ipar'), - ClassIcon.technika: RegExp(r'technika'), - ClassIcon.dance: RegExp(r'tanc'), - ClassIcon.philosophy: RegExp(r'filozofia'), - ClassIcon.ofo: RegExp(r'osztaly(fonoki|kozosseg)|kozossegi|neveles'), - ClassIcon.diligence: RegExp(r'szorgalom'), - ClassIcon.attitude: RegExp(r'magatartas'), - ClassIcon.language: RegExp( - r'angol|nemet|francia|olasz|orosz|spanyol|latin|kinai|nyelv', - ), - ClassIcon.linux: RegExp(r'linux'), - ClassIcon.database: RegExp(r'adatbazis.*'), - ClassIcon.applications: RegExp(r'asztali alkalmazasok'), - ClassIcon.project: RegExp(r'projekt'), -}; - -Map _iconMap = { - ClassIcon.mathematics: Majesticon.calculatorSolid, - ClassIcon.grammar: Majesticon.bookSolid, - ClassIcon.literature: Majesticon.bookOpenSolid, - ClassIcon.history: Majesticon.compass2Solid, - ClassIcon.geography: Majesticon.globeEarth2Solid, - ClassIcon.art: Majesticon.editPen2Solid, - // ClassIcon.physics: , - ClassIcon.music: Majesticon.musicNoteSolid, - // ClassIcon.pe: , - ClassIcon.chemistry: Majesticon.testTubeFilledSolid, - ClassIcon.biology: Majesticon.covidSolid, - // ClassIcon.env: , - // ClassIcon.religion: , - // ClassIcon.economics: , - ClassIcon.it: Majesticon.laptopSolid, - ClassIcon.code: Majesticon.curlyBracesSolid, - ClassIcon.networking: Majesticon.cloudSolid, - // ClassIcon.theatre: , - // ClassIcon.film: , - // ClassIcon.electricalEngineering: , - // ClassIcon.mechanicalEngineering: , - ClassIcon.technika: Majesticon.ruler2Solid, - // ClassIcon.dance: , - // ClassIcon.philosophy: , - // ClassIcon.ofo: , - // ClassIcon.diligence: , - // ClassIcon.attitude: , - ClassIcon.language: Majesticon.tooltipsSolid, - // ClassIcon.linux: , - ClassIcon.database: Majesticon.dataSolid, - // ClassIcon.applications: , - // ClassIcon.project: , -}; - -ClassIcon? getIconType(String uid, String className, String category) { - ClassIcon? icon; - if (category.toLowerCase() == "matematika") { - icon = ClassIcon.mathematics; - } - - if (icon == null) { - for (var desc in _descriptors.entries) { - if (desc.value.hasMatch( - className - .replaceAll("ö", "o") - .replaceAll("ü", "u") - .replaceAll("ó", "o") - .replaceAll("ő", "o") - .replaceAll("ú", "u") - .replaceAll("é", "e") - .replaceAll("á", "a") - .replaceAll("ű", "u") - .replaceAll("í", "i") - .toLowerCase(), - )) { - icon = desc.key; - - break; - } - } - } - - return icon; -} - -Uint8List getIconData(ClassIcon? icon) { - if (icon == null) return Majesticon.alertCircleSolid; - - var iconData = _iconMap[icon]; - iconData ??= Majesticon.alertCircleSolid; - - return iconData; -} +export 'package:firka_common/core/icon_helper.dart'; diff --git a/firka/lib/core/json_helper.dart b/firka/lib/core/json_helper.dart index 4e07713..cd391aa 100644 --- a/firka/lib/core/json_helper.dart +++ b/firka/lib/core/json_helper.dart @@ -1,9 +1 @@ -List listToTyped(List dynamicList) { - var newList = List.empty(growable: true); - - for (var item in dynamicList) { - newList.add(item as T); - } - - return newList; -} +export 'package:firka_common/core/json_helper.dart'; diff --git a/firka/lib/data/models/token_model.dart b/firka/lib/data/models/token_model.dart index 01c0523..974a830 100644 --- a/firka/lib/data/models/token_model.dart +++ b/firka/lib/data/models/token_model.dart @@ -3,8 +3,8 @@ import 'dart:convert'; import 'package:crypto/crypto.dart'; import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; import 'package:kreta_api/kreta_api.dart'; -import 'package:firka/core/debug_helper.dart'; import 'package:firka/core/extensions.dart'; +import 'package:firka_common/core/debug_helper.dart'; import 'package:isar_community/isar.dart'; part 'token_model.g.dart'; diff --git a/firka/lib/ui/components/firka_card.dart b/firka/lib/ui/components/firka_card.dart index 02ba429..d732b90 100644 --- a/firka/lib/ui/components/firka_card.dart +++ b/firka/lib/ui/components/firka_card.dart @@ -1,134 +1 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'package:firka/core/bloc/theme_cubit.dart'; -import 'package:firka/ui/components/firka_shadow.dart'; -import 'package:firka/ui/theme/style.dart'; - -enum Attach { none, bottom, top } - -class FirkaCard extends StatelessWidget { - final List left; - final List? center; - final double? height; - final List? right; - final bool shadow; - final Widget? extra; - final Attach? attached; - final Color? color; - - const FirkaCard({ - required this.left, - this.shadow = true, - this.center, - this.right, - this.extra, - this.attached, - this.color, - this.height, - super.key, - }); - - @override - Widget build(BuildContext context) { - var right = this.right ?? []; - - var attached = this.attached != null ? this.attached! : Attach.none; - final defaultRounding = 16.0; - final attachedRounding = 8.0; - final isLight = context.watch().state.isLightMode; - - if (extra != null) { - return SizedBox( - width: MediaQuery.of(context).size.width, - height: height, - child: FirkaShadow( - shadow: shadow, - child: Card( - color: color ?? appStyle.colors.card, - shadowColor: isLight && shadow ? null : Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular( - attached == Attach.top ? attachedRounding : defaultRounding, - ), - topRight: Radius.circular( - attached == Attach.top ? attachedRounding : defaultRounding, - ), - bottomLeft: Radius.circular( - attached == Attach.bottom - ? attachedRounding - : defaultRounding, - ), - bottomRight: Radius.circular( - attached == Attach.bottom - ? attachedRounding - : defaultRounding, - ), - ), - ), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row(children: left), - Row(children: center ?? []), - Row(children: right), - ], - ), - extra ?? SizedBox(), - ], - ), - ), - ), - ), - ); - } else { - return SizedBox( - width: MediaQuery.of(context).size.width, - height: height, - child: FirkaShadow( - shadow: shadow, - child: Card( - color: color ?? appStyle.colors.card, - shadowColor: isLight && shadow ? null : Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular( - attached == Attach.top ? attachedRounding : defaultRounding, - ), - topRight: Radius.circular( - attached == Attach.top ? attachedRounding : defaultRounding, - ), - bottomLeft: Radius.circular( - attached == Attach.bottom - ? attachedRounding - : defaultRounding, - ), - bottomRight: Radius.circular( - attached == Attach.bottom - ? attachedRounding - : defaultRounding, - ), - ), - ), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row(children: left), - Row(children: center ?? []), - Row(children: right), - ], - ), - ), - ), - ), - ); - } - } -} +export 'package:firka_common/ui/components/firka_card.dart'; diff --git a/firka/lib/ui/components/firka_shadow.dart b/firka/lib/ui/components/firka_shadow.dart index f3e7e13..29a96dc 100644 --- a/firka/lib/ui/components/firka_shadow.dart +++ b/firka/lib/ui/components/firka_shadow.dart @@ -1,45 +1 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'package:firka/core/bloc/theme_cubit.dart'; -import 'package:firka/ui/theme/style.dart'; - -class FirkaShadow extends StatelessWidget { - final Widget child; - final bool shadow; - - const FirkaShadow({required this.shadow, required this.child, super.key}); - - @override - Widget build(BuildContext context) { - final borderRadius = BorderRadius.circular(8.0); - - final shadowBox = BoxDecoration( - color: Colors.transparent, - shape: BoxShape.rectangle, - boxShadow: [ - BoxShadow( - color: appStyle.colors.shadowColor, - spreadRadius: -4, - blurRadius: 0, - offset: Offset(0, 2), - ), - ], - borderRadius: BorderRadius.all(Radius.circular(16)), - ); - - if (!shadow) { - return ClipRRect(borderRadius: borderRadius, child: child); - } - - final isLight = context.watch().state.isLightMode; - if (isLight) { - return child; - } else { - return Container( - decoration: shadowBox, - child: ClipRRect(borderRadius: borderRadius, child: child), - ); - } - } -} +export 'package:firka_common/ui/components/firka_shadow.dart'; diff --git a/firka/lib/ui/components/grade.dart b/firka/lib/ui/components/grade.dart index a83e4c3..fdc5b4a 100644 --- a/firka/lib/ui/components/grade.dart +++ b/firka/lib/ui/components/grade.dart @@ -1,97 +1 @@ -import 'package:kreta_api/kreta_api.dart'; -import 'package:flutter/material.dart'; - -import 'package:firka/ui/theme/style.dart'; -import 'package:firka/ui/components/grade_helpers.dart'; - -class GradeWidget extends StatelessWidget { - const GradeWidget(this.grade, {super.key}) - : gradeValue = null, - _fromValue = false; - - const GradeWidget.gradeValue(int value, {super.key}) - : grade = null, - gradeValue = value, - _fromValue = true; - - final Grade? grade; - final int? gradeValue; - final bool _fromValue; - - @override - Widget build(BuildContext context) { - if (_fromValue && gradeValue != null) { - return _buildNumericCircle( - gradeValue!, - getGradeColor(gradeValue!.toDouble()), - ); - } - - final g = grade!; - Color gradeColor = appStyle.colors.grade1; - final gradeStr = g.numericValue?.toString() ?? '0'; - - if (g.valueType.name == 'Szazalekos') { - if (g.numericValue != null) { - gradeColor = getGradeColor( - percentageToGrade(g.numericValue!).toDouble(), - ); - } - - final str = g.strValue.replaceAll('%', ''); - return Card( - shape: const CircleBorder(), - shadowColor: Colors.transparent, - color: gradeColor.withAlpha(38), - child: Padding( - padding: const EdgeInsets.all(8), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(str, style: appStyle.fonts.P_14.copyWith(color: gradeColor)), - Text('%', style: appStyle.fonts.P_12.copyWith(color: gradeColor)), - ], - ), - ), - ); - } - - if (g.numericValue != null) { - gradeColor = getGradeColor(g.numericValue!.toDouble()); - } - - if (gradeStr == '0') { - return Card( - shadowColor: Colors.transparent, - color: gradeColor.withAlpha(38), - child: Padding( - padding: const EdgeInsets.only(left: 8, right: 8, top: 2, bottom: 2), - child: Text( - g.strValue, - style: appStyle.fonts.H_H1.copyWith( - fontSize: 16, - color: gradeColor, - ), - ), - ), - ); - } - - return _buildNumericCircle(g.numericValue!, gradeColor); - } - - Widget _buildNumericCircle(int value, Color gradeColor) { - return Card( - shape: const CircleBorder(), - shadowColor: Colors.transparent, - color: gradeColor.withAlpha(38), - child: Padding( - padding: const EdgeInsets.only(left: 8, right: 8), - child: Text( - value.toString(), - style: appStyle.fonts.H_H1.copyWith(fontSize: 24, color: gradeColor), - ), - ), - ); - } -} +export 'package:firka_common/ui/components/grade.dart'; diff --git a/firka/lib/ui/components/grade_helpers.dart b/firka/lib/ui/components/grade_helpers.dart index caaea9b..cfa04d2 100644 --- a/firka/lib/ui/components/grade_helpers.dart +++ b/firka/lib/ui/components/grade_helpers.dart @@ -1,95 +1 @@ -import 'dart:ui'; - -import 'package:firka/core/settings.dart'; -import 'package:firka/app/app_state.dart'; - -import 'package:firka/ui/theme/style.dart'; -import 'package:kreta_api/kreta_api.dart'; - -int roundGrade(double grade) { - final rounding = initData.settings - .group("settings") - .subGroup("application") - .subGroup("rounding"); - if (grade < 1 + rounding.dbl("1")) { - return 1; - } - if (grade < 2 + rounding.dbl("2")) { - return 2; - } - if (grade < 3 + rounding.dbl("3")) { - return 3; - } - if (grade < 4 + rounding.dbl("4")) { - return 4; - } - - return 5; -} - -int percentageToGrade(int grade) { - if (grade < 50) { - return 1; - } - if (grade < 60) { - return 2; - } - if (grade < 70) { - return 3; - } - if (grade < 80) { - return 4; - } - - return 5; -} - -Color getGradeColor(double grade) { - switch (roundGrade(grade)) { - case 2: - return appStyle.colors.grade2; - case 3: - return appStyle.colors.grade3; - case 4: - return appStyle.colors.grade4; - case 5: - return appStyle.colors.grade5; - default: - return appStyle.colors.grade1; - } -} - -(int total, List countsByGrade) getGradeDistribution(List grades) { - final filtered = grades - .where((g) => g.type.name != "felevi_jegy_ertekeles") - .toList(); - final counts = [0, 0, 0, 0, 0]; - for (final g in filtered) { - if (g.numericValue == null) continue; - final value = g.valueType.name == "Szazalekos" - ? percentageToGrade(g.numericValue!.round()) - : g.numericValue!.round().clamp(1, 5); - counts[value - 1]++; - } - return (filtered.length, counts); -} - -extension GradeListExtension on List { - double getAverageBySubject(Subject subject) { - var weightTotal = 0.00; - var sum = 0.00; - - for (var grade in this) { - if (grade.subject.uid == subject.uid) { - if (grade.numericValue != null) { - var weight = (grade.weightPercentage ?? 100) / 100.0; - weightTotal += weight; - - sum += grade.numericValue! * weight; - } - } - } - - return sum / weightTotal; - } -} +export 'package:firka_common/ui/components/grade_helpers.dart'; diff --git a/firka/lib/ui/phone/pages/home/home_grades.dart b/firka/lib/ui/phone/pages/home/home_grades.dart index 7b9abd3..35e98ba 100644 --- a/firka/lib/ui/phone/pages/home/home_grades.dart +++ b/firka/lib/ui/phone/pages/home/home_grades.dart @@ -14,6 +14,7 @@ import 'package:firka/core/debug_helper.dart'; import 'package:firka/core/state/firka_state.dart'; import 'package:firka/app/app_state.dart'; import 'package:firka/core/bloc/home_refresh_cubit.dart'; +import 'package:firka/core/settings.dart'; import 'package:firka/ui/theme/style.dart'; import 'package:firka/ui/shared/delayed_spinner.dart'; @@ -197,7 +198,17 @@ class _HomeGradesScreen extends FirkaState { if (!avg.isNaN) { subjectCount++; subjectAvg += avg; - subjectAvgRounded += roundGrade(avg); + final rounding = widget.data.settings + .group("settings") + .subGroup("application") + .subGroup("rounding"); + subjectAvgRounded += roundGrade( + avg, + t1: rounding.dbl("1"), + t2: rounding.dbl("2"), + t3: rounding.dbl("3"), + t4: rounding.dbl("4"), + ); } } @@ -209,7 +220,17 @@ class _HomeGradesScreen extends FirkaState { subjectAvgRounded = 0.00; } - var subjectAvgColor = getGradeColor(subjectAvg); + final rounding = widget.data.settings + .group("settings") + .subGroup("application") + .subGroup("rounding"); + var subjectAvgColor = getGradeColor( + subjectAvg, + t1: rounding.dbl("1"), + t2: rounding.dbl("2"), + t3: rounding.dbl("3"), + t4: rounding.dbl("4"), + ); return Padding( padding: const EdgeInsets.only(left: 20.0, right: 20.0, top: 12.0), diff --git a/firka/lib/ui/phone/pages/home/home_timetable.dart b/firka/lib/ui/phone/pages/home/home_timetable.dart index 0062cfc..e306136 100644 --- a/firka/lib/ui/phone/pages/home/home_timetable.dart +++ b/firka/lib/ui/phone/pages/home/home_timetable.dart @@ -608,6 +608,7 @@ class _HomeTimetableScreen extends FirkaState "dropdownLeft", size: 24, color: appStyle.colors.accent, + package: 'firka', ), ), onTap: () async { @@ -672,6 +673,7 @@ class _HomeTimetableScreen extends FirkaState "dropdownRight", size: 24, color: appStyle.colors.accent, + package: 'firka', ), ), onTap: () async { diff --git a/firka/lib/ui/phone/pages/home/home_timetable_mo.dart b/firka/lib/ui/phone/pages/home/home_timetable_mo.dart index e438f2f..f451096 100644 --- a/firka/lib/ui/phone/pages/home/home_timetable_mo.dart +++ b/firka/lib/ui/phone/pages/home/home_timetable_mo.dart @@ -438,6 +438,7 @@ class _HomeTimetableMonthlyScreen "dropdownLeft", size: 24, color: appStyle.colors.accent, + package: 'firka', ), ), onTap: () async { @@ -467,6 +468,7 @@ class _HomeTimetableMonthlyScreen "dropdownRight", size: 24, color: appStyle.colors.accent, + package: 'firka', ), onTap: () async { var newNow = DateTime(now!.year, now!.month + 1); diff --git a/firka/lib/ui/phone/screens/settings/settings_screen.dart b/firka/lib/ui/phone/screens/settings/settings_screen.dart index 02e3d36..bfc83a2 100644 --- a/firka/lib/ui/phone/screens/settings/settings_screen.dart +++ b/firka/lib/ui/phone/screens/settings/settings_screen.dart @@ -157,6 +157,11 @@ class _SettingsScreenState extends FirkaState { item.iconType!, item.iconData!, color: appStyle.colors.accent, + package: + item.iconType == FirkaIconType.icons || + item.iconType == FirkaIconType.majesticonsLocal + ? 'firka' + : null, ), ); cardWidgets.add(SizedBox(width: 8)); @@ -224,6 +229,12 @@ class _SettingsScreenState extends FirkaState { item.iconType!, item.iconData!, color: appStyle.colors.accent, + package: + item.iconType == FirkaIconType.icons || + item.iconType == + FirkaIconType.majesticonsLocal + ? 'firka' + : null, ), SizedBox(width: 4), ], @@ -265,6 +276,12 @@ class _SettingsScreenState extends FirkaState { item.iconType!, item.iconData!, color: appStyle.colors.accent, + package: + item.iconType == FirkaIconType.icons || + item.iconType == + FirkaIconType.majesticonsLocal + ? 'firka' + : null, ), SizedBox(width: 4), ], @@ -930,6 +947,7 @@ class _SettingsScreenState extends FirkaState { FirkaIconType.icons, "group", color: appStyle.colors.accent, + package: 'firka', ), SizedBox(width: 8), Text( @@ -1067,6 +1085,12 @@ class _SettingsScreenState extends FirkaState { item.iconType!, item.iconData!, color: appStyle.colors.accent, + package: + item.iconType == FirkaIconType.icons || + item.iconType == + FirkaIconType.majesticonsLocal + ? 'firka' + : null, ), SizedBox(width: 8), ], diff --git a/firka/lib/ui/phone/widgets/home_main_welcome.dart b/firka/lib/ui/phone/widgets/home_main_welcome.dart index cdc2170..4b90323 100644 --- a/firka/lib/ui/phone/widgets/home_main_welcome.dart +++ b/firka/lib/ui/phone/widgets/home_main_welcome.dart @@ -56,12 +56,14 @@ class _WelcomeWidgetState extends State { FirkaIconType.majesticonsLocal, "sunSolid", color: appStyle.colors.accent, + package: 'firka', ); case Cycle.day: return FirkaIconWidget( FirkaIconType.majesticonsLocal, "parkSolidSchool", color: appStyle.colors.accent, + package: 'firka', ); case Cycle.afternoon: return FirkaIconWidget( diff --git a/firka/lib/ui/phone/widgets/homework.dart b/firka/lib/ui/phone/widgets/homework.dart index 18634d8..956b650 100644 --- a/firka/lib/ui/phone/widgets/homework.dart +++ b/firka/lib/ui/phone/widgets/homework.dart @@ -34,6 +34,7 @@ class HomeworkWidget extends StatelessWidget { "homeWithMark", color: appStyle.colors.accent, size: 24, + package: 'firka', ) : FirkaIconWidget( FirkaIconType.majesticons, diff --git a/firka/lib/ui/phone/widgets/lesson.dart b/firka/lib/ui/phone/widgets/lesson.dart index a7a04fc..b9a795f 100644 --- a/firka/lib/ui/phone/widgets/lesson.dart +++ b/firka/lib/ui/phone/widgets/lesson.dart @@ -370,6 +370,7 @@ class LessonWidget extends StatelessWidget { 'cupFilled', color: appStyle.colors.accent, size: 24, + package: 'firka', ), ), ), diff --git a/firka/lib/ui/phone/widgets/lesson_big.dart b/firka/lib/ui/phone/widgets/lesson_big.dart index d3c5654..28acf96 100644 --- a/firka/lib/ui/phone/widgets/lesson_big.dart +++ b/firka/lib/ui/phone/widgets/lesson_big.dart @@ -245,6 +245,7 @@ class LessonBigWidget extends StatelessWidget { 'cupFilled', color: appStyle.colors.accent, size: 24, + package: 'firka', ), ), ), @@ -487,6 +488,7 @@ class LessonBigWidget extends StatelessWidget { 'cupFilled', color: appStyle.colors.accent, size: 24, + package: 'firka', ), ), ), diff --git a/firka/lib/ui/shared/class_icon.dart b/firka/lib/ui/shared/class_icon.dart index b689ed3..c650795 100644 --- a/firka/lib/ui/shared/class_icon.dart +++ b/firka/lib/ui/shared/class_icon.dart @@ -1,35 +1 @@ -import 'package:firka/core/icon_helper.dart'; -import 'package:flutter/material.dart'; - -import 'package:firka/ui/shared/firka_icon.dart'; - -class ClassIconWidget extends StatelessWidget { - final String _uid; - final String _className; - final String _category; - final Color color; - final double? size; - - const ClassIconWidget({ - super.key, - required String uid, - required String className, - required String category, - this.color = Colors.white, - this.size, - }) : _className = className, - _uid = uid, - _category = category; - - @override - Widget build(BuildContext context) { - var iconCategory = getIconType(_uid, _className, _category); - - return FirkaIconWidget( - FirkaIconType.majesticons, - getIconData(iconCategory), - color: color, - size: size, - ); - } -} +export 'package:firka_common/ui/shared/class_icon.dart'; diff --git a/firka/lib/ui/shared/counter_digit.dart b/firka/lib/ui/shared/counter_digit.dart index 16cf02f..88857fc 100644 --- a/firka/lib/ui/shared/counter_digit.dart +++ b/firka/lib/ui/shared/counter_digit.dart @@ -1,21 +1 @@ -import 'package:firka/ui/theme/style.dart'; -import 'package:flutter/material.dart'; - -class CounterDigitWidget extends StatelessWidget { - final String c; - final TextStyle? style; - - const CounterDigitWidget(this.c, this.style, {super.key}); - - @override - Widget build(BuildContext context) { - return Card( - shadowColor: Colors.transparent, - color: appStyle.colors.buttonSecondaryFill, - child: Padding( - padding: EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 4), - child: Text(c, style: style), - ), - ); - } -} +export 'package:firka_common/ui/shared/counter_digit.dart'; diff --git a/firka/lib/ui/shared/delayed_spinner.dart b/firka/lib/ui/shared/delayed_spinner.dart index d505ed5..d1176ae 100644 --- a/firka/lib/ui/shared/delayed_spinner.dart +++ b/firka/lib/ui/shared/delayed_spinner.dart @@ -1,45 +1 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; - -import 'package:firka/core/state/firka_state.dart'; -import 'package:firka/ui/theme/style.dart'; - -class DelayedSpinnerWidget extends StatefulWidget { - const DelayedSpinnerWidget({super.key}); - - @override - State createState() => _DelayedSpinner(); -} - -class _DelayedSpinner extends FirkaState { - Timer? timer; - bool showSpinner = false; - - @override - void initState() { - super.initState(); - - timer = Timer(Duration(milliseconds: 50), () { - setState(() { - showSpinner = true; - }); - }); - } - - @override - Widget build(BuildContext context) { - if (showSpinner) { - return CircularProgressIndicator(color: appStyle.colors.accent); - } else { - return SizedBox(); - } - } - - @override - void dispose() { - super.dispose(); - - timer?.cancel(); - } -} +export 'package:firka_common/ui/shared/delayed_spinner.dart'; diff --git a/firka/lib/ui/shared/firka_icon.dart b/firka/lib/ui/shared/firka_icon.dart index 6bc8118..347aeb2 100644 --- a/firka/lib/ui/shared/firka_icon.dart +++ b/firka/lib/ui/shared/firka_icon.dart @@ -1,42 +1 @@ -import 'dart:typed_data'; - -import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; -import 'package:majesticons_flutter/majesticons_flutter.dart'; - -enum FirkaIconType { icons, majesticons, majesticonsLocal } - -class FirkaIconWidget extends StatelessWidget { - final FirkaIconType iconType; - final Object iconData; - final Color color; - final double? size; - - const FirkaIconWidget( - this.iconType, - this.iconData, { - super.key, - this.color = Colors.white, - this.size, - }); - - @override - Widget build(BuildContext context) { - switch (iconType) { - case FirkaIconType.icons: - return SvgPicture.asset( - 'assets/icons/${iconData as String}.svg', - color: color, - height: size, - ); - case FirkaIconType.majesticons: - return Majesticon(iconData as Uint8List, color: color, size: size); - case FirkaIconType.majesticonsLocal: - return SvgPicture.asset( - 'assets/majesticons/${iconData as String}.svg', - color: color, - height: size, - ); - } - } -} +export 'package:firka_common/ui/shared/firka_icon.dart'; diff --git a/firka/lib/ui/shared/grade_small_card.dart b/firka/lib/ui/shared/grade_small_card.dart index 8c6fb24..e01f9cd 100644 --- a/firka/lib/ui/shared/grade_small_card.dart +++ b/firka/lib/ui/shared/grade_small_card.dart @@ -1,60 +1 @@ -import 'package:kreta_api/kreta_api.dart'; -import 'package:firka/ui/components/firka_card.dart'; -import 'package:firka/ui/components/grade_helpers.dart'; -import 'package:firka/ui/shared/class_icon.dart'; -import 'package:flutter/material.dart'; - -import 'package:firka/ui/theme/style.dart'; - -class GradeSmallCard extends FirkaCard { - final List grades; - final Subject subject; - - GradeSmallCard(this.grades, this.subject, {super.key}) - : super( - left: [ - ClassIconWidget( - uid: subject.uid, - className: subject.name, - category: subject.category.name!, - color: appStyle.colors.accent, - ), - SizedBox(width: 4), - SizedBox( - width: 200, - child: Text( - subject.name, - style: appStyle.fonts.B_16SB.apply( - color: appStyle.colors.textPrimary, - ), - ), - ), - ], - right: [ - grades.getAverageBySubject(subject).isNaN - ? SizedBox() - : Card( - shadowColor: Colors.transparent, - color: getGradeColor( - grades.getAverageBySubject(subject), - ).withAlpha(38), - child: Padding( - padding: EdgeInsets.only( - left: 8, - right: 8, - top: 4, - bottom: 4, - ), - child: Text( - grades.getAverageBySubject(subject).toStringAsFixed(2), - style: appStyle.fonts.B_16SB.apply( - color: getGradeColor( - grades.getAverageBySubject(subject), - ), - ), - ), - ), - ), - ], - ); -} +export 'package:firka_common/ui/shared/grade_small_card.dart'; diff --git a/firka/lib/ui/theme/style.dart b/firka/lib/ui/theme/style.dart index 691bbfd..8ce3436 100644 --- a/firka/lib/ui/theme/style.dart +++ b/firka/lib/ui/theme/style.dart @@ -1,309 +1 @@ -import 'package:flutter/material.dart'; - -// Design system token names; ignore non_constant_identifier_names for consistency with design specs -// ignore_for_file: non_constant_identifier_names - -class FirkaFonts { - TextStyle H_H1; - TextStyle H_18px; - TextStyle H_H2; - TextStyle H_16px; - TextStyle H_14px; - TextStyle H_12px; - - TextStyle H_16px_trimmed; - - TextStyle B_16R; - TextStyle B_16SB; - TextStyle B_15SB; - - TextStyle B_14R; - TextStyle B_14SB; - - TextStyle B_12R; - TextStyle B_12SB; - - TextStyle P_14; - TextStyle P_12; - - FirkaFonts({ - required this.H_H1, - required this.H_18px, - required this.H_H2, - required this.H_16px, - required this.H_14px, - required this.H_12px, - required this.H_16px_trimmed, - required this.B_16R, - required this.B_16SB, - required this.B_15SB, - required this.B_14R, - required this.B_14SB, - required this.B_12R, - required this.B_12SB, - required this.P_14, - required this.P_12, - }); -} - -class FirkaColors { - Color background; - Color backgroundAmoled; - Color background0p; - Color success; - int shadowBlur; - - Color textPrimary; - Color textSecondary; - Color textTertiary; - - Color textPrimaryLight; - Color textSecondaryLight; - Color textTertiaryLight; - - Color card; - Color cardTranslucent; - - Color buttonSecondaryFill; - - Color accent; - Color secondary; - Color shadowColor; - Color a10p; // 10% - Color a15p; // 15% - - Color warningAccent; - Color warningText; - Color warning15p; - Color warningCard; - - Color errorAccent; - Color errorText; - Color error15p; - Color errorCard; - - Color grade5; - Color grade4; - Color grade3; - Color grade2; - Color grade1; - - FirkaColors({ - required this.background, - required this.backgroundAmoled, - required this.background0p, - required this.success, - required this.shadowBlur, - required this.textPrimary, - required this.textSecondary, - required this.textTertiary, - required this.textPrimaryLight, - required this.textSecondaryLight, - required this.textTertiaryLight, - required this.card, - required this.cardTranslucent, - required this.buttonSecondaryFill, - required this.accent, - required this.secondary, - required this.shadowColor, - required this.a10p, - required this.a15p, - required this.warningAccent, - required this.warningText, - required this.warning15p, - required this.warningCard, - required this.errorAccent, - required this.errorText, - required this.error15p, - required this.errorCard, - required this.grade5, - required this.grade4, - required this.grade3, - required this.grade2, - required this.grade1, - }); -} - -class FirkaStyle { - FirkaColors colors; - FirkaFonts fonts; - bool isLight; - - FirkaStyle({ - required this.isLight, - required this.colors, - required this.fonts, - }); -} - -final _defaultFonts = FirkaFonts( - H_H1: TextStyle( - fontSize: 30, - fontFamily: 'Montserrat', - fontVariations: [FontVariation("wght", 700)], - ), - H_18px: TextStyle( - fontSize: 18, - fontFamily: 'Montserrat', - fontVariations: [FontVariation("wght", 700)], - ), - H_H2: TextStyle( - fontSize: 20, - fontFamily: 'Montserrat', - fontVariations: [FontVariation("wght", 700)], - ), - H_16px: TextStyle( - fontSize: 16, - fontFamily: 'Montserrat', - fontVariations: [FontVariation("wght", 600)], - ), - H_14px: TextStyle( - fontSize: 14, - fontFamily: 'Montserrat', - fontVariations: [FontVariation("wght", 600)], - ), - H_12px: TextStyle( - fontSize: 12, - fontFamily: 'Montserrat', - fontVariations: [FontVariation("wght", 600)], - ), - H_16px_trimmed: TextStyle( - fontSize: 16, - fontFamily: 'Montserrat', - fontVariations: [FontVariation("wght", 600)], - height: 1.3, - ), - B_16R: TextStyle( - fontSize: 16, - fontFamily: 'Figtree', - fontVariations: [FontVariation("wght", 600)], - height: 1.3, - ), - B_16SB: TextStyle( - fontSize: 16, - fontFamily: 'Figtree', - fontVariations: [FontVariation("wght", 700)], - height: 1.3, - ), - B_14R: TextStyle( - fontSize: 14, - fontFamily: 'Figtree', - fontVariations: [FontVariation("wght", 600)], - height: 1.3, - ), - B_14SB: TextStyle( - fontSize: 14, - fontFamily: 'Figtree', - fontVariations: [FontVariation("wght", 700)], - height: 1.3, - ), - B_15SB: TextStyle( - fontSize: 15, - fontFamily: 'Figtree', - fontVariations: [FontVariation("wght", 700)], - height: 1.3, - ), - B_12R: TextStyle( - fontSize: 12, - fontFamily: 'Figtree', - fontVariations: [FontVariation("wght", 600)], - height: 1.3, - ), - B_12SB: TextStyle( - fontSize: 12, - fontFamily: 'Figtree', - fontVariations: [FontVariation("wght", 700)], - height: 1.3, - ), - P_14: TextStyle( - fontSize: 14, - fontFamily: 'RobotoMono', - fontVariations: [FontVariation("wght", 700)], - ), - P_12: TextStyle( - fontSize: 12, - fontFamily: 'RobotoMono', - fontVariations: [FontVariation("wght", 700)], - ), -); - -final FirkaStyle lightStyle = FirkaStyle( - isLight: true, - colors: FirkaColors( - background: Color(0xFFFAFFF0), - backgroundAmoled: Colors.black, - background0p: Color(0x00fafff0), - success: Color(0xFF92EA3B), - shadowBlur: 2, - textPrimary: Color(0xFF394C0A), - textSecondary: Color(0xCC394C0A), - textTertiary: Color(0x80394C0A), - textPrimaryLight: Color(0xFF394C0A), - textSecondaryLight: Color(0xCC394C0A), - textTertiaryLight: Color(0x80394C0A), - card: Color(0xFFF3FBDE), - cardTranslucent: Color(0x80F3FBDE), - buttonSecondaryFill: Color(0xFFFEFFFD), - accent: Color(0xFFA7DC22), - secondary: Color(0xFF6E8F1B), - shadowColor: Color(0x33647e22), - a10p: Color(0x1aa7dc22), - a15p: Color(0x26a7dc22), - warningAccent: Color(0xFFFFA046), - warningText: Color(0xFF8F531B), - warning15p: Color(0x26FFA046), - warningCard: Color(0xFFFAEBDC), - errorAccent: Color(0xFFFF54A1), - errorText: Color(0xFF8F1B4F), - error15p: Color(0x26FF54A1), - errorCard: Color(0xFFFADCE9), - grade5: Color(0xFF22CCAD), - grade4: Color(0xFF92EA3B), - grade3: Color(0xFFF9CF00), - grade2: Color(0xFFFFA046), - grade1: Color(0xFFFF54A1), - ), - fonts: _defaultFonts, -); - -final FirkaStyle darkStyle = FirkaStyle( - isLight: false, - colors: FirkaColors( - background: Color(0xFF0D1202), - backgroundAmoled: Colors.black, - background0p: Color(0x00fafff0), - success: Color(0xFF92EA3B), - shadowBlur: 0, - textPrimary: Color(0xFFEAF7CC), - textSecondary: Color(0xB3EAF7CC), - textTertiary: Color(0x80EAF7CC), - textPrimaryLight: Color(0xFF394C0A), - textSecondaryLight: Color(0xCC394C0A), - textTertiaryLight: Color(0x80394C0A), - card: Color(0xFF141905), - cardTranslucent: Color(0x80141905), - buttonSecondaryFill: Color(0xFF20290B), - accent: Color(0xFFA7DC22), - secondary: Color(0xFFCBEE71), - shadowColor: Color(0x26CBEE71), - a10p: Color(0x1AA7DC22), - a15p: Color(0x26A7DC22), - warningAccent: Color(0xFFFFA046), - warningText: Color(0xFFF0B37A), - warning15p: Color(0x26FFA046), - warningCard: Color(0xFF201203), - errorAccent: Color(0xFFFF54A1), - errorText: Color(0xFFF59EC5), - error15p: Color(0x26FF54A1), - errorCard: Color(0xFF1E030F), - grade5: Color(0xFF22CCAD), - grade4: Color(0xFF92EA3B), - grade3: Color(0xFFF9CF00), - grade2: Color(0xFFFFA046), - grade1: Color(0xFFFF54A1), - ), - fonts: _defaultFonts, -); - -FirkaStyle appStyle = lightStyle; -FirkaStyle wearStyle = darkStyle; +export 'package:firka_common/ui/theme/style.dart'; diff --git a/firka/pubspec.yaml b/firka/pubspec.yaml index b60ee83..6cf647b 100644 --- a/firka/pubspec.yaml +++ b/firka/pubspec.yaml @@ -10,6 +10,8 @@ environment: dependencies: flutter: sdk: flutter + firka_common: + path: ../firka_common kreta_api: path: ../kreta_api diff --git a/firka_common/lib/core/debug_helper.dart b/firka_common/lib/core/debug_helper.dart new file mode 100644 index 0000000..027d8e8 --- /dev/null +++ b/firka_common/lib/core/debug_helper.dart @@ -0,0 +1,17 @@ +DateTime? debugFakeTime; +DateTime? debugSetAt; +var debugTimeAdvance = false; + +DateTime timeNow() { + if (debugFakeTime != null) { + if (debugTimeAdvance && debugSetAt != null) { + var diff = DateTime.now().difference(debugSetAt!); + + return debugFakeTime!.add(diff); + } else { + return debugFakeTime!; + } + } else { + return DateTime.now(); + } +} diff --git a/firka_common/lib/core/extensions.dart b/firka_common/lib/core/extensions.dart new file mode 100644 index 0000000..fd5388d --- /dev/null +++ b/firka_common/lib/core/extensions.dart @@ -0,0 +1,19 @@ +extension IterableExtensionMap on Iterable> { + Map toMap() { + var map = {}; + for (var item in this) { + map[item.key] = item.value; + } + + return map; + } +} + +extension IterableExtension on Iterable { + T? firstWhereOrNull(bool Function(T element) test) { + for (var element in this) { + if (test(element)) return element; + } + return null; + } +} diff --git a/firka_common/lib/core/icon_helper.dart b/firka_common/lib/core/icon_helper.dart new file mode 100644 index 0000000..6779666 --- /dev/null +++ b/firka_common/lib/core/icon_helper.dart @@ -0,0 +1,135 @@ +import 'dart:typed_data'; + +import 'package:majesticons_flutter/majesticons_flutter.dart'; + +enum ClassIcon { + mathematics, + grammar, + literature, + history, + geography, + art, + physics, + music, + pe, + chemistry, + biology, + env, + religion, + economics, + it, + code, + networking, + theatre, + film, + electricalEngineering, + mechanicalEngineering, + technika, + dance, + philosophy, + ofo, + diligence, + attitude, + language, + linux, + database, + applications, + project, +} + +Map _descriptors = { + ClassIcon.mathematics: RegExp(r'mate(k|matika)'), + ClassIcon.grammar: RegExp(r'magyar nyelv|nyelvtan'), + ClassIcon.literature: RegExp(r'irodalom'), + ClassIcon.history: RegExp(r'tor(i|tenelem)'), + ClassIcon.geography: RegExp(r'foldrajz'), + ClassIcon.art: RegExp(r'rajz|muvtori|muveszet|vizualis'), + ClassIcon.physics: RegExp(r'fizika'), + ClassIcon.music: RegExp(r'^enek|zene|szolfezs|zongora|korus'), + ClassIcon.pe: RegExp(r'^tes(i|tneveles)|sport|edzeselmelet'), + ClassIcon.chemistry: RegExp(r'kemia'), + ClassIcon.biology: RegExp(r'biologia'), + ClassIcon.env: RegExp( + r'kornyezet|termeszet ?(tudomany|ismeret)|hon( es nep)?ismeret', + ), + ClassIcon.religion: RegExp(r'(hit|erkolcs)tan|vallas|etika|bibliaismeret'), + ClassIcon.economics: RegExp(r'penzugy|gazdasag'), + ClassIcon.it: RegExp(r'informatika|szoftver|iroda|digitalis'), + ClassIcon.code: RegExp(r'prog|alkalmazas'), + ClassIcon.networking: RegExp(r'halozat'), + ClassIcon.theatre: RegExp(r'szinhaz'), + ClassIcon.film: RegExp(r'film|media'), + ClassIcon.electricalEngineering: RegExp(r'elektro(tech)?nika'), + ClassIcon.mechanicalEngineering: RegExp(r'gepesz|mernok|ipar'), + ClassIcon.technika: RegExp(r'technika'), + ClassIcon.dance: RegExp(r'tanc'), + ClassIcon.philosophy: RegExp(r'filozofia'), + ClassIcon.ofo: RegExp(r'osztaly(fonoki|kozosseg)|kozossegi|neveles'), + ClassIcon.diligence: RegExp(r'szorgalom'), + ClassIcon.attitude: RegExp(r'magatartas'), + ClassIcon.language: RegExp( + r'angol|nemet|francia|olasz|orosz|spanyol|latin|kinai|nyelv', + ), + ClassIcon.linux: RegExp(r'linux'), + ClassIcon.database: RegExp(r'adatbazis.*'), + ClassIcon.applications: RegExp(r'asztali alkalmazasok'), + ClassIcon.project: RegExp(r'projekt'), +}; + +Map _iconMap = { + ClassIcon.mathematics: Majesticon.calculatorSolid, + ClassIcon.grammar: Majesticon.bookSolid, + ClassIcon.literature: Majesticon.bookOpenSolid, + ClassIcon.history: Majesticon.compass2Solid, + ClassIcon.geography: Majesticon.globeEarth2Solid, + ClassIcon.art: Majesticon.editPen2Solid, + ClassIcon.music: Majesticon.musicNoteSolid, + ClassIcon.chemistry: Majesticon.testTubeFilledSolid, + ClassIcon.biology: Majesticon.covidSolid, + ClassIcon.it: Majesticon.laptopSolid, + ClassIcon.code: Majesticon.curlyBracesSolid, + ClassIcon.networking: Majesticon.cloudSolid, + ClassIcon.technika: Majesticon.ruler2Solid, + ClassIcon.language: Majesticon.tooltipsSolid, + ClassIcon.database: Majesticon.dataSolid, +}; + +ClassIcon? getIconType(String uid, String className, String category) { + ClassIcon? icon; + if (category.toLowerCase() == "matematika") { + icon = ClassIcon.mathematics; + } + + if (icon == null) { + for (var desc in _descriptors.entries) { + if (desc.value.hasMatch( + className + .replaceAll("ö", "o") + .replaceAll("ü", "u") + .replaceAll("ó", "o") + .replaceAll("ő", "o") + .replaceAll("ú", "u") + .replaceAll("é", "e") + .replaceAll("á", "a") + .replaceAll("ű", "u") + .replaceAll("í", "i") + .toLowerCase(), + )) { + icon = desc.key; + + break; + } + } + } + + return icon; +} + +Uint8List getIconData(ClassIcon? icon) { + if (icon == null) return Majesticon.alertCircleSolid; + + var iconData = _iconMap[icon]; + iconData ??= Majesticon.alertCircleSolid; + + return iconData; +} diff --git a/firka_common/lib/core/json_helper.dart b/firka_common/lib/core/json_helper.dart new file mode 100644 index 0000000..4e07713 --- /dev/null +++ b/firka_common/lib/core/json_helper.dart @@ -0,0 +1,9 @@ +List listToTyped(List dynamicList) { + var newList = List.empty(growable: true); + + for (var item in dynamicList) { + newList.add(item as T); + } + + return newList; +} diff --git a/firka_common/lib/firka_common.dart b/firka_common/lib/firka_common.dart new file mode 100644 index 0000000..b3f95c9 --- /dev/null +++ b/firka_common/lib/firka_common.dart @@ -0,0 +1,16 @@ +library firka_common; + +export 'core/debug_helper.dart'; +export 'core/extensions.dart'; +export 'core/icon_helper.dart'; +export 'core/json_helper.dart'; +export 'ui/components/firka_card.dart'; +export 'ui/components/firka_shadow.dart'; +export 'ui/components/grade.dart'; +export 'ui/components/grade_helpers.dart'; +export 'ui/shared/class_icon.dart'; +export 'ui/shared/counter_digit.dart'; +export 'ui/shared/delayed_spinner.dart'; +export 'ui/shared/firka_icon.dart'; +export 'ui/shared/grade_small_card.dart'; +export 'ui/theme/style.dart'; diff --git a/firka_common/lib/ui/components/firka_card.dart b/firka_common/lib/ui/components/firka_card.dart new file mode 100644 index 0000000..b29a260 --- /dev/null +++ b/firka_common/lib/ui/components/firka_card.dart @@ -0,0 +1,137 @@ +import 'package:flutter/material.dart'; + +import 'package:firka_common/ui/components/firka_shadow.dart'; +import 'package:firka_common/ui/theme/style.dart'; + +enum Attach { none, bottom, top } + +class FirkaCard extends StatelessWidget { + final List left; + final List? center; + final double? height; + final List? right; + final bool shadow; + final Widget? extra; + final Attach? attached; + final Color? color; + final bool? isLightMode; + + const FirkaCard({ + required this.left, + this.shadow = true, + this.center, + this.right, + this.extra, + this.attached, + this.color, + this.height, + this.isLightMode, + super.key, + }); + + @override + Widget build(BuildContext context) { + var right = this.right ?? []; + + var attached = this.attached != null ? this.attached! : Attach.none; + final defaultRounding = 16.0; + final attachedRounding = 8.0; + final isLight = + isLightMode ?? Theme.of(context).brightness == Brightness.light; + + if (extra != null) { + return SizedBox( + width: MediaQuery.of(context).size.width, + height: height, + child: FirkaShadow( + shadow: shadow, + isLightMode: isLight, + child: Card( + color: color ?? appStyle.colors.card, + shadowColor: isLight && shadow ? null : Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular( + attached == Attach.top ? attachedRounding : defaultRounding, + ), + topRight: Radius.circular( + attached == Attach.top ? attachedRounding : defaultRounding, + ), + bottomLeft: Radius.circular( + attached == Attach.bottom + ? attachedRounding + : defaultRounding, + ), + bottomRight: Radius.circular( + attached == Attach.bottom + ? attachedRounding + : defaultRounding, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row(children: left), + Row(children: center ?? []), + Row(children: right), + ], + ), + extra ?? const SizedBox(), + ], + ), + ), + ), + ), + ); + } else { + return SizedBox( + width: MediaQuery.of(context).size.width, + height: height, + child: FirkaShadow( + shadow: shadow, + isLightMode: isLight, + child: Card( + color: color ?? appStyle.colors.card, + shadowColor: isLight && shadow ? null : Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular( + attached == Attach.top ? attachedRounding : defaultRounding, + ), + topRight: Radius.circular( + attached == Attach.top ? attachedRounding : defaultRounding, + ), + bottomLeft: Radius.circular( + attached == Attach.bottom + ? attachedRounding + : defaultRounding, + ), + bottomRight: Radius.circular( + attached == Attach.bottom + ? attachedRounding + : defaultRounding, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row(children: left), + Row(children: center ?? []), + Row(children: right), + ], + ), + ), + ), + ), + ); + } + } +} diff --git a/firka_common/lib/ui/components/firka_shadow.dart b/firka_common/lib/ui/components/firka_shadow.dart new file mode 100644 index 0000000..5c1a24c --- /dev/null +++ b/firka_common/lib/ui/components/firka_shadow.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; + +import 'package:firka_common/ui/theme/style.dart'; + +class FirkaShadow extends StatelessWidget { + final Widget child; + final bool shadow; + final double radius; + final bool? isLightMode; + + const FirkaShadow({ + required this.shadow, + required this.child, + this.radius = 16.0, + this.isLightMode, + super.key, + }); + + @override + Widget build(BuildContext context) { + final isLight = + isLightMode ?? Theme.of(context).brightness == Brightness.light; + final borderRadius = BorderRadius.circular(radius); + + final shadowBox = BoxDecoration( + color: Colors.transparent, + shape: BoxShape.rectangle, + boxShadow: [ + BoxShadow( + color: appStyle.colors.shadowColor, + spreadRadius: -4, + blurRadius: 0, + offset: const Offset(0, 2), + ), + ], + borderRadius: BorderRadius.all(Radius.circular(radius)), + ); + + if (!shadow) { + return ClipRRect(borderRadius: borderRadius, child: child); + } + + if (isLight) { + return child; + } else { + return Container( + decoration: shadowBox, + child: ClipRRect(borderRadius: borderRadius, child: child), + ); + } + } +} diff --git a/firka_common/lib/ui/components/grade.dart b/firka_common/lib/ui/components/grade.dart new file mode 100644 index 0000000..0a2a555 --- /dev/null +++ b/firka_common/lib/ui/components/grade.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:kreta_api/kreta_api.dart'; + +import 'package:firka_common/ui/components/grade_helpers.dart'; +import 'package:firka_common/ui/theme/style.dart'; + +class GradeWidget extends StatelessWidget { + const GradeWidget(this.grade, {super.key}) + : gradeValue = null, + _fromValue = false; + + const GradeWidget.gradeValue(int value, {super.key}) + : grade = null, + gradeValue = value, + _fromValue = true; + + final Grade? grade; + final int? gradeValue; + final bool _fromValue; + + @override + Widget build(BuildContext context) { + if (_fromValue && gradeValue != null) { + return _buildNumericCircle( + gradeValue!, + getGradeColor(gradeValue!.toDouble()), + ); + } + + final g = grade!; + Color gradeColor = appStyle.colors.grade1; + final gradeStr = g.numericValue?.toString() ?? '0'; + + if (g.valueType.name == 'Szazalekos') { + if (g.numericValue != null) { + gradeColor = getGradeColor( + percentageToGrade(g.numericValue!).toDouble(), + ); + } + + final str = g.strValue.replaceAll('%', ''); + return Card( + shape: const CircleBorder(), + shadowColor: Colors.transparent, + color: gradeColor.withAlpha(38), + child: Padding( + padding: const EdgeInsets.all(8), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(str, style: appStyle.fonts.P_14.copyWith(color: gradeColor)), + Text('%', style: appStyle.fonts.P_12.copyWith(color: gradeColor)), + ], + ), + ), + ); + } + + if (g.numericValue != null) { + gradeColor = getGradeColor(g.numericValue!.toDouble()); + } + + if (gradeStr == '0') { + return Card( + shadowColor: Colors.transparent, + color: gradeColor.withAlpha(38), + child: Padding( + padding: const EdgeInsets.only(left: 8, right: 8, top: 2, bottom: 2), + child: Text( + g.strValue, + style: appStyle.fonts.H_H1.copyWith( + fontSize: 16, + color: gradeColor, + ), + ), + ), + ); + } + + return _buildNumericCircle(g.numericValue!, gradeColor); + } + + Widget _buildNumericCircle(int value, Color gradeColor) { + return Card( + shape: const CircleBorder(), + shadowColor: Colors.transparent, + color: gradeColor.withAlpha(38), + child: Padding( + padding: const EdgeInsets.only(left: 8, right: 8), + child: Text( + value.toString(), + style: appStyle.fonts.H_H1.copyWith(fontSize: 24, color: gradeColor), + ), + ), + ); + } +} diff --git a/firka_common/lib/ui/components/grade_helpers.dart b/firka_common/lib/ui/components/grade_helpers.dart new file mode 100644 index 0000000..7d28172 --- /dev/null +++ b/firka_common/lib/ui/components/grade_helpers.dart @@ -0,0 +1,100 @@ +import 'dart:ui'; + +import 'package:firka_common/ui/theme/style.dart'; +import 'package:kreta_api/kreta_api.dart'; + +int roundGrade( + double grade, { + double t1 = 1, + double t2 = 0.5, + double t3 = 0.5, + double t4 = 0.5, +}) { + if (grade < 1 + t1) { + return 1; + } + if (grade < 2 + t2) { + return 2; + } + if (grade < 3 + t3) { + return 3; + } + if (grade < 4 + t4) { + return 4; + } + + return 5; +} + +int percentageToGrade(int grade) { + if (grade < 50) { + return 1; + } + if (grade < 60) { + return 2; + } + if (grade < 70) { + return 3; + } + if (grade < 80) { + return 4; + } + + return 5; +} + +Color getGradeColor( + double grade, { + double t1 = 1, + double t2 = 0.5, + double t3 = 0.5, + double t4 = 0.5, +}) { + switch (roundGrade(grade, t1: t1, t2: t2, t3: t3, t4: t4)) { + case 2: + return appStyle.colors.grade2; + case 3: + return appStyle.colors.grade3; + case 4: + return appStyle.colors.grade4; + case 5: + return appStyle.colors.grade5; + default: + return appStyle.colors.grade1; + } +} + +(int total, List countsByGrade) getGradeDistribution(List grades) { + final filtered = grades + .where((g) => g.type.name != "felevi_jegy_ertekeles") + .toList(); + final counts = [0, 0, 0, 0, 0]; + for (final g in filtered) { + if (g.numericValue == null) continue; + final value = g.valueType.name == "Szazalekos" + ? percentageToGrade(g.numericValue!.round()) + : g.numericValue!.round().clamp(1, 5); + counts[value - 1]++; + } + return (filtered.length, counts); +} + +extension GradeListExtension on List { + double getAverageBySubject(Subject subject) { + var weightTotal = 0.00; + var sum = 0.00; + + for (var grade in this) { + if (grade.subject.uid == subject.uid) { + if (grade.numericValue != null) { + var weight = (grade.weightPercentage ?? 100) / 100.0; + weightTotal += weight; + + sum += grade.numericValue! * weight; + } + } + } + + return sum / weightTotal; + } +} diff --git a/firka_common/lib/ui/shared/class_icon.dart b/firka_common/lib/ui/shared/class_icon.dart new file mode 100644 index 0000000..1ee71aa --- /dev/null +++ b/firka_common/lib/ui/shared/class_icon.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; + +import 'package:firka_common/core/icon_helper.dart'; +import 'package:firka_common/ui/shared/firka_icon.dart'; + +class ClassIconWidget extends StatelessWidget { + final String _uid; + final String _className; + final String _category; + final Color color; + final double? size; + + const ClassIconWidget({ + super.key, + required String uid, + required String className, + required String category, + this.color = Colors.white, + this.size, + }) : _className = className, + _uid = uid, + _category = category; + + @override + Widget build(BuildContext context) { + var iconCategory = getIconType(_uid, _className, _category); + + return FirkaIconWidget( + FirkaIconType.majesticons, + getIconData(iconCategory), + color: color, + size: size, + ); + } +} diff --git a/firka_common/lib/ui/shared/counter_digit.dart b/firka_common/lib/ui/shared/counter_digit.dart new file mode 100644 index 0000000..4ce82e0 --- /dev/null +++ b/firka_common/lib/ui/shared/counter_digit.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +import 'package:firka_common/ui/theme/style.dart'; + +class CounterDigitWidget extends StatelessWidget { + final String c; + final TextStyle? style; + + const CounterDigitWidget(this.c, this.style, {super.key}); + + @override + Widget build(BuildContext context) { + return Card( + shadowColor: Colors.transparent, + color: appStyle.colors.buttonSecondaryFill, + child: Padding( + padding: const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 4), + child: Text(c, style: style), + ), + ); + } +} diff --git a/firka_common/lib/ui/shared/delayed_spinner.dart b/firka_common/lib/ui/shared/delayed_spinner.dart new file mode 100644 index 0000000..a86c37d --- /dev/null +++ b/firka_common/lib/ui/shared/delayed_spinner.dart @@ -0,0 +1,48 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:firka_common/ui/theme/style.dart'; + +class DelayedSpinnerWidget extends StatefulWidget { + final Color? color; + + const DelayedSpinnerWidget({super.key, this.color}); + + @override + State createState() => _DelayedSpinner(); +} + +class _DelayedSpinner extends State { + Timer? timer; + bool showSpinner = false; + + @override + void initState() { + super.initState(); + + timer = Timer(const Duration(milliseconds: 50), () { + setState(() { + showSpinner = true; + }); + }); + } + + @override + Widget build(BuildContext context) { + if (showSpinner) { + return CircularProgressIndicator( + color: widget.color ?? appStyle.colors.accent, + ); + } else { + return const SizedBox(); + } + } + + @override + void dispose() { + super.dispose(); + + timer?.cancel(); + } +} diff --git a/firka_common/lib/ui/shared/firka_icon.dart b/firka_common/lib/ui/shared/firka_icon.dart new file mode 100644 index 0000000..2fc3236 --- /dev/null +++ b/firka_common/lib/ui/shared/firka_icon.dart @@ -0,0 +1,46 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:majesticons_flutter/majesticons_flutter.dart'; + +enum FirkaIconType { icons, majesticons, majesticonsLocal } + +class FirkaIconWidget extends StatelessWidget { + final FirkaIconType iconType; + final Object iconData; + final Color color; + final double? size; + final String? package; + + const FirkaIconWidget( + this.iconType, + this.iconData, { + super.key, + this.color = Colors.white, + this.size, + this.package, + }); + + @override + Widget build(BuildContext context) { + switch (iconType) { + case FirkaIconType.icons: + return SvgPicture.asset( + 'assets/icons/${iconData as String}.svg', + color: color, + height: size, + package: package, + ); + case FirkaIconType.majesticons: + return Majesticon(iconData as Uint8List, color: color, size: size); + case FirkaIconType.majesticonsLocal: + return SvgPicture.asset( + 'assets/majesticons/${iconData as String}.svg', + color: color, + height: size, + package: package, + ); + } + } +} diff --git a/firka_common/lib/ui/shared/grade_small_card.dart b/firka_common/lib/ui/shared/grade_small_card.dart new file mode 100644 index 0000000..f18252e --- /dev/null +++ b/firka_common/lib/ui/shared/grade_small_card.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:kreta_api/kreta_api.dart'; + +import 'package:firka_common/ui/components/firka_card.dart'; +import 'package:firka_common/ui/components/grade_helpers.dart'; +import 'package:firka_common/ui/shared/class_icon.dart'; +import 'package:firka_common/ui/theme/style.dart'; + +class GradeSmallCard extends FirkaCard { + final List grades; + final Subject subject; + + GradeSmallCard(this.grades, this.subject, {super.key}) + : super( + left: [ + ClassIconWidget( + uid: subject.uid, + className: subject.name, + category: subject.category.name!, + color: appStyle.colors.accent, + ), + const SizedBox(width: 4), + SizedBox( + width: 200, + child: Text( + subject.name, + style: appStyle.fonts.B_16SB.apply( + color: appStyle.colors.textPrimary, + ), + ), + ), + ], + right: [ + grades.getAverageBySubject(subject).isNaN + ? const SizedBox() + : Card( + shadowColor: Colors.transparent, + color: getGradeColor( + grades.getAverageBySubject(subject), + ).withAlpha(38), + child: Padding( + padding: const EdgeInsets.only( + left: 8, + right: 8, + top: 4, + bottom: 4, + ), + child: Text( + grades.getAverageBySubject(subject).toStringAsFixed(2), + style: appStyle.fonts.B_16SB.apply( + color: getGradeColor( + grades.getAverageBySubject(subject), + ), + ), + ), + ), + ), + ], + ); +} diff --git a/firka_common/lib/ui/theme/style.dart b/firka_common/lib/ui/theme/style.dart new file mode 100644 index 0000000..691bbfd --- /dev/null +++ b/firka_common/lib/ui/theme/style.dart @@ -0,0 +1,309 @@ +import 'package:flutter/material.dart'; + +// Design system token names; ignore non_constant_identifier_names for consistency with design specs +// ignore_for_file: non_constant_identifier_names + +class FirkaFonts { + TextStyle H_H1; + TextStyle H_18px; + TextStyle H_H2; + TextStyle H_16px; + TextStyle H_14px; + TextStyle H_12px; + + TextStyle H_16px_trimmed; + + TextStyle B_16R; + TextStyle B_16SB; + TextStyle B_15SB; + + TextStyle B_14R; + TextStyle B_14SB; + + TextStyle B_12R; + TextStyle B_12SB; + + TextStyle P_14; + TextStyle P_12; + + FirkaFonts({ + required this.H_H1, + required this.H_18px, + required this.H_H2, + required this.H_16px, + required this.H_14px, + required this.H_12px, + required this.H_16px_trimmed, + required this.B_16R, + required this.B_16SB, + required this.B_15SB, + required this.B_14R, + required this.B_14SB, + required this.B_12R, + required this.B_12SB, + required this.P_14, + required this.P_12, + }); +} + +class FirkaColors { + Color background; + Color backgroundAmoled; + Color background0p; + Color success; + int shadowBlur; + + Color textPrimary; + Color textSecondary; + Color textTertiary; + + Color textPrimaryLight; + Color textSecondaryLight; + Color textTertiaryLight; + + Color card; + Color cardTranslucent; + + Color buttonSecondaryFill; + + Color accent; + Color secondary; + Color shadowColor; + Color a10p; // 10% + Color a15p; // 15% + + Color warningAccent; + Color warningText; + Color warning15p; + Color warningCard; + + Color errorAccent; + Color errorText; + Color error15p; + Color errorCard; + + Color grade5; + Color grade4; + Color grade3; + Color grade2; + Color grade1; + + FirkaColors({ + required this.background, + required this.backgroundAmoled, + required this.background0p, + required this.success, + required this.shadowBlur, + required this.textPrimary, + required this.textSecondary, + required this.textTertiary, + required this.textPrimaryLight, + required this.textSecondaryLight, + required this.textTertiaryLight, + required this.card, + required this.cardTranslucent, + required this.buttonSecondaryFill, + required this.accent, + required this.secondary, + required this.shadowColor, + required this.a10p, + required this.a15p, + required this.warningAccent, + required this.warningText, + required this.warning15p, + required this.warningCard, + required this.errorAccent, + required this.errorText, + required this.error15p, + required this.errorCard, + required this.grade5, + required this.grade4, + required this.grade3, + required this.grade2, + required this.grade1, + }); +} + +class FirkaStyle { + FirkaColors colors; + FirkaFonts fonts; + bool isLight; + + FirkaStyle({ + required this.isLight, + required this.colors, + required this.fonts, + }); +} + +final _defaultFonts = FirkaFonts( + H_H1: TextStyle( + fontSize: 30, + fontFamily: 'Montserrat', + fontVariations: [FontVariation("wght", 700)], + ), + H_18px: TextStyle( + fontSize: 18, + fontFamily: 'Montserrat', + fontVariations: [FontVariation("wght", 700)], + ), + H_H2: TextStyle( + fontSize: 20, + fontFamily: 'Montserrat', + fontVariations: [FontVariation("wght", 700)], + ), + H_16px: TextStyle( + fontSize: 16, + fontFamily: 'Montserrat', + fontVariations: [FontVariation("wght", 600)], + ), + H_14px: TextStyle( + fontSize: 14, + fontFamily: 'Montserrat', + fontVariations: [FontVariation("wght", 600)], + ), + H_12px: TextStyle( + fontSize: 12, + fontFamily: 'Montserrat', + fontVariations: [FontVariation("wght", 600)], + ), + H_16px_trimmed: TextStyle( + fontSize: 16, + fontFamily: 'Montserrat', + fontVariations: [FontVariation("wght", 600)], + height: 1.3, + ), + B_16R: TextStyle( + fontSize: 16, + fontFamily: 'Figtree', + fontVariations: [FontVariation("wght", 600)], + height: 1.3, + ), + B_16SB: TextStyle( + fontSize: 16, + fontFamily: 'Figtree', + fontVariations: [FontVariation("wght", 700)], + height: 1.3, + ), + B_14R: TextStyle( + fontSize: 14, + fontFamily: 'Figtree', + fontVariations: [FontVariation("wght", 600)], + height: 1.3, + ), + B_14SB: TextStyle( + fontSize: 14, + fontFamily: 'Figtree', + fontVariations: [FontVariation("wght", 700)], + height: 1.3, + ), + B_15SB: TextStyle( + fontSize: 15, + fontFamily: 'Figtree', + fontVariations: [FontVariation("wght", 700)], + height: 1.3, + ), + B_12R: TextStyle( + fontSize: 12, + fontFamily: 'Figtree', + fontVariations: [FontVariation("wght", 600)], + height: 1.3, + ), + B_12SB: TextStyle( + fontSize: 12, + fontFamily: 'Figtree', + fontVariations: [FontVariation("wght", 700)], + height: 1.3, + ), + P_14: TextStyle( + fontSize: 14, + fontFamily: 'RobotoMono', + fontVariations: [FontVariation("wght", 700)], + ), + P_12: TextStyle( + fontSize: 12, + fontFamily: 'RobotoMono', + fontVariations: [FontVariation("wght", 700)], + ), +); + +final FirkaStyle lightStyle = FirkaStyle( + isLight: true, + colors: FirkaColors( + background: Color(0xFFFAFFF0), + backgroundAmoled: Colors.black, + background0p: Color(0x00fafff0), + success: Color(0xFF92EA3B), + shadowBlur: 2, + textPrimary: Color(0xFF394C0A), + textSecondary: Color(0xCC394C0A), + textTertiary: Color(0x80394C0A), + textPrimaryLight: Color(0xFF394C0A), + textSecondaryLight: Color(0xCC394C0A), + textTertiaryLight: Color(0x80394C0A), + card: Color(0xFFF3FBDE), + cardTranslucent: Color(0x80F3FBDE), + buttonSecondaryFill: Color(0xFFFEFFFD), + accent: Color(0xFFA7DC22), + secondary: Color(0xFF6E8F1B), + shadowColor: Color(0x33647e22), + a10p: Color(0x1aa7dc22), + a15p: Color(0x26a7dc22), + warningAccent: Color(0xFFFFA046), + warningText: Color(0xFF8F531B), + warning15p: Color(0x26FFA046), + warningCard: Color(0xFFFAEBDC), + errorAccent: Color(0xFFFF54A1), + errorText: Color(0xFF8F1B4F), + error15p: Color(0x26FF54A1), + errorCard: Color(0xFFFADCE9), + grade5: Color(0xFF22CCAD), + grade4: Color(0xFF92EA3B), + grade3: Color(0xFFF9CF00), + grade2: Color(0xFFFFA046), + grade1: Color(0xFFFF54A1), + ), + fonts: _defaultFonts, +); + +final FirkaStyle darkStyle = FirkaStyle( + isLight: false, + colors: FirkaColors( + background: Color(0xFF0D1202), + backgroundAmoled: Colors.black, + background0p: Color(0x00fafff0), + success: Color(0xFF92EA3B), + shadowBlur: 0, + textPrimary: Color(0xFFEAF7CC), + textSecondary: Color(0xB3EAF7CC), + textTertiary: Color(0x80EAF7CC), + textPrimaryLight: Color(0xFF394C0A), + textSecondaryLight: Color(0xCC394C0A), + textTertiaryLight: Color(0x80394C0A), + card: Color(0xFF141905), + cardTranslucent: Color(0x80141905), + buttonSecondaryFill: Color(0xFF20290B), + accent: Color(0xFFA7DC22), + secondary: Color(0xFFCBEE71), + shadowColor: Color(0x26CBEE71), + a10p: Color(0x1AA7DC22), + a15p: Color(0x26A7DC22), + warningAccent: Color(0xFFFFA046), + warningText: Color(0xFFF0B37A), + warning15p: Color(0x26FFA046), + warningCard: Color(0xFF201203), + errorAccent: Color(0xFFFF54A1), + errorText: Color(0xFFF59EC5), + error15p: Color(0x26FF54A1), + errorCard: Color(0xFF1E030F), + grade5: Color(0xFF22CCAD), + grade4: Color(0xFF92EA3B), + grade3: Color(0xFFF9CF00), + grade2: Color(0xFFFFA046), + grade1: Color(0xFFFF54A1), + ), + fonts: _defaultFonts, +); + +FirkaStyle appStyle = lightStyle; +FirkaStyle wearStyle = darkStyle; diff --git a/firka_common/pubspec.yaml b/firka_common/pubspec.yaml new file mode 100644 index 0000000..61b4c41 --- /dev/null +++ b/firka_common/pubspec.yaml @@ -0,0 +1,21 @@ +name: firka_common +description: Shared widgets, data structures, and helpers for firka and firka_wear. +publish_to: "none" +version: 0.1.0 + +environment: + sdk: ^3.11.0 + +dependencies: + flutter: + sdk: flutter + kreta_api: + path: ../kreta_api + majesticons_flutter: ^0.0.1 + flutter_svg: ^1.1.6 + intl: any + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^6.0.0 diff --git a/firka_wear/lib/core/debug_helper.dart b/firka_wear/lib/core/debug_helper.dart index 027d8e8..d374108 100644 --- a/firka_wear/lib/core/debug_helper.dart +++ b/firka_wear/lib/core/debug_helper.dart @@ -1,17 +1 @@ -DateTime? debugFakeTime; -DateTime? debugSetAt; -var debugTimeAdvance = false; - -DateTime timeNow() { - if (debugFakeTime != null) { - if (debugTimeAdvance && debugSetAt != null) { - var diff = DateTime.now().difference(debugSetAt!); - - return debugFakeTime!.add(diff); - } else { - return debugFakeTime!; - } - } else { - return DateTime.now(); - } -} +export 'package:firka_common/core/debug_helper.dart'; diff --git a/firka_wear/lib/core/extensions.dart b/firka_wear/lib/core/extensions.dart index ec9cd2b..179e003 100644 --- a/firka_wear/lib/core/extensions.dart +++ b/firka_wear/lib/core/extensions.dart @@ -3,27 +3,10 @@ import 'package:intl/intl.dart'; import 'package:firka_wear/core/debug_helper.dart'; import 'package:firka_wear/l10n/app_localizations.dart'; +import 'package:firka_common/core/extensions.dart'; import 'package:kreta_api/kreta_api.dart'; -extension IterableExtensionMap on Iterable> { - Map toMap() { - var map = {}; - for (var item in this) { - map[item.key] = item.value; - } - - return map; - } -} - -extension IterableExtension on Iterable { - T? firstWhereOrNull(bool Function(T element) test) { - for (var element in this) { - if (test(element)) return element; - } - return null; - } -} +export 'package:firka_common/core/extensions.dart'; extension DurationExtension on Duration { String formatDuration() { diff --git a/firka_wear/lib/core/icon_helper.dart b/firka_wear/lib/core/icon_helper.dart index 34d5343..dbf6f91 100644 --- a/firka_wear/lib/core/icon_helper.dart +++ b/firka_wear/lib/core/icon_helper.dart @@ -1,152 +1 @@ -import 'dart:typed_data'; - -import 'package:majesticons_flutter/majesticons_flutter.dart'; - -enum ClassIcon { - mathematics, - grammar, - literature, - history, - geography, - art, - physics, - music, - pe, - chemistry, - biology, - env, - religion, - economics, - it, - code, - networking, - theatre, - film, - electricalEngineering, - mechanicalEngineering, - technika, - dance, - philosophy, - ofo, - diligence, - attitude, - language, - linux, - database, - applications, - project, -} - -Map _descriptors = { - ClassIcon.mathematics: RegExp(r'mate(k|matika)'), - ClassIcon.grammar: RegExp(r'magyar nyelv|nyelvtan'), - ClassIcon.literature: RegExp(r'irodalom'), - ClassIcon.history: RegExp(r'tor(i|tenelem)'), - ClassIcon.geography: RegExp(r'foldrajz'), - ClassIcon.art: RegExp(r'rajz|muvtori|muveszet|vizualis'), - ClassIcon.physics: RegExp(r'fizika'), - ClassIcon.music: RegExp(r'^enek|zene|szolfezs|zongora|korus'), - ClassIcon.pe: RegExp(r'^tes(i|tneveles)|sport|edzeselmelet'), - ClassIcon.chemistry: RegExp(r'kemia'), - ClassIcon.biology: RegExp(r'biologia'), - ClassIcon.env: RegExp( - r'kornyezet|termeszet ?(tudomany|ismeret)|hon( es nep)?ismeret', - ), - ClassIcon.religion: RegExp(r'(hit|erkolcs)tan|vallas|etika|bibliaismeret'), - ClassIcon.economics: RegExp(r'penzugy|gazdasag'), - ClassIcon.it: RegExp(r'informatika|szoftver|iroda|digitalis'), - ClassIcon.code: RegExp(r'prog|alkalmazas'), - ClassIcon.networking: RegExp(r'halozat'), - ClassIcon.theatre: RegExp(r'szinhaz'), - ClassIcon.film: RegExp(r'film|media'), - ClassIcon.electricalEngineering: RegExp(r'elektro(tech)?nika'), - ClassIcon.mechanicalEngineering: RegExp(r'gepesz|mernok|ipar'), - ClassIcon.technika: RegExp(r'technika'), - ClassIcon.dance: RegExp(r'tanc'), - ClassIcon.philosophy: RegExp(r'filozofia'), - ClassIcon.ofo: RegExp(r'osztaly(fonoki|kozosseg)|kozossegi|neveles'), - ClassIcon.diligence: RegExp(r'szorgalom'), - ClassIcon.attitude: RegExp(r'magatartas'), - ClassIcon.language: RegExp( - r'angol|nemet|francia|olasz|orosz|spanyol|latin|kinai|nyelv', - ), - ClassIcon.linux: RegExp(r'linux'), - ClassIcon.database: RegExp(r'adatbazis.*'), - ClassIcon.applications: RegExp(r'asztali alkalmazasok'), - ClassIcon.project: RegExp(r'projekt'), -}; - -Map _iconMap = { - ClassIcon.mathematics: Majesticon.calculatorSolid, - ClassIcon.grammar: Majesticon.bookSolid, - ClassIcon.literature: Majesticon.bookOpenSolid, - ClassIcon.history: Majesticon.compass2Solid, - ClassIcon.geography: Majesticon.globeEarth2Solid, - ClassIcon.art: Majesticon.editPen2Solid, - // ClassIcon.physics: , - ClassIcon.music: Majesticon.musicNoteSolid, - // ClassIcon.pe: , - ClassIcon.chemistry: Majesticon.testTubeFilledSolid, - ClassIcon.biology: Majesticon.covidSolid, - // ClassIcon.env: , - // ClassIcon.religion: , - // ClassIcon.economics: , - ClassIcon.it: Majesticon.laptopSolid, - ClassIcon.code: Majesticon.curlyBracesSolid, - ClassIcon.networking: Majesticon.cloudSolid, - // ClassIcon.theatre: , - // ClassIcon.film: , - // ClassIcon.electricalEngineering: , - // ClassIcon.mechanicalEngineering: , - ClassIcon.technika: Majesticon.ruler2Solid, - // ClassIcon.dance: , - // ClassIcon.philosophy: , - // ClassIcon.ofo: , - // ClassIcon.diligence: , - // ClassIcon.attitude: , - ClassIcon.language: Majesticon.tooltipsSolid, - // ClassIcon.linux: , - ClassIcon.database: Majesticon.dataSolid, - // ClassIcon.applications: , - // ClassIcon.project: , -}; - -ClassIcon? getIconType(String uid, String className, String category) { - ClassIcon? icon; - if (category.toLowerCase() == "matematika") { - icon = ClassIcon.mathematics; - } - - if (icon == null) { - for (var desc in _descriptors.entries) { - if (desc.value.hasMatch( - className - .replaceAll("ö", "o") - .replaceAll("ü", "u") - .replaceAll("ó", "o") - .replaceAll("ő", "o") - .replaceAll("ú", "u") - .replaceAll("é", "e") - .replaceAll("á", "a") - .replaceAll("ű", "u") - .replaceAll("í", "i") - .toLowerCase(), - )) { - icon = desc.key; - - break; - } - } - } - - return icon; -} - -Uint8List getIconData(ClassIcon? icon) { - if (icon == null) return Majesticon.alertCircleSolid; - - var iconData = _iconMap[icon]; - iconData ??= Majesticon.alertCircleSolid; - - return iconData; -} +export 'package:firka_common/core/icon_helper.dart'; diff --git a/firka_wear/lib/core/json_helper.dart b/firka_wear/lib/core/json_helper.dart index 4e07713..cd391aa 100644 --- a/firka_wear/lib/core/json_helper.dart +++ b/firka_wear/lib/core/json_helper.dart @@ -1,9 +1 @@ -List listToTyped(List dynamicList) { - var newList = List.empty(growable: true); - - for (var item in dynamicList) { - newList.add(item as T); - } - - return newList; -} +export 'package:firka_common/core/json_helper.dart'; diff --git a/firka_wear/lib/data/models/homework_cache_model.dart b/firka_wear/lib/data/models/homework_cache_model.dart index a38682d..2621cd3 100644 --- a/firka_wear/lib/data/models/homework_cache_model.dart +++ b/firka_wear/lib/data/models/homework_cache_model.dart @@ -19,7 +19,7 @@ Future resetOldHomeworkCache(Isar isar) async { var date = getDate(week.cacheKey!); if (date.millisecondsSinceEpoch < - now.subtract(Duration(days: 30)).millisecondsSinceEpoch) { + now.subtract(const Duration(days: 30)).millisecondsSinceEpoch) { weeksToRemove.add(week.cacheKey!); } } diff --git a/firka_wear/lib/data/models/timetable_cache_model.dart b/firka_wear/lib/data/models/timetable_cache_model.dart index b6c879a..c5c7757 100644 --- a/firka_wear/lib/data/models/timetable_cache_model.dart +++ b/firka_wear/lib/data/models/timetable_cache_model.dart @@ -19,7 +19,7 @@ Future resetOldTimeTableCache(Isar isar) async { var date = getDate(week.cacheKey!); if (date.millisecondsSinceEpoch < - now.subtract(Duration(days: 30)).millisecondsSinceEpoch) { + now.subtract(const Duration(days: 30)).millisecondsSinceEpoch) { weeksToRemove.add(week.cacheKey!); } } diff --git a/firka_wear/lib/ui/components/firka_card.dart b/firka_wear/lib/ui/components/firka_card.dart index ae11e1e..d732b90 100644 --- a/firka_wear/lib/ui/components/firka_card.dart +++ b/firka_wear/lib/ui/components/firka_card.dart @@ -1,57 +1 @@ -import 'package:flutter/material.dart'; - -import 'package:firka_wear/ui/theme/style.dart'; - -class FirkaCard extends StatelessWidget { - final List left; - final List? right; - final Widget? extra; - - const FirkaCard({required this.left, this.right, this.extra, super.key}); - - @override - Widget build(BuildContext context) { - var right = this.right ?? []; - - if (extra != null) { - return SizedBox( - width: MediaQuery.of(context).size.width, - child: Card( - color: appStyle.colors.card, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row(children: left), - Row(children: right), - ], - ), - extra ?? SizedBox(), - ], - ), - ), - ), - ); - } else { - return SizedBox( - width: MediaQuery.of(context).size.width, - child: Card( - color: appStyle.colors.card, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row(children: left), - Row(children: right), - ], - ), - ), - ), - ); - } - } -} +export 'package:firka_common/ui/components/firka_card.dart'; diff --git a/firka_wear/lib/ui/components/firka_shadow.dart b/firka_wear/lib/ui/components/firka_shadow.dart index 9818a0e..29a96dc 100644 --- a/firka_wear/lib/ui/components/firka_shadow.dart +++ b/firka_wear/lib/ui/components/firka_shadow.dart @@ -1,44 +1 @@ -import 'package:flutter/material.dart'; - -import 'package:firka_wear/ui/theme/style.dart'; - -class FirkaShadow extends StatelessWidget { - final Widget child; - final bool shadow; - final double radius; - - const FirkaShadow({ - required this.shadow, - required this.child, - this.radius = 16.0, - super.key, - }); - - @override - Widget build(BuildContext context) { - final borderRadius = BorderRadius.circular(radius); - - final shadowBox = BoxDecoration( - color: Colors.transparent, - shape: BoxShape.rectangle, - boxShadow: [ - BoxShadow( - color: wearStyle.colors.shadowColor, - spreadRadius: -4, - blurRadius: 0, - offset: Offset(0, 2), - ), - ], - borderRadius: borderRadius, - ); - - if (!shadow) { - return ClipRRect(borderRadius: borderRadius, child: child); - } - - return Container( - decoration: shadowBox, - child: ClipRRect(borderRadius: borderRadius, child: child), - ); - } -} +export 'package:firka_common/ui/components/firka_shadow.dart'; diff --git a/firka_wear/lib/ui/components/grade.dart b/firka_wear/lib/ui/components/grade.dart index 45e8698..fdc5b4a 100644 --- a/firka_wear/lib/ui/components/grade.dart +++ b/firka_wear/lib/ui/components/grade.dart @@ -1,87 +1 @@ -import 'package:flutter/material.dart'; -import 'package:kreta_api/kreta_api.dart'; - -import 'package:firka_wear/ui/theme/style.dart'; -import 'grade_helpers.dart'; - -class GradeWidget extends StatelessWidget { - final Grade grade; - - const GradeWidget(this.grade, {super.key}); - - @override - Widget build(BuildContext context) { - Color gradeColor = appStyle.colors.grade1; - var gradeStr = grade.numericValue?.toString() ?? "0"; - double eccentricity = 0; - - if (grade.valueType.name == "Szazalekos") { - gradeStr = grade.strValue.replaceAll("%", ""); - if (grade.numericValue != null) { - gradeColor = getGradeColor( - percentageToGrade(grade.numericValue!).toDouble(), - ); - } - - if (grade.numericValue != null && grade.numericValue == 100) { - return Card( - shape: CircleBorder(eccentricity: eccentricity), - shadowColor: Colors.transparent, - color: gradeColor.withAlpha(38), - child: Padding( - padding: EdgeInsets.only(left: 8, right: 8), - child: Row( - children: [ - Text( - "100", // TODO: Make this curved - style: appStyle.fonts.P_14.copyWith(color: gradeColor), - ), - ], - ), - ), - ); - } else { - return Card( - shape: CircleBorder(eccentricity: eccentricity), - shadowColor: Colors.transparent, - color: gradeColor.withAlpha(38), - child: Padding( - padding: EdgeInsets.only(left: 8, right: 8), - child: Row( - children: [ - Text( - gradeStr, - style: appStyle.fonts.P_14.copyWith(color: gradeColor), - ), - Text( - "%", - style: appStyle.fonts.P_12.copyWith(color: gradeColor), - ), - ], - ), - ), - ); - } - } else { - if (grade.numericValue != null) { - gradeColor = getGradeColor(grade.numericValue!.toDouble()); - } - - return Card( - shape: CircleBorder(eccentricity: eccentricity), - shadowColor: Colors.transparent, - color: gradeColor.withAlpha(38), - child: Padding( - padding: EdgeInsets.only(left: 8, right: 8), - child: Text( - gradeStr, - style: appStyle.fonts.H_H1.copyWith( - fontSize: 24, - color: gradeColor, - ), - ), - ), - ); - } - } -} +export 'package:firka_common/ui/components/grade.dart'; diff --git a/firka_wear/lib/ui/components/grade_helpers.dart b/firka_wear/lib/ui/components/grade_helpers.dart index 4e00135..cfa04d2 100644 --- a/firka_wear/lib/ui/components/grade_helpers.dart +++ b/firka_wear/lib/ui/components/grade_helpers.dart @@ -1,75 +1 @@ -import 'dart:ui'; - -import 'package:flutter/material.dart'; -import 'package:kreta_api/kreta_api.dart'; - -import 'package:firka_wear/ui/theme/style.dart'; - -int roundGrade(double grade) { - if (grade < 2) { - return 1; - } - if (grade < 2.5) { - return 2; - } - if (grade < 3.5) { - return 3; - } - if (grade < 4.5) { - return 4; - } - - return 5; -} - -int percentageToGrade(int grade) { - if (grade < 50) { - return 1; - } - if (grade < 60) { - return 2; - } - if (grade < 70) { - return 3; - } - if (grade < 80) { - return 4; - } - - return 5; -} - -Color getGradeColor(double grade) { - switch (roundGrade(grade)) { - case 2: - return appStyle.colors.grade2; - case 3: - return appStyle.colors.grade3; - case 4: - return appStyle.colors.grade4; - case 5: - return appStyle.colors.grade5; - default: - return appStyle.colors.grade1; - } -} - -extension GradeListExtension on List { - double getAverageBySubject(Subject subject) { - var weightTotal = 0.00; - var sum = 0.00; - - for (var grade in this) { - if (grade.subject.uid == subject.uid) { - if (grade.numericValue != null) { - var weight = (grade.weightPercentage ?? 100) / 100.0; - weightTotal += weight; - - sum += grade.numericValue! * weight; - } - } - } - - return sum / weightTotal; - } -} +export 'package:firka_common/ui/components/grade_helpers.dart'; diff --git a/firka_wear/lib/ui/shared/class_icon.dart b/firka_wear/lib/ui/shared/class_icon.dart index bf7c04e..c650795 100644 --- a/firka_wear/lib/ui/shared/class_icon.dart +++ b/firka_wear/lib/ui/shared/class_icon.dart @@ -1,36 +1 @@ -import 'package:flutter/material.dart'; - -import 'package:firka_wear/core/icon_helper.dart'; - -import 'firka_icon.dart'; - -class ClassIconWidget extends StatelessWidget { - final String _uid; - final String _className; - final String _category; - final Color color; - final double? size; - - const ClassIconWidget({ - super.key, - required String uid, - required String className, - required String category, - this.color = Colors.white, - this.size, - }) : _className = className, - _uid = uid, - _category = category; - - @override - Widget build(BuildContext context) { - var iconCategory = getIconType(_uid, _className, _category); - - return FirkaIconWidget( - FirkaIconType.Majesticons, - getIconData(iconCategory), - color: color, - size: size, - ); - } -} +export 'package:firka_common/ui/shared/class_icon.dart'; diff --git a/firka_wear/lib/ui/shared/counter_digit.dart b/firka_wear/lib/ui/shared/counter_digit.dart index fd1d7e7..88857fc 100644 --- a/firka_wear/lib/ui/shared/counter_digit.dart +++ b/firka_wear/lib/ui/shared/counter_digit.dart @@ -1,22 +1 @@ -import 'package:flutter/material.dart'; - -import 'package:firka_wear/ui/theme/style.dart'; - -class CounterDigitWidget extends StatelessWidget { - final String c; - final TextStyle? style; - - const CounterDigitWidget(this.c, this.style, {super.key}); - - @override - Widget build(BuildContext context) { - return Card( - shadowColor: Colors.transparent, - color: appStyle.colors.buttonSecondaryFill, - child: Padding( - padding: EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 4), - child: Text(c, style: style), - ), - ); - } -} +export 'package:firka_common/ui/shared/counter_digit.dart'; diff --git a/firka_wear/lib/ui/shared/delayed_spinner.dart b/firka_wear/lib/ui/shared/delayed_spinner.dart index e359014..d1176ae 100644 --- a/firka_wear/lib/ui/shared/delayed_spinner.dart +++ b/firka_wear/lib/ui/shared/delayed_spinner.dart @@ -1,42 +1 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; - -class DelayedSpinnerWidget extends StatefulWidget { - const DelayedSpinnerWidget({super.key}); - - @override - State createState() => _DelayedSpinner(); -} - -class _DelayedSpinner extends State { - Timer? timer; - bool showSpinner = false; - - @override - void initState() { - super.initState(); - - timer = Timer(Duration(milliseconds: 50), () { - setState(() { - showSpinner = true; - }); - }); - } - - @override - Widget build(BuildContext context) { - if (showSpinner) { - return CircularProgressIndicator(); - } else { - return SizedBox(); - } - } - - @override - void dispose() { - super.dispose(); - - timer?.cancel(); - } -} +export 'package:firka_common/ui/shared/delayed_spinner.dart'; diff --git a/firka_wear/lib/ui/shared/firka_icon.dart b/firka_wear/lib/ui/shared/firka_icon.dart index 2ba4418..347aeb2 100644 --- a/firka_wear/lib/ui/shared/firka_icon.dart +++ b/firka_wear/lib/ui/shared/firka_icon.dart @@ -1,39 +1 @@ -// Enum values match external asset/API naming. -// ignore_for_file: constant_identifier_names - -import 'dart:typed_data'; - -import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; -import 'package:majesticons_flutter/majesticons_flutter.dart'; - -enum FirkaIconType { Majesticons, MajesticonsLocal } - -class FirkaIconWidget extends StatelessWidget { - final FirkaIconType iconType; - final Object iconData; - final Color color; - final double? size; - - const FirkaIconWidget( - this.iconType, - this.iconData, { - super.key, - this.color = Colors.white, - this.size, - }); - - @override - Widget build(BuildContext context) { - switch (iconType) { - case FirkaIconType.Majesticons: - return Majesticon(iconData as Uint8List, color: color, size: size); - case FirkaIconType.MajesticonsLocal: - return SvgPicture.asset( - 'assets/majesticons/${iconData as String}.svg', - color: color, - height: size, - ); - } - } -} +export 'package:firka_common/ui/shared/firka_icon.dart'; diff --git a/firka_wear/lib/ui/shared/grade_small_card.dart b/firka_wear/lib/ui/shared/grade_small_card.dart index 6c7acd4..e01f9cd 100644 --- a/firka_wear/lib/ui/shared/grade_small_card.dart +++ b/firka_wear/lib/ui/shared/grade_small_card.dart @@ -1,60 +1 @@ -import 'package:flutter/material.dart'; -import 'package:kreta_api/kreta_api.dart'; - -import 'package:firka_wear/ui/components/firka_card.dart'; -import 'package:firka_wear/ui/components/grade_helpers.dart'; -import 'package:firka_wear/ui/shared/class_icon.dart'; -import 'package:firka_wear/ui/theme/style.dart'; - -class GradeSmallCard extends FirkaCard { - final List grades; - final Subject subject; - - GradeSmallCard(this.grades, this.subject, {super.key}) - : super( - left: [ - ClassIconWidget( - uid: subject.uid, - className: subject.name, - category: subject.category.name!, - color: appStyle.colors.accent, - ), - SizedBox(width: 4), - SizedBox( - width: 200, - child: Text( - subject.name, - style: appStyle.fonts.B_16SB.apply( - color: appStyle.colors.textPrimary, - ), - ), - ), - ], - right: [ - grades.getAverageBySubject(subject).isNaN - ? SizedBox() - : Card( - shadowColor: Colors.transparent, - color: getGradeColor( - grades.getAverageBySubject(subject), - ).withAlpha(38), - child: Padding( - padding: EdgeInsets.only( - left: 8, - right: 8, - top: 4, - bottom: 4, - ), - child: Text( - grades.getAverageBySubject(subject).toStringAsFixed(2), - style: appStyle.fonts.B_16SB.apply( - color: getGradeColor( - grades.getAverageBySubject(subject), - ), - ), - ), - ), - ), - ], - ); -} +export 'package:firka_common/ui/shared/grade_small_card.dart'; diff --git a/firka_wear/lib/ui/theme/style.dart b/firka_wear/lib/ui/theme/style.dart index 63e7b3d..8ce3436 100644 --- a/firka_wear/lib/ui/theme/style.dart +++ b/firka_wear/lib/ui/theme/style.dart @@ -1,271 +1 @@ -// Design token names (e.g. H_H1, B_16R) follow the design system. -// ignore_for_file: non_constant_identifier_names - -import 'package:flutter/material.dart'; - -class FirkaFonts { - TextStyle H_H1; - TextStyle H_18px; - TextStyle H_H2; - TextStyle H_16px; - TextStyle H_14px; - TextStyle H_12px; - - TextStyle H_16px_trimmed; // TODO: somehow implement this - // the design has this trimmed to 130% line height - - TextStyle B_16R; - TextStyle B_16SB; - - TextStyle B_14R; - TextStyle B_14SB; - - TextStyle B_12R; - TextStyle B_12SB; - - TextStyle P_14; - TextStyle P_12; - - FirkaFonts({ - required this.H_H1, - required this.H_18px, - required this.H_H2, - required this.H_16px, - required this.H_14px, - required this.H_12px, - required this.H_16px_trimmed, - required this.B_16R, - required this.B_16SB, - required this.B_14R, - required this.B_14SB, - required this.B_12R, - required this.B_12SB, - required this.P_14, - required this.P_12, - }); -} - -class FirkaColors { - Color background; - Color backgroundAmoled; - Color background0p; - Color success; - int shadowBlur; - - Color textPrimary; - Color textSecondary; - Color textTertiary; - - Color card; - Color cardTranslucent; - - Color buttonSecondaryFill; - - Color accent; - Color secondary; - Color shadowColor; - Color a15p; // 15% - - Color warningAccent; - Color warningText; - Color warning15p; - Color warningCard; - - Color errorAccent; - Color errorText; - Color error15p; - Color errorCard; - - Color grade5; - Color grade4; - Color grade3; - Color grade2; - Color grade1; - - FirkaColors({ - required this.background, - required this.backgroundAmoled, - required this.background0p, - required this.success, - required this.shadowBlur, - required this.textPrimary, - required this.textSecondary, - required this.textTertiary, - required this.card, - required this.cardTranslucent, - required this.buttonSecondaryFill, - required this.accent, - required this.secondary, - required this.shadowColor, - required this.a15p, - required this.warningAccent, - required this.warningText, - required this.warning15p, - required this.warningCard, - required this.errorAccent, - required this.errorText, - required this.error15p, - required this.errorCard, - required this.grade5, - required this.grade4, - required this.grade3, - required this.grade2, - required this.grade1, - }); -} - -class FirkaStyle { - FirkaColors colors; - FirkaFonts fonts; - - FirkaStyle({required this.colors, required this.fonts}); -} - -final _defaultFonts = FirkaFonts( - H_H1: TextStyle( - fontSize: 30, - fontFamily: 'Montserrat', - fontVariations: [FontVariation("wght", 700)], - ), - H_18px: TextStyle( - fontSize: 18, - fontFamily: 'Montserrat', - fontVariations: [FontVariation("wght", 700)], - ), - H_H2: TextStyle( - fontSize: 20, - fontFamily: 'Montserrat', - fontVariations: [FontVariation("wght", 700)], - ), - H_16px: TextStyle( - fontSize: 16, - fontFamily: 'Montserrat', - fontVariations: [FontVariation("wght", 600)], - ), - H_14px: TextStyle( - fontSize: 14, - fontFamily: 'Montserrat', - fontVariations: [FontVariation("wght", 600)], - ), - H_12px: TextStyle( - fontSize: 12, - fontFamily: 'Montserrat', - fontVariations: [FontVariation("wght", 600)], - ), - H_16px_trimmed: TextStyle( - fontSize: 16, - fontFamily: 'Montserrat', - fontVariations: [FontVariation("wght", 600)], - ), - B_16R: TextStyle( - fontSize: 16, - fontFamily: 'Figtree', - fontVariations: [FontVariation("wght", 600)], - ), - B_16SB: TextStyle( - fontSize: 16, - fontFamily: 'Figtree', - fontVariations: [FontVariation("wght", 700)], - ), - B_14R: TextStyle( - fontSize: 14, - fontFamily: 'Figtree', - fontVariations: [FontVariation("wght", 600)], - ), - B_14SB: TextStyle( - fontSize: 14, - fontFamily: 'Figtree', - fontVariations: [FontVariation("wght", 700)], - ), - B_12R: TextStyle( - fontSize: 12, - fontFamily: 'Figtree', - fontVariations: [FontVariation("wght", 600)], - ), - B_12SB: TextStyle( - fontSize: 12, - fontFamily: 'Figtree', - fontVariations: [FontVariation("wght", 700)], - ), - P_14: TextStyle( - fontSize: 14, - fontFamily: 'RobotoMono', - fontVariations: [FontVariation("wght", 700)], - ), - P_12: TextStyle( - fontSize: 12, - fontFamily: 'RobotoMono', - fontVariations: [FontVariation("wght", 700)], - ), -); - -final FirkaStyle lightStyle = FirkaStyle( - colors: FirkaColors( - background: Color(0xFFFAFFF0), - backgroundAmoled: Colors.black, - background0p: Color(0x00fafff0), - success: Color(0xFF92EA3B), - shadowBlur: 2, - textPrimary: Color(0xFF394C0A), - textSecondary: Color(0xCC394C0A), - textTertiary: Color(0x80394C0A), - card: Color(0xFFF3FBDE), - cardTranslucent: Color(0x80F3FBDE), - buttonSecondaryFill: Color(0xFFFEFFFD), - accent: Color(0xFFA7DC22), - secondary: Color(0xFF6E8F1B), - shadowColor: Color(0x33647e22), - a15p: Color(0x26a7dc22), - warningAccent: Color(0xFFFFA046), - warningText: Color(0xFF8F531B), - warning15p: Color(0x26FFA046), - warningCard: Color(0xFFFAEBDC), - errorAccent: Color(0xFFFF54A1), - errorText: Color(0xFF8F1B4F), - error15p: Color(0x26FF54A1), - errorCard: Color(0xFFFADCE9), - grade5: Color(0xFF22CCAD), - grade4: Color(0xFF92EA3B), - grade3: Color(0xFFF9CF00), - grade2: Color(0xFFFFA046), - grade1: Color(0xFFFF54A1), - ), - fonts: _defaultFonts, -); - -final FirkaStyle darkStyle = FirkaStyle( - colors: FirkaColors( - background: Color(0xFF0D1202), - backgroundAmoled: Colors.black, - background0p: Color(0x00fafff0), - success: Color(0xFF92EA3B), - shadowBlur: 0, - textPrimary: Color(0xFFEAF7CC), - textSecondary: Color(0xB3EAF7CC), - textTertiary: Color(0x80EAF7CC), - card: Color(0xFF141905), - cardTranslucent: Color(0x80141905), - buttonSecondaryFill: Color(0xFF20290B), - accent: Color(0xFFA7DC22), - secondary: Color(0xFFCBEE71), - shadowColor: Color(0x26CBEE71), - a15p: Color(0x26A7DC22), - warningAccent: Color(0xFFFFA046), - warningText: Color(0xFFF0B37A), - warning15p: Color(0x26FFA046), - warningCard: Color(0xFF201203), - errorAccent: Color(0xFFFF54A1), - errorText: Color(0xFFF59EC5), - error15p: Color(0x26FF54A1), - errorCard: Color(0xFF1E030F), - grade5: Color(0xFF22CCAD), - grade4: Color(0xFF92EA3B), - grade3: Color(0xFFF9CF00), - grade2: Color(0xFFFFA046), - grade1: Color(0xFFFF54A1), - ), - fonts: _defaultFonts, -); - -FirkaStyle appStyle = lightStyle; -FirkaStyle wearStyle = darkStyle; +export 'package:firka_common/ui/theme/style.dart'; diff --git a/firka_wear/pubspec.yaml b/firka_wear/pubspec.yaml index 19c3a58..08f6dd6 100644 --- a/firka_wear/pubspec.yaml +++ b/firka_wear/pubspec.yaml @@ -33,6 +33,8 @@ environment: dependencies: flutter: sdk: flutter + firka_common: + path: ../firka_common kreta_api: path: ../kreta_api From ba075c3b14b93fe9eb9c16987780960138ef1872 Mon Sep 17 00:00:00 2001 From: Armand <4831c0@proton.me> Date: Tue, 3 Mar 2026 17:23:03 +0100 Subject: [PATCH 2/6] firka: add a section for ghost grades --- firka/lib/l10n | 2 +- firka/lib/ui/components/common_bottom_sheets.dart | 9 --------- .../lib/ui/phone/pages/home/home_grades_subject.dart | 12 +++++++++++- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/firka/lib/l10n b/firka/lib/l10n index 2ac00c3..138d4d9 160000 --- a/firka/lib/l10n +++ b/firka/lib/l10n @@ -1 +1 @@ -Subproject commit 2ac00c3bea85db999c961eabf02dd8515f08e75f +Subproject commit 138d4d97641743a2e0203643fefe49244d6412f1 diff --git a/firka/lib/ui/components/common_bottom_sheets.dart b/firka/lib/ui/components/common_bottom_sheets.dart index ac9e276..1f7bd55 100644 --- a/firka/lib/ui/components/common_bottom_sheets.dart +++ b/firka/lib/ui/components/common_bottom_sheets.dart @@ -1245,15 +1245,6 @@ class _GradeCalculatorSheetContentState ), ), ), - if (entries.isNotEmpty) ...[ - SizedBox(height: 16), - Text( - '${widget.data.l10n.subject_avg}: ${_weightedAverage.toStringAsFixed(2)}', - style: appStyle.fonts.B_14R.apply( - color: appStyle.colors.textPrimary, - ), - ), - ], ], ); } diff --git a/firka/lib/ui/phone/pages/home/home_grades_subject.dart b/firka/lib/ui/phone/pages/home/home_grades_subject.dart index 807d10a..087bc60 100644 --- a/firka/lib/ui/phone/pages/home/home_grades_subject.dart +++ b/firka/lib/ui/phone/pages/home/home_grades_subject.dart @@ -131,7 +131,17 @@ class _HomeGradesSubjectScreen extends FirkaState { }).toList(); var gradeWidgets = List.empty(growable: true); - gradeWidgets.addAll(ghostGradeWidgets); + if (ghostGradeWidgets.isNotEmpty) { + gradeWidgets.add( + Text( + widget.data.l10n.ghost_grades, + style: appStyle.fonts.B_16R.apply( + color: appStyle.colors.textPrimary, + ), + ), + ); + gradeWidgets.addAll(ghostGradeWidgets); + } for (var group in groups.entries) { gradeWidgets.add(SizedBox(height: 8)); From e031c18ecb22e2fb682bcfe984232bb0de961feb Mon Sep 17 00:00:00 2001 From: Armand <4831c0@proton.me> Date: Tue, 3 Mar 2026 17:37:39 +0100 Subject: [PATCH 3/6] firka: fix Live Activity registration with fallback on resume and delayed retry --- .../ui/phone/screens/home/home_screen.dart | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/firka/lib/ui/phone/screens/home/home_screen.dart b/firka/lib/ui/phone/screens/home/home_screen.dart index 2a69742..ea3248e 100644 --- a/firka/lib/ui/phone/screens/home/home_screen.dart +++ b/firka/lib/ui/phone/screens/home/home_screen.dart @@ -410,6 +410,11 @@ class _HomeScreenState extends FirkaState await LiveActivityService.showConsentScreenIfNeeded(); }); } + if (Platform.isIOS) { + Future.delayed(const Duration(seconds: 4), () { + if (!_disposed) _runLiveActivityLoginIfNeeded(); + }); + } } Future _preloadImages() async { @@ -535,10 +540,35 @@ class _HomeScreenState extends FirkaState if (Platform.isIOS) { _refreshLiveActivityOnResume(); + _runLiveActivityLoginIfNeeded(); } } } + /// Fallback: if Live Activity login never ran (e.g. prefetch bailed on lifecycle + /// or fetchData didn't complete), run it once when app is resumed. + void _runLiveActivityLoginIfNeeded() { + if (_didRunLiveActivityLogin || _disposed) return; + Future.delayed(const Duration(milliseconds: 500), () async { + if (_disposed || _didRunLiveActivityLogin) return; + _didRunLiveActivityLogin = true; + final token = pickActiveToken( + tokens: initData.tokens, + settings: initData.settings, + preferredStudentIdNorm: initData.client.model.studentIdNorm, + ); + final studentName = token?.studentId ?? 'Student'; + LiveActivityService.onUserLogin( + client: initData.client, + studentName: studentName, + settingsStore: initData.settings, + ).catchError((e, st) { + _didRunLiveActivityLogin = false; + logger.severe('LiveActivity registration failed: $e', e, st); + }); + }); + } + void _refreshLiveActivityOnResume() async { if (!_hasCompletedFirstPrefetch) return; try { From a8983074ddd365ea57f1e6dda8ebcf107197d04a Mon Sep 17 00:00:00 2001 From: Armand <4831c0@proton.me> Date: Tue, 3 Mar 2026 18:09:35 +0100 Subject: [PATCH 4/6] firka: show weighted avg when adding ghost grades --- .../lib/ui/components/common_bottom_sheets.dart | 12 ------------ .../ui/phone/pages/home/home_grades_subject.dart | 7 +++++++ .../lib/ui/phone/widgets/grade_summary_bar.dart | 16 ++++++++++++++-- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/firka/lib/ui/components/common_bottom_sheets.dart b/firka/lib/ui/components/common_bottom_sheets.dart index 1f7bd55..dbf1dc2 100644 --- a/firka/lib/ui/components/common_bottom_sheets.dart +++ b/firka/lib/ui/components/common_bottom_sheets.dart @@ -1051,18 +1051,6 @@ class _GradeCalculatorSheetContentState int weightPercent = 100; final List<(int grade, int weight)> entries = []; - double get _weightedAverage { - if (entries.isEmpty) return 0; - double sum = 0; - double weightTotal = 0; - for (final e in entries) { - final w = e.$2 / 100.0; - weightTotal += w; - sum += e.$1 * w; - } - return weightTotal > 0 ? sum / weightTotal : 0; - } - @override Widget build(BuildContext context) { return Column( diff --git a/firka/lib/ui/phone/pages/home/home_grades_subject.dart b/firka/lib/ui/phone/pages/home/home_grades_subject.dart index 087bc60..0c069d7 100644 --- a/firka/lib/ui/phone/pages/home/home_grades_subject.dart +++ b/firka/lib/ui/phone/pages/home/home_grades_subject.dart @@ -1,3 +1,4 @@ +import 'package:firka/ui/phone/widgets/grade_summary_bar.dart'; import 'package:kreta_api/kreta_api.dart'; import 'package:firka/core/extensions.dart'; import 'package:firka/ui/components/common_bottom_sheets.dart'; @@ -312,6 +313,12 @@ class _HomeGradesSubjectScreen extends FirkaState { GradeChartWithInteraction( grades: _gradesWithGhosts(aGrade.subject), ), + SizedBox(height: 2), + GradeSummaryBar( + grades: _gradesWithGhosts(aGrade.subject), + l10n: widget.data.l10n, + showAverage: ghostGradeWidgets.isNotEmpty, + ), SizedBox(height: 12), Padding( padding: EdgeInsets.only(left: 4), diff --git a/firka/lib/ui/phone/widgets/grade_summary_bar.dart b/firka/lib/ui/phone/widgets/grade_summary_bar.dart index f55fd67..fb8bb76 100644 --- a/firka/lib/ui/phone/widgets/grade_summary_bar.dart +++ b/firka/lib/ui/phone/widgets/grade_summary_bar.dart @@ -1,3 +1,4 @@ +import 'package:firka/core/average_helper.dart'; import 'package:kreta_api/kreta_api.dart'; import 'package:firka/ui/components/grade.dart'; import 'package:firka/ui/components/grade_helpers.dart'; @@ -11,8 +12,14 @@ import 'package:firka/ui/theme/style.dart'; class GradeSummaryBar extends StatefulWidget { final List grades; final AppLocalizations l10n; + final bool showAverage; - const GradeSummaryBar({super.key, required this.grades, required this.l10n}); + const GradeSummaryBar({ + super.key, + required this.grades, + required this.l10n, + this.showAverage = false, + }); @override State createState() => _GradeSummaryBarState(); @@ -32,6 +39,9 @@ class _GradeSummaryBarState extends State { appStyle.colors.grade5, ]; final totalCounted = countsByGrade.reduce((a, b) => a + b); + final averageText = widget.showAverage + ? calculateAverage(widget.grades).toStringAsFixed(2) + : ''; return Card( shadowColor: Colors.transparent, @@ -49,7 +59,9 @@ class _GradeSummaryBarState extends State { Row( children: [ Text( - widget.l10n.gradesCount(total), + widget.showAverage + ? '${widget.l10n.gradesCount(total)} ($averageText)' + : widget.l10n.gradesCount(total), style: appStyle.fonts.B_16SB.apply( color: appStyle.colors.textPrimary, ), From 863f9c8077c50a8616f47abb17c9b67e0c48ae78 Mon Sep 17 00:00:00 2001 From: Armand <4831c0@proton.me> Date: Tue, 3 Mar 2026 18:15:52 +0100 Subject: [PATCH 5/6] firka_wear: add codegen script --- .../drawable-hdpi/ic_launcher_background.png | Bin 4550 -> 5310 bytes .../drawable-hdpi/ic_launcher_foreground.png | Bin 2909 -> 2963 bytes .../drawable-hdpi/ic_launcher_monochrome.png | Bin 7519 -> 7691 bytes .../main/res/mipmap-mdpi/launcher_icon.png | Bin 1968 -> 2087 bytes .../main/res/mipmap-xxhdpi/launcher_icon.png | Bin 6803 -> 7692 bytes firka_wear/flutter_launcher_icons.yaml | 2 - firka_wear/lib/l10n | 2 +- firka_wear/scripts/codegen.dart | 137 ++++++++++++++++++ 8 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 firka_wear/scripts/codegen.dart diff --git a/firka_wear/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png b/firka_wear/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png index a4b37589b9e4ad1e752d48ea1ffaf0d52b1dab9b..791200f2308bfa6b75bf7d3c8210c52f4c238986 100644 GIT binary patch literal 5310 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 literal 4550 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 O7(8A5T-G@yGywoy0T9Um diff --git a/firka_wear/android/app/src/main/res/drawable-hdpi/ic_launcher_monochrome.png b/firka_wear/android/app/src/main/res/drawable-hdpi/ic_launcher_monochrome.png index 31e7cce7304622743f94899d8df66c9a1b5af6aa..b0dc1a54629eef0ae1489860afa47761385e6db3 100644 GIT binary patch literal 7691 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 literal 7519 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 Gp$PyAPW%f1 delta 1954 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 YUPLR;%#z>Az`(%Z>FVdQ&MBb@0AwZ}z5oCK literal 6803 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 inputs, File output) { + final outTime = _modified(output); + if (outTime == null) return true; + for (final f in inputs) { + final t = _modified(f); + if (t != null && t.isAfter(outTime)) return true; + } + return false; +} + +bool _iconsOutOfDate(String root) { + final config = File(p.join(root, 'flutter_launcher_icons.yaml')); + final pubspec = File(p.join(root, 'pubspec.yaml')); + final imagePath = File(p.join(root, 'assets/images/logos/colored_logo.png')); + final monochrome = File( + p.join(root, 'assets/images/logos/monochrome_logo.png'), + ); + final background = File( + p.join(root, 'assets/images/logos/colored_logo_without_mustache.png'), + ); + final foreground = File( + p.join(root, 'assets/images/logos/colored_logo_only_mustache.png'), + ); + + final inputs = [ + config, + pubspec, + imagePath, + monochrome, + background, + foreground, + ].where((f) => f.existsSync()).map((f) => File(p.canonicalize(f.path))); + final output = File( + p.join( + root, + 'android/app/src/main/res/mipmap-anydpi-v26/launcher_icon.xml', + ), + ); + return _anyNewerThan(inputs, output); +} + +bool _l10nOutOfDate(String root) { + final l10nDir = p.join(root, 'lib/l10n'); + final l10nYml = File(p.join(root, 'l10n.yml')); + final arbs = Directory(l10nDir) + .listSync() + .whereType() + .where((f) => f.path.endsWith('.arb')) + .map((f) => File(p.canonicalize(f.path))) + .toList(); + final inputs = [l10nYml, ...arbs].where((f) => f.existsSync()).cast(); + final output = File(p.join(root, 'lib/l10n/app_localizations.dart')); + return _anyNewerThan(inputs, output); +} + +bool _isarOutOfDate(String root) { + final modelsDir = p.join(root, 'lib/data/models'); + if (!Directory(modelsDir).existsSync()) return false; + + for (final entity in Directory(modelsDir).listSync()) { + if (entity is! File || !entity.path.endsWith('.dart')) continue; + final content = entity.readAsStringSync(); + if (!content.contains("part '") || !content.contains('.g.dart')) continue; + + final baseName = p.basenameWithoutExtension(entity.path); + final gPath = p.join(modelsDir, '$baseName.g.dart'); + final dartFile = File(p.canonicalize(entity.path)); + final gFile = File(gPath); + if (_anyNewerThan([dartFile], gFile)) return true; + } + return false; +} + +Future _run( + String executable, + List args, + String workingDirectory, +) async { + final result = await Process.run( + executable, + args, + workingDirectory: workingDirectory, + runInShell: true, + ); + if (result.exitCode != 0) { + stderr.write(result.stderr); + exit(result.exitCode); + } + return true; +} From 484d8cf4cb4c3fcb6ad2c27960c2cc579e4bb54f Mon Sep 17 00:00:00 2001 From: Armand <4831c0@proton.me> Date: Tue, 3 Mar 2026 18:37:00 +0100 Subject: [PATCH 6/6] codegen: add lock files --- firka/codegen-lock.yaml | 21 ++++ firka/pubspec.yaml | 1 + firka/scripts/codegen.dart | 169 +++++++++++++++++++++++++++----- firka_wear/codegen-lock.yaml | 18 ++++ firka_wear/pubspec.yaml | 2 + firka_wear/scripts/codegen.dart | 156 ++++++++++++++++++++++++----- 6 files changed, 318 insertions(+), 49 deletions(-) create mode 100644 firka/codegen-lock.yaml create mode 100644 firka_wear/codegen-lock.yaml diff --git a/firka/codegen-lock.yaml b/firka/codegen-lock.yaml new file mode 100644 index 0000000..bba4228 --- /dev/null +++ b/firka/codegen-lock.yaml @@ -0,0 +1,21 @@ +icons: + "flutter_launcher_icons.yaml": "c600507ca0df7cebd0f708124842512a14ed3d597b779176200d6ba25b1335b1" + "pubspec.yaml": "57b4c0a018bc425e4b28d8942dca6071371d1801d81f36951c8c37a894a38987" + "assets/images/logos/colored_logo.webp": "4b4fa99d144fe6694aa4487ba1b26aeecafae41e3c877836cd7da28d61a77983" + "assets/images/logos/monochrome_logo.png": "188d2b0a64c70323b09bcee721663d6698fb557066f20ddaec97bba6869c1c6c" + "assets/images/logos/colored_logo_without_mustache.png": "d11cff9f38985885873bfdd2d84e61f8fab03803eada94d4caac1545ef3685f3" + "assets/images/logos/colored_logo_only_mustache.png": "bad6220c11bdfb1dfe04e5173bd2ebedd3999689d4b3a68fc63dc520c96dd33b" +l10n: + "l10n.yml": "a57bc304cac4a2b0235593586f17f400a5165d67fc9aadeaa11893cfa36ee082" + "lib/l10n/app_de.arb": "55f030b312cc07ff05cdc3d6ee10ef9bdec3243b507225e9a47196444518d955" + "lib/l10n/app_en.arb": "cbad6dd2485a983e399cce97371c19089b9110d30536488c14a7ea709c7b6ead" + "lib/l10n/app_hu.arb": "17077ec76b68ed03796a264b99e4dba9e6ddd532e27a92d8fb237ea6f211f757" +isar: + "lib/data/models/app_settings_model.dart": "5eb5af345f1347f104257f0999763650fe2673f9da1754bd12d3f756fe5c9723" + "lib/data/models/generic_cache_model.dart": "79151d0467fb5d40c532eaaa08ad7c7e24a34304199280fbf49cf6e5adcce6bc" + "lib/data/models/homework_cache_model.dart": "45789970b27d5790cdc54c292ef2f5feaa5f4e293b8a8862fd676d5eb3e25d29" + "lib/data/models/timetable_cache_model.dart": "b972bf51e399f8d20d4f9ad660082d4cc4a9798df9ac9d6ec9ef6ac640205572" + "lib/data/models/token_model.dart": "8c957cd07e473827d78fd8fd4fb6c1336b636a69c25c93618e1e7f94b7cf0683" +splash: + "flutter_native_splash.yaml": "0fd4a85d6f950d97298e99916927649940ffcfdadfc136ceee126fed0dbaa9f2" + "assets/images/logos/splash.png": "88fbebc3d686cb9095bcce362029b69978b1b14270e465e91d962b1425db1152" diff --git a/firka/pubspec.yaml b/firka/pubspec.yaml index 6cf647b..0ae6d96 100644 --- a/firka/pubspec.yaml +++ b/firka/pubspec.yaml @@ -62,6 +62,7 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^6.0.0 + yaml: ^3.1.2 isar_community_generator: 3.3.0 android_notification_icons: ^0.0.1 integration_test: diff --git a/firka/scripts/codegen.dart b/firka/scripts/codegen.dart index 032c5ad..aa6bfea 100644 --- a/firka/scripts/codegen.dart +++ b/firka/scripts/codegen.dart @@ -1,42 +1,55 @@ import 'dart:io'; +import 'package:crypto/crypto.dart'; import 'package:image/image.dart' as img; import 'package:path/path.dart' as p; +import 'package:yaml/yaml.dart' as yaml; + +const _lockFileName = 'codegen-lock.yaml'; void main() async { final root = _projectRoot(); var ran = false; if (_iconsOutOfDate(root)) { + final inputs = _iconsInputs(root); stdout.writeln('Icons out of date, running flutter_launcher_icons...'); await _run('dart', ['run', 'flutter_launcher_icons'], root); + _updateLockWithHashes(root, 'icons', _computeHashes(root, inputs)); ran = true; } if (_l10nOutOfDate(root)) { + final inputs = _l10nInputs(root); stdout.writeln('l10n out of date, running flutter gen-l10n...'); await _run('flutter', [ 'gen-l10n', '--template-arb-file', 'app_hu.arb', ], root); + _updateLockWithHashes(root, 'l10n', _computeHashes(root, inputs)); ran = true; } if (_isarOutOfDate(root)) { + final inputs = _isarInputs(root); + final hashes = _computeHashes(root, inputs); stdout.writeln( 'Isar generated dart files out of date, running build_runner...', ); await _run('dart', ['run', 'build_runner', 'build'], root); + _updateLockWithHashes(root, 'isar', hashes); ran = true; } if (_splashOutOfDate(root)) { + final inputs = _splashInputs(root); await _generateAndroid12SplashImage(root); stdout.writeln( 'Splash out of date, running flutter_native_splash:create...', ); await _run('dart', ['run', 'flutter_native_splash:create'], root); + _updateLockWithHashes(root, 'splash', _computeHashes(root, inputs)); ran = true; } @@ -47,7 +60,92 @@ void main() async { String _projectRoot() { final script = p.canonicalize(Platform.script.toFilePath()); - return p.dirname(p.dirname(script)); + return p.canonicalize(p.dirname(p.dirname(script))); +} + +String _lockPath(String root) => p.join(root, _lockFileName); + +Map>? _readLock(String root) { + final file = File(_lockPath(root)); + if (!file.existsSync()) return null; + try { + final content = file.readAsStringSync(); + final decoded = yaml.loadYaml(content); + if (decoded is! Map) return null; + final result = >{}; + for (final entry in decoded.entries) { + if (entry.value is! Map) continue; + final inner = entry.value as Map; + result[entry.key.toString()] = inner.map( + (k, v) => MapEntry( + Platform.isWindows ? k.toString().toLowerCase() : k.toString(), + v?.toString() ?? '', + ), + ); + } + return result; + } catch (_) { + return null; + } +} + +void _writeLock(String root, Map> lock) { + final buf = StringBuffer(); + for (final stepEntry in lock.entries) { + buf.writeln('${stepEntry.key}:'); + for (final fileEntry in stepEntry.value.entries) { + buf.writeln( + ' "${_escapeYaml(fileEntry.key)}": "${_escapeYaml(fileEntry.value)}"', + ); + } + } + File(_lockPath(root)).writeAsStringSync(buf.toString()); +} + +String _escapeYaml(String s) => + s.replaceAll('\\', '\\\\').replaceAll('"', '\\"'); + +String _fileHash(File file) { + final bytes = file.readAsBytesSync(); + final digest = sha256.convert(bytes); + return digest.toString(); +} + +String _relativePath(String root, File file) { + final rel = p.relative(file.path, from: root); + final normalized = rel.replaceAll('\\', '/'); + return Platform.isWindows ? normalized.toLowerCase() : normalized; +} + +bool _hashesMatch( + String root, + String stepName, + List inputs, + Map>? lock, +) { + if (lock == null || !lock.containsKey(stepName)) return false; + final stepHashes = lock[stepName]!; + for (final f in inputs) { + final rel = _relativePath(root, f); + final stored = stepHashes[rel]; + if (stored == null || stored != _fileHash(f)) return false; + } + if (stepHashes.length != inputs.length) return false; + return true; +} + +Map _computeHashes(String root, List inputs) { + return {for (final f in inputs) _relativePath(root, f): _fileHash(f)}; +} + +void _updateLockWithHashes( + String root, + String stepName, + Map hashes, +) { + final lock = _readLock(root) ?? >{}; + lock[stepName] = Map.from(hashes); + _writeLock(root, lock); } DateTime? _modified(File file) { @@ -65,7 +163,7 @@ bool _anyNewerThan(Iterable inputs, File output) { return false; } -bool _iconsOutOfDate(String root) { +List _iconsInputs(String root) { final config = File(p.join(root, 'flutter_launcher_icons.yaml')); final pubspec = File(p.join(root, 'pubspec.yaml')); final imagePath = File(p.join(root, 'assets/images/logos/colored_logo.webp')); @@ -78,25 +176,25 @@ bool _iconsOutOfDate(String root) { final foreground = File( p.join(root, 'assets/images/logos/colored_logo_only_mustache.png'), ); + return [config, pubspec, imagePath, monochrome, background, foreground] + .where((f) => f.existsSync()) + .map((f) => File(p.canonicalize(f.path))) + .toList(); +} - final inputs = [ - config, - pubspec, - imagePath, - monochrome, - background, - foreground, - ].where((f) => f.existsSync()).map((f) => File(p.canonicalize(f.path))); +bool _iconsOutOfDate(String root) { + final inputs = _iconsInputs(root); final output = File( p.join( root, 'android/app/src/main/res/mipmap-anydpi-v26/launcher_icon.xml', ), ); - return _anyNewerThan(inputs, output); + if (!_anyNewerThan(inputs, output)) return false; + return !_hashesMatch(root, 'icons', inputs, _readLock(root)); } -bool _l10nOutOfDate(String root) { +List _l10nInputs(String root) { final l10nDir = p.join(root, 'lib/l10n'); final l10nYml = File(p.join(root, 'l10n.yml')); final arbs = Directory(l10nDir) @@ -105,41 +203,60 @@ bool _l10nOutOfDate(String root) { .where((f) => f.path.endsWith('.arb')) .map((f) => File(p.canonicalize(f.path))) .toList(); - final inputs = [l10nYml, ...arbs].where((f) => f.existsSync()).cast(); - final output = File(p.join(root, 'lib/l10n/app_localizations.dart')); - return _anyNewerThan(inputs, output); + return [l10nYml, ...arbs].where((f) => f.existsSync()).cast().toList(); } -bool _isarOutOfDate(String root) { - final modelsDir = p.join(root, 'lib/data/models'); - if (!Directory(modelsDir).existsSync()) return false; +bool _l10nOutOfDate(String root) { + final inputs = _l10nInputs(root); + final output = File(p.join(root, 'lib/l10n/app_localizations.dart')); + if (!_anyNewerThan(inputs, output)) return false; + return !_hashesMatch(root, 'l10n', inputs, _readLock(root)); +} +List _isarInputs(String root) { + final modelsDir = p.join(root, 'lib/data/models'); + if (!Directory(modelsDir).existsSync()) return []; + final list = []; for (final entity in Directory(modelsDir).listSync()) { if (entity is! File || !entity.path.endsWith('.dart')) continue; final content = entity.readAsStringSync(); if (!content.contains("part '") || !content.contains('.g.dart')) continue; + list.add(File(p.canonicalize(entity.path))); + } + return list; +} - final baseName = p.basenameWithoutExtension(entity.path); - final gPath = p.join(modelsDir, '$baseName.g.dart'); - final dartFile = File(p.canonicalize(entity.path)); - final gFile = File(gPath); - if (_anyNewerThan([dartFile], gFile)) return true; +bool _isarOutOfDate(String root) { + final inputs = _isarInputs(root); + if (inputs.isEmpty) return false; + final modelsDir = p.join(root, 'lib/data/models'); + for (final dartFile in inputs) { + final baseName = p.basenameWithoutExtension(dartFile.path); + final gFile = File(p.join(modelsDir, '$baseName.g.dart')); + if (_anyNewerThan([dartFile], gFile)) { + return !_hashesMatch(root, 'isar', inputs, _readLock(root)); + } } return false; } -bool _splashOutOfDate(String root) { +List _splashInputs(String root) { final config = File(p.join(root, 'flutter_native_splash.yaml')); final splashImage = File(p.join(root, 'assets/images/logos/splash.png')); - final inputs = [config, splashImage] + return [config, splashImage] .where((f) => f.existsSync()) .map((f) => File(p.canonicalize(f.path))) .toList(); +} + +bool _splashOutOfDate(String root) { + final inputs = _splashInputs(root); if (inputs.isEmpty) return false; final output = File( p.join(root, 'android/app/src/main/res/drawable/launch_background.xml'), ); - return _anyNewerThan(inputs, output); + if (!_anyNewerThan(inputs, output)) return false; + return !_hashesMatch(root, 'splash', inputs, _readLock(root)); } Future _generateAndroid12SplashImage(String root) async { diff --git a/firka_wear/codegen-lock.yaml b/firka_wear/codegen-lock.yaml new file mode 100644 index 0000000..f80d03c --- /dev/null +++ b/firka_wear/codegen-lock.yaml @@ -0,0 +1,18 @@ +icons: + "flutter_launcher_icons.yaml": "2c1bf9056dfe8db94333143643d2b46308fa332e08de9eda62046a941e83aaaa" + "pubspec.yaml": "6be6ac0844c8554f0e2d3eb4d60adf3debae1b29528b4cfba2023d0ccc5e33bf" + "assets/images/logos/colored_logo.png": "ff9c3452b1b0ed07ffa9067fa4cf4dae45dad3e46f5cb6ef4a62ac8c05d8c080" + "assets/images/logos/monochrome_logo.png": "188d2b0a64c70323b09bcee721663d6698fb557066f20ddaec97bba6869c1c6c" + "assets/images/logos/colored_logo_without_mustache.png": "d11cff9f38985885873bfdd2d84e61f8fab03803eada94d4caac1545ef3685f3" + "assets/images/logos/colored_logo_only_mustache.png": "bad6220c11bdfb1dfe04e5173bd2ebedd3999689d4b3a68fc63dc520c96dd33b" +l10n: + "l10n.yml": "a57bc304cac4a2b0235593586f17f400a5165d67fc9aadeaa11893cfa36ee082" + "lib/l10n/app_de.arb": "55f030b312cc07ff05cdc3d6ee10ef9bdec3243b507225e9a47196444518d955" + "lib/l10n/app_en.arb": "efac3f14d8ecc3e278f80a3e5aff599a88e408d2e30ff9e30f889978f465823a" + "lib/l10n/app_hu.arb": "a7f61bf4452a639d61c350f6674fdb5fd424f9ab31a195a200d446763fa8b396" +isar: + "lib/data/models/app_settings_model.dart": "2bf4d089ccfcb73edbca5b2d5757e1e698ddde2b8783d212a870aac3157fbb5b" + "lib/data/models/generic_cache_model.dart": "dd9979a4f0ba37ce5fd733bf0966088a759b5f356d97ea09c65eefffe8984639" + "lib/data/models/homework_cache_model.dart": "911748133c4bcb32bebe40a7c2f6f30d63c030b89a77c6825ec19643d8f8b3c6" + "lib/data/models/timetable_cache_model.dart": "078cbc0c5b1e3f0303a56bfe1e55df7669f0b06687ba399ddcae2df2b565d4c7" + "lib/data/models/token_model.dart": "3dc6211102c00d8382bfaa929e0ca7dedd7b1771c337f4e96d54e47572e5f6e1" diff --git a/firka_wear/pubspec.yaml b/firka_wear/pubspec.yaml index 08f6dd6..f7bd217 100644 --- a/firka_wear/pubspec.yaml +++ b/firka_wear/pubspec.yaml @@ -65,9 +65,11 @@ dependencies: dev_dependencies: build_runner: any + crypto: ^3.0.6 flutter_test: sdk: flutter flutter_lints: ^6.0.0 + yaml: ^3.1.2 isar_community_generator: 3.3.0 android_notification_icons: ^0.0.1 integration_test: diff --git a/firka_wear/scripts/codegen.dart b/firka_wear/scripts/codegen.dart index 190f2bc..6325632 100644 --- a/firka_wear/scripts/codegen.dart +++ b/firka_wear/scripts/codegen.dart @@ -1,32 +1,43 @@ import 'dart:io'; +import 'package:crypto/crypto.dart'; import 'package:path/path.dart' as p; +import 'package:yaml/yaml.dart' as yaml; + +const _lockFileName = 'codegen-lock.yaml'; void main() async { final root = _projectRoot(); var ran = false; if (_iconsOutOfDate(root)) { + final inputs = _iconsInputs(root); stdout.writeln('Icons out of date, running flutter_launcher_icons...'); await _run('dart', ['run', 'flutter_launcher_icons'], root); + _updateLockWithHashes(root, 'icons', _computeHashes(root, inputs)); ran = true; } if (_l10nOutOfDate(root)) { + final inputs = _l10nInputs(root); stdout.writeln('l10n out of date, running flutter gen-l10n...'); await _run('flutter', [ 'gen-l10n', '--template-arb-file', 'app_hu.arb', ], root); + _updateLockWithHashes(root, 'l10n', _computeHashes(root, inputs)); ran = true; } if (_isarOutOfDate(root)) { + final inputs = _isarInputs(root); + final hashes = _computeHashes(root, inputs); stdout.writeln( 'Isar generated dart files out of date, running build_runner...', ); await _run('dart', ['run', 'build_runner', 'build'], root); + _updateLockWithHashes(root, 'isar', hashes); ran = true; } @@ -37,7 +48,92 @@ void main() async { String _projectRoot() { final script = p.canonicalize(Platform.script.toFilePath()); - return p.dirname(p.dirname(script)); + return p.canonicalize(p.dirname(p.dirname(script))); +} + +String _lockPath(String root) => p.join(root, _lockFileName); + +Map>? _readLock(String root) { + final file = File(_lockPath(root)); + if (!file.existsSync()) return null; + try { + final content = file.readAsStringSync(); + final decoded = yaml.loadYaml(content); + if (decoded is! Map) return null; + final result = >{}; + for (final entry in decoded.entries) { + if (entry.value is! Map) continue; + final inner = entry.value as Map; + result[entry.key.toString()] = inner.map( + (k, v) => MapEntry( + Platform.isWindows ? k.toString().toLowerCase() : k.toString(), + v?.toString() ?? '', + ), + ); + } + return result; + } catch (_) { + return null; + } +} + +void _writeLock(String root, Map> lock) { + final buf = StringBuffer(); + for (final stepEntry in lock.entries) { + buf.writeln('${stepEntry.key}:'); + for (final fileEntry in stepEntry.value.entries) { + buf.writeln( + ' "${_escapeYaml(fileEntry.key)}": "${_escapeYaml(fileEntry.value)}"', + ); + } + } + File(_lockPath(root)).writeAsStringSync(buf.toString()); +} + +String _escapeYaml(String s) => + s.replaceAll('\\', '\\\\').replaceAll('"', '\\"'); + +String _fileHash(File file) { + final bytes = file.readAsBytesSync(); + final digest = sha256.convert(bytes); + return digest.toString(); +} + +String _relativePath(String root, File file) { + final rel = p.relative(file.path, from: root); + final normalized = rel.replaceAll('\\', '/'); + return Platform.isWindows ? normalized.toLowerCase() : normalized; +} + +bool _hashesMatch( + String root, + String stepName, + List inputs, + Map>? lock, +) { + if (lock == null || !lock.containsKey(stepName)) return false; + final stepHashes = lock[stepName]!; + for (final f in inputs) { + final rel = _relativePath(root, f); + final stored = stepHashes[rel]; + if (stored == null || stored != _fileHash(f)) return false; + } + if (stepHashes.length != inputs.length) return false; + return true; +} + +Map _computeHashes(String root, List inputs) { + return {for (final f in inputs) _relativePath(root, f): _fileHash(f)}; +} + +void _updateLockWithHashes( + String root, + String stepName, + Map hashes, +) { + final lock = _readLock(root) ?? >{}; + lock[stepName] = Map.from(hashes); + _writeLock(root, lock); } DateTime? _modified(File file) { @@ -55,7 +151,7 @@ bool _anyNewerThan(Iterable inputs, File output) { return false; } -bool _iconsOutOfDate(String root) { +List _iconsInputs(String root) { final config = File(p.join(root, 'flutter_launcher_icons.yaml')); final pubspec = File(p.join(root, 'pubspec.yaml')); final imagePath = File(p.join(root, 'assets/images/logos/colored_logo.png')); @@ -68,25 +164,25 @@ bool _iconsOutOfDate(String root) { final foreground = File( p.join(root, 'assets/images/logos/colored_logo_only_mustache.png'), ); + return [config, pubspec, imagePath, monochrome, background, foreground] + .where((f) => f.existsSync()) + .map((f) => File(p.canonicalize(f.path))) + .toList(); +} - final inputs = [ - config, - pubspec, - imagePath, - monochrome, - background, - foreground, - ].where((f) => f.existsSync()).map((f) => File(p.canonicalize(f.path))); +bool _iconsOutOfDate(String root) { + final inputs = _iconsInputs(root); final output = File( p.join( root, 'android/app/src/main/res/mipmap-anydpi-v26/launcher_icon.xml', ), ); - return _anyNewerThan(inputs, output); + if (!_anyNewerThan(inputs, output)) return false; + return !_hashesMatch(root, 'icons', inputs, _readLock(root)); } -bool _l10nOutOfDate(String root) { +List _l10nInputs(String root) { final l10nDir = p.join(root, 'lib/l10n'); final l10nYml = File(p.join(root, 'l10n.yml')); final arbs = Directory(l10nDir) @@ -95,25 +191,39 @@ bool _l10nOutOfDate(String root) { .where((f) => f.path.endsWith('.arb')) .map((f) => File(p.canonicalize(f.path))) .toList(); - final inputs = [l10nYml, ...arbs].where((f) => f.existsSync()).cast(); - final output = File(p.join(root, 'lib/l10n/app_localizations.dart')); - return _anyNewerThan(inputs, output); + return [l10nYml, ...arbs].where((f) => f.existsSync()).cast().toList(); } -bool _isarOutOfDate(String root) { - final modelsDir = p.join(root, 'lib/data/models'); - if (!Directory(modelsDir).existsSync()) return false; +bool _l10nOutOfDate(String root) { + final inputs = _l10nInputs(root); + final output = File(p.join(root, 'lib/l10n/app_localizations.dart')); + if (!_anyNewerThan(inputs, output)) return false; + return !_hashesMatch(root, 'l10n', inputs, _readLock(root)); +} +List _isarInputs(String root) { + final modelsDir = p.join(root, 'lib/data/models'); + if (!Directory(modelsDir).existsSync()) return []; + final list = []; for (final entity in Directory(modelsDir).listSync()) { if (entity is! File || !entity.path.endsWith('.dart')) continue; final content = entity.readAsStringSync(); if (!content.contains("part '") || !content.contains('.g.dart')) continue; + list.add(File(p.canonicalize(entity.path))); + } + return list; +} - final baseName = p.basenameWithoutExtension(entity.path); - final gPath = p.join(modelsDir, '$baseName.g.dart'); - final dartFile = File(p.canonicalize(entity.path)); - final gFile = File(gPath); - if (_anyNewerThan([dartFile], gFile)) return true; +bool _isarOutOfDate(String root) { + final inputs = _isarInputs(root); + if (inputs.isEmpty) return false; + final modelsDir = p.join(root, 'lib/data/models'); + for (final dartFile in inputs) { + final baseName = p.basenameWithoutExtension(dartFile.path); + final gFile = File(p.join(modelsDir, '$baseName.g.dart')); + if (_anyNewerThan([dartFile], gFile)) { + return !_hashesMatch(root, 'isar', inputs, _readLock(root)); + } } return false; }