3 Commits

Author SHA1 Message Date
e031c18ecb firka: fix Live Activity registration with fallback on resume and delayed retry 2026-03-03 17:37:39 +01:00
ba075c3b14 firka: add a section for ghost grades 2026-03-03 17:34:44 +01:00
32936c2aa5 firka: extract firka_common package with shared widgets (Isar kept separate)
- Create firka_common package with core helpers (debug, json, icon), theme,
  and shared widgets (FirkaCard, FirkaShadow, GradeWidget, GradeSmallCard,
  ClassIconWidget, FirkaIconWidget, DelayedSpinnerWidget, CounterDigitWidget)
- Keep Isar models (GenericCacheModel, TimetableCacheModel, HomeworkCacheModel,
  DatedCacheEntry, util) in firka and firka_wear - not moved to firka_common
- Update firka and firka_wear to depend on firka_common for shared UI only
- Add configurable roundGrade thresholds for firka settings
- Add package param to FirkaIconWidget for app asset paths
2026-03-03 15:33:11 +01:00
62 changed files with 1263 additions and 2030 deletions

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

@@ -1245,15 +1245,6 @@ class _GradeCalculatorSheetContentState
),
),
),
if (entries.isNotEmpty) ...[
SizedBox(height: 16),
Text(
'${widget.data.l10n.subject_avg}: ${_weightedAverage.toStringAsFixed(2)}',
style: appStyle.fonts.B_14R.apply(
color: appStyle.colors.textPrimary,
),
),
],
],
);
}

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

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

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),
],
@@ -930,6 +947,7 @@ class _SettingsScreenState extends FirkaState<SettingsScreen> {
FirkaIconType.icons,
"group",
color: appStyle.colors.accent,
package: 'firka',
),
SizedBox(width: 8),
Text(
@@ -1067,6 +1085,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

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

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

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