Merge branch 'dev' of https://git.firka.app/firka/firka into dev
21
firka/codegen-lock.yaml
Normal 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"
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -34,6 +34,7 @@ class HomeworkWidget extends StatelessWidget {
|
||||
"homeWithMark",
|
||||
color: appStyle.colors.accent,
|
||||
size: 24,
|
||||
package: 'firka',
|
||||
)
|
||||
: FirkaIconWidget(
|
||||
FirkaIconType.majesticons,
|
||||
|
||||
@@ -370,6 +370,7 @@ class LessonWidget extends StatelessWidget {
|
||||
'cupFilled',
|
||||
color: appStyle.colors.accent,
|
||||
size: 24,
|
||||
package: 'firka',
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
17
firka_common/lib/core/debug_helper.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
19
firka_common/lib/core/extensions.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
135
firka_common/lib/core/icon_helper.dart
Normal 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;
|
||||
}
|
||||
9
firka_common/lib/core/json_helper.dart
Normal 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;
|
||||
}
|
||||
16
firka_common/lib/firka_common.dart
Normal 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';
|
||||
137
firka_common/lib/ui/components/firka_card.dart
Normal 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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
52
firka_common/lib/ui/components/firka_shadow.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
97
firka_common/lib/ui/components/grade.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
100
firka_common/lib/ui/components/grade_helpers.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
35
firka_common/lib/ui/shared/class_icon.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
22
firka_common/lib/ui/shared/counter_digit.dart
Normal 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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
48
firka_common/lib/ui/shared/delayed_spinner.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
46
firka_common/lib/ui/shared/firka_icon.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
60
firka_common/lib/ui/shared/grade_small_card.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
309
firka_common/lib/ui/theme/style.dart
Normal 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
@@ -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
|
||||
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 7.5 KiB |
18
firka_wear/codegen-lock.yaml
Normal 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"
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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!);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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:
|
||||
|
||||
247
firka_wear/scripts/codegen.dart
Normal 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;
|
||||
}
|
||||