Merge branch 'dev' of https://git.firka.app/firka/firka into dev

This commit is contained in:
zypherift
2026-03-03 20:00:42 +01:00
74 changed files with 1717 additions and 2073 deletions

21
firka/codegen-lock.yaml Normal file
View File

@@ -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"

View File

@@ -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';

View File

@@ -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';

View File

@@ -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<Lesson> {
List<Lesson> getAllSeqs(Lesson reference) {
@@ -59,26 +62,6 @@ extension TimetableExtension on Iterable<Lesson> {
}
}
extension IterableExtensionMap on Iterable<MapEntry<String, dynamic>> {
Map<String, dynamic> toMap() {
var map = <String, dynamic>{};
for (var item in this) {
map[item.key] = item.value;
}
return map;
}
}
extension IterableExtension<T> on Iterable<T> {
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');

View File

@@ -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<ClassIcon, RegExp> _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<ClassIcon, Uint8List> _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';

View File

@@ -1,9 +1 @@
List<T> listToTyped<T>(List<dynamic> dynamicList) {
var newList = List<T>.empty(growable: true);
for (var item in dynamicList) {
newList.add(item as T);
}
return newList;
}
export 'package:firka_common/core/json_helper.dart';

View File

@@ -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';

View File

@@ -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,
),
),
],
],
);
}

View File

@@ -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<Widget> left;
final List<Widget>? center;
final double? height;
final List<Widget>? 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<ThemeCubit>().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';

View File

@@ -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<ThemeCubit>().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';

View File

@@ -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';

View File

@@ -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<int> countsByGrade) getGradeDistribution(List<Grade> 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<Grade> {
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';

View File

@@ -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<HomeGradesScreen> {
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<HomeGradesScreen> {
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),

View File

@@ -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<HomeGradesSubjectScreen> {
}).toList();
var gradeWidgets = List<Widget>.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<HomeGradesSubjectScreen> {
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),

View File

@@ -608,6 +608,7 @@ class _HomeTimetableScreen extends FirkaState<HomeTimetableScreen>
"dropdownLeft",
size: 24,
color: appStyle.colors.accent,
package: 'firka',
),
),
onTap: () async {
@@ -672,6 +673,7 @@ class _HomeTimetableScreen extends FirkaState<HomeTimetableScreen>
"dropdownRight",
size: 24,
color: appStyle.colors.accent,
package: 'firka',
),
),
onTap: () async {

View File

@@ -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);

View File

@@ -410,6 +410,11 @@ class _HomeScreenState extends FirkaState<HomeScreen>
await LiveActivityService.showConsentScreenIfNeeded();
});
}
if (Platform.isIOS) {
Future.delayed(const Duration(seconds: 4), () {
if (!_disposed) _runLiveActivityLoginIfNeeded();
});
}
}
Future<void> _preloadImages() async {
@@ -535,10 +540,35 @@ class _HomeScreenState extends FirkaState<HomeScreen>
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 {

View File

@@ -157,6 +157,11 @@ class _SettingsScreenState extends FirkaState<SettingsScreen> {
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<SettingsScreen> {
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<SettingsScreen> {
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<SettingsScreen> {
FirkaIconType.icons,
"group",
color: appStyle.colors.accent,
package: 'firka',
),
SizedBox(width: 8),
Text(
@@ -1068,6 +1086,12 @@ class _SettingsScreenState extends FirkaState<SettingsScreen> {
item.iconType!,
item.iconData!,
color: appStyle.colors.accent,
package:
item.iconType == FirkaIconType.icons ||
item.iconType ==
FirkaIconType.majesticonsLocal
? 'firka'
: null,
),
SizedBox(width: 8),
],

View File

@@ -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<Grade> 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<GradeSummaryBar> createState() => _GradeSummaryBarState();
@@ -32,6 +39,9 @@ class _GradeSummaryBarState extends State<GradeSummaryBar> {
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<GradeSummaryBar> {
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,
),

View File

@@ -56,12 +56,14 @@ class _WelcomeWidgetState extends State<WelcomeWidget> {
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(

View File

@@ -34,6 +34,7 @@ class HomeworkWidget extends StatelessWidget {
"homeWithMark",
color: appStyle.colors.accent,
size: 24,
package: 'firka',
)
: FirkaIconWidget(
FirkaIconType.majesticons,

View File

@@ -370,6 +370,7 @@ class LessonWidget extends StatelessWidget {
'cupFilled',
color: appStyle.colors.accent,
size: 24,
package: 'firka',
),
),
),

View File

@@ -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',
),
),
),

View File

@@ -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';

View File

@@ -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';

View File

@@ -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<DelayedSpinnerWidget> createState() => _DelayedSpinner();
}
class _DelayedSpinner extends FirkaState<DelayedSpinnerWidget> {
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';

View File

@@ -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';

View File

@@ -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<Grade> 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';

View File

@@ -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';

View File

@@ -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:

View File

@@ -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<String, Map<String, String>>? _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 = <String, Map<String, String>>{};
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<String, Map<String, String>> 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<File> inputs,
Map<String, Map<String, String>>? 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<String, String> _computeHashes(String root, List<File> inputs) {
return {for (final f in inputs) _relativePath(root, f): _fileHash(f)};
}
void _updateLockWithHashes(
String root,
String stepName,
Map<String, String> hashes,
) {
final lock = _readLock(root) ?? <String, Map<String, String>>{};
lock[stepName] = Map.from(hashes);
_writeLock(root, lock);
}
DateTime? _modified(File file) {
@@ -65,7 +163,7 @@ bool _anyNewerThan(Iterable<File> inputs, File output) {
return false;
}
bool _iconsOutOfDate(String root) {
List<File> _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<File> _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<File>();
final output = File(p.join(root, 'lib/l10n/app_localizations.dart'));
return _anyNewerThan(inputs, output);
return [l10nYml, ...arbs].where((f) => f.existsSync()).cast<File>().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<File> _isarInputs(String root) {
final modelsDir = p.join(root, 'lib/data/models');
if (!Directory(modelsDir).existsSync()) return [];
final list = <File>[];
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<File> _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<void> _generateAndroid12SplashImage(String root) async {

View File

@@ -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();
}
}

View File

@@ -0,0 +1,19 @@
extension IterableExtensionMap on Iterable<MapEntry<String, dynamic>> {
Map<String, dynamic> toMap() {
var map = <String, dynamic>{};
for (var item in this) {
map[item.key] = item.value;
}
return map;
}
}
extension IterableExtension<T> on Iterable<T> {
T? firstWhereOrNull(bool Function(T element) test) {
for (var element in this) {
if (test(element)) return element;
}
return null;
}
}

View File

@@ -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<ClassIcon, RegExp> _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<ClassIcon, Uint8List> _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;
}

View File

@@ -0,0 +1,9 @@
List<T> listToTyped<T>(List<dynamic> dynamicList) {
var newList = List<T>.empty(growable: true);
for (var item in dynamicList) {
newList.add(item as T);
}
return newList;
}

View File

@@ -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';

View File

@@ -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<Widget> left;
final List<Widget>? center;
final double? height;
final List<Widget>? 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),
],
),
),
),
),
);
}
}
}

View File

@@ -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),
);
}
}
}

View File

@@ -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),
),
),
);
}
}

View File

@@ -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<int> countsByGrade) getGradeDistribution(List<Grade> 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<Grade> {
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;
}
}

View File

@@ -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,
);
}
}

View File

@@ -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),
),
);
}
}

View File

@@ -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<DelayedSpinnerWidget> createState() => _DelayedSpinner();
}
class _DelayedSpinner extends State<DelayedSpinnerWidget> {
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();
}
}

View File

@@ -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,
);
}
}
}

View File

@@ -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<Grade> 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),
),
),
),
),
),
],
);
}

View File

@@ -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;

21
firka_common/pubspec.yaml Normal file
View File

@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@@ -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"

View File

@@ -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

View File

@@ -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';

View File

@@ -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<MapEntry<String, dynamic>> {
Map<String, dynamic> toMap() {
var map = <String, dynamic>{};
for (var item in this) {
map[item.key] = item.value;
}
return map;
}
}
extension IterableExtension<T> on Iterable<T> {
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() {

View File

@@ -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<ClassIcon, RegExp> _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<ClassIcon, Uint8List> _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';

View File

@@ -1,9 +1 @@
List<T> listToTyped<T>(List<dynamic> dynamicList) {
var newList = List<T>.empty(growable: true);
for (var item in dynamicList) {
newList.add(item as T);
}
return newList;
}
export 'package:firka_common/core/json_helper.dart';

View File

@@ -19,7 +19,7 @@ Future<void> 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!);
}
}

View File

@@ -19,7 +19,7 @@ Future<void> 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!);
}
}

View File

@@ -1,57 +1 @@
import 'package:flutter/material.dart';
import 'package:firka_wear/ui/theme/style.dart';
class FirkaCard extends StatelessWidget {
final List<Widget> left;
final List<Widget>? 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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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<Grade> {
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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -1,42 +1 @@
import 'dart:async';
import 'package:flutter/material.dart';
class DelayedSpinnerWidget extends StatefulWidget {
const DelayedSpinnerWidget({super.key});
@override
State<DelayedSpinnerWidget> createState() => _DelayedSpinner();
}
class _DelayedSpinner extends State<DelayedSpinnerWidget> {
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';

View File

@@ -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';

View File

@@ -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<Grade> 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';

View File

@@ -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';

View File

@@ -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:

View File

@@ -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<String, Map<String, String>>? _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 = <String, Map<String, String>>{};
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<String, Map<String, String>> 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<File> inputs,
Map<String, Map<String, String>>? 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<String, String> _computeHashes(String root, List<File> inputs) {
return {for (final f in inputs) _relativePath(root, f): _fileHash(f)};
}
void _updateLockWithHashes(
String root,
String stepName,
Map<String, String> hashes,
) {
final lock = _readLock(root) ?? <String, Map<String, String>>{};
lock[stepName] = Map.from(hashes);
_writeLock(root, lock);
}
DateTime? _modified(File file) {
if (!file.existsSync()) return null;
return file.lastModifiedSync();
}
bool _anyNewerThan(Iterable<File> 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<File> _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<File> _l10nInputs(String root) {
final l10nDir = p.join(root, 'lib/l10n');
final l10nYml = File(p.join(root, 'l10n.yml'));
final arbs = Directory(l10nDir)
.listSync()
.whereType<File>()
.where((f) => f.path.endsWith('.arb'))
.map((f) => File(p.canonicalize(f.path)))
.toList();
return [l10nYml, ...arbs].where((f) => f.existsSync()).cast<File>().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<File> _isarInputs(String root) {
final modelsDir = p.join(root, 'lib/data/models');
if (!Directory(modelsDir).existsSync()) return [];
final list = <File>[];
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<bool> _run(
String executable,
List<String> 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;
}