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/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/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..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( @@ -1245,15 +1233,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/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_grades_subject.dart b/firka/lib/ui/phone/pages/home/home_grades_subject.dart index 807d10a..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'; @@ -131,7 +132,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)); @@ -302,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/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/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 { diff --git a/firka/lib/ui/phone/screens/settings/settings_screen.dart b/firka/lib/ui/phone/screens/settings/settings_screen.dart index 9646fb2..4418afd 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), ], @@ -931,6 +948,7 @@ class _SettingsScreenState extends FirkaState { FirkaIconType.icons, "group", color: appStyle.colors.accent, + package: 'firka', ), SizedBox(width: 8), Text( @@ -1068,6 +1086,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/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, ), 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 b0bcd0e..2e74080 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 @@ -63,6 +65,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_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/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 a4b3758..791200f 100644 Binary files a/firka_wear/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png and b/firka_wear/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png differ diff --git a/firka_wear/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/firka_wear/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png index ede3642..d4e1eca 100644 Binary files a/firka_wear/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png and b/firka_wear/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png differ 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 31e7cce..b0dc1a5 100644 Binary files a/firka_wear/android/app/src/main/res/drawable-hdpi/ic_launcher_monochrome.png and b/firka_wear/android/app/src/main/res/drawable-hdpi/ic_launcher_monochrome.png differ diff --git a/firka_wear/android/app/src/main/res/mipmap-mdpi/launcher_icon.png b/firka_wear/android/app/src/main/res/mipmap-mdpi/launcher_icon.png index c7c3b95..b9e2d30 100644 Binary files a/firka_wear/android/app/src/main/res/mipmap-mdpi/launcher_icon.png and b/firka_wear/android/app/src/main/res/mipmap-mdpi/launcher_icon.png differ diff --git a/firka_wear/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png b/firka_wear/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png index a816189..80c55b2 100644 Binary files a/firka_wear/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png and b/firka_wear/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png differ 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/flutter_launcher_icons.yaml b/firka_wear/flutter_launcher_icons.yaml index 7342d0f..5ca5242 100644 --- a/firka_wear/flutter_launcher_icons.yaml +++ b/firka_wear/flutter_launcher_icons.yaml @@ -7,5 +7,3 @@ flutter_launcher_icons: adaptive_icon_foreground: "assets/images/logos/colored_logo_only_mustache.png" adaptive_icon_foreground_inset: 0 min_sdk_android: 21 - ios: true - remove_alpha_channel_ios: true 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/l10n b/firka_wear/lib/l10n index 72b2574..2ac00c3 160000 --- a/firka_wear/lib/l10n +++ b/firka_wear/lib/l10n @@ -1 +1 @@ -Subproject commit 72b257433b67e8b5a69eda118c31184eab047d7e +Subproject commit 2ac00c3bea85db999c961eabf02dd8515f08e75f 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..f7bd217 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 @@ -63,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 new file mode 100644 index 0000000..6325632 --- /dev/null +++ b/firka_wear/scripts/codegen.dart @@ -0,0 +1,247 @@ +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; + } + + if (!ran) { + stdout.writeln('All generated files are up to date.'); + } +} + +String _projectRoot() { + final script = p.canonicalize(Platform.script.toFilePath()); + 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) { + if (!file.existsSync()) return null; + return file.lastModifiedSync(); +} + +bool _anyNewerThan(Iterable 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; +} + +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')); + 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'), + ); + return [config, pubspec, imagePath, monochrome, background, foreground] + .where((f) => f.existsSync()) + .map((f) => File(p.canonicalize(f.path))) + .toList(); +} + +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', + ), + ); + if (!_anyNewerThan(inputs, output)) return false; + return !_hashesMatch(root, 'icons', inputs, _readLock(root)); +} + +List _l10nInputs(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(); + return [l10nYml, ...arbs].where((f) => f.existsSync()).cast().toList(); +} + +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; +} + +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; +} + +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; +}