7 Commits

Author SHA1 Message Date
zypherift
38f2837034 feat: smoother graph using 3 weighted avarages :O 2026-03-05 00:17:47 +01:00
zypherift
b3b40b5e39 chore: stream home data cache-first then refresh 2026-03-05 00:12:23 +01:00
zypherift
85c2576804 fix: snap grade calculator weights to common percentages 2026-03-05 00:09:05 +01:00
zypherift
22d17f6969 fix: flashbang on new screen pushes 2026-03-04 23:44:24 +01:00
zypherift
7f09c507eb edit: replace spinner with dave 2026-03-04 23:26:01 +01:00
zypherift
1d00505ca7 edit: fine tune wall collision vibration 2026-03-04 23:09:27 +01:00
zypherift
94e6a13ab7 edit: l10n 2026-03-04 23:02:25 +01:00
9 changed files with 116 additions and 54 deletions

Submodule firka/android/app/src/main/java/org/brotli added at da8f329432

View File

@@ -16,6 +16,7 @@ import 'package:firka/core/bloc/theme_cubit.dart';
import 'package:firka/core/firka_bundle.dart'; import 'package:firka/core/firka_bundle.dart';
import 'package:firka/routing/app_router.dart'; import 'package:firka/routing/app_router.dart';
import 'package:firka/services/watch_sync_helper.dart'; import 'package:firka/services/watch_sync_helper.dart';
import 'package:firka/ui/theme/style.dart';
import 'package:firka/ui/phone/pages/extras/main_wear_pair.dart'; import 'package:firka/ui/phone/pages/extras/main_wear_pair.dart';
import 'package:firka/l10n/app_localizations.dart'; import 'package:firka/l10n/app_localizations.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
@@ -34,6 +35,23 @@ class _InitializationScreenState extends State<InitializationScreen> {
const Duration(seconds: 20), const Duration(seconds: 20),
); );
ThemeData _buildTheme(FirkaStyle style) {
return ThemeData(
scaffoldBackgroundColor: style.colors.background,
canvasColor: style.colors.background,
bottomSheetTheme: const BottomSheetThemeData(
backgroundColor: Colors.transparent,
),
colorScheme: ColorScheme.fromSeed(
seedColor: style.colors.accent,
brightness: style.isLight ? Brightness.light : Brightness.dark,
background: style.colors.background,
surface: style.colors.card,
),
useMaterial3: false,
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FutureBuilder<AppInitialization>( return FutureBuilder<AppInitialization>(
@@ -180,40 +198,47 @@ class _InitializationScreenState extends State<InitializationScreen> {
BlocProvider<ReauthCubit>.value(value: reauthCubit), BlocProvider<ReauthCubit>.value(value: reauthCubit),
BlocProvider<HomeRefreshCubit>.value(value: homeRefreshCubit), BlocProvider<HomeRefreshCubit>.value(value: homeRefreshCubit),
], ],
child: MaterialApp.router( child: BlocBuilder<ThemeCubit, ThemeState>(
title: 'Firka', builder: (context, themeState) {
key: const ValueKey('firkaApp'), final isLight = themeState.isLightMode;
routerConfig: _router!, final overlay = SystemUiOverlayStyle(
theme: ThemeData( statusBarColor: Colors.transparent,
primarySwatch: Colors.lightGreen, statusBarIconBrightness:
visualDensity: VisualDensity.adaptivePlatformDensity, isLight ? Brightness.dark : Brightness.light,
), statusBarBrightness:
localizationsDelegates: const [ isLight ? Brightness.light : Brightness.dark,
AppLocalizations.delegate, systemStatusBarContrastEnforced: false,
GlobalMaterialLocalizations.delegate, );
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
builder: (context, child) {
return BlocBuilder<ThemeCubit, ThemeState>(
builder: (context, themeState) {
final isLight = themeState.isLightMode;
final overlay = SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: isLight
? Brightness.dark
: Brightness.light,
statusBarBrightness: isLight
? Brightness.light
: Brightness.dark,
systemStatusBarContrastEnforced: false,
);
SystemChrome.setSystemUIOverlayStyle(overlay); final themeMode =
isLight ? ThemeMode.light : ThemeMode.dark;
final fallbackBg = isLight
? lightStyle.colors.background
: darkStyle.colors.background;
SystemChrome.setSystemUIOverlayStyle(overlay);
return MaterialApp.router(
title: 'Firka',
key: const ValueKey('firkaApp'),
routerConfig: _router!,
theme: _buildTheme(lightStyle),
darkTheme: _buildTheme(darkStyle),
themeMode: themeMode,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
builder: (context, child) {
return AnnotatedRegion<SystemUiOverlayStyle>( return AnnotatedRegion<SystemUiOverlayStyle>(
value: overlay, value: overlay,
child: child ?? const SizedBox.shrink(), child: ColoredBox(
color: fallbackBg,
child: child ?? const SizedBox.shrink(),
),
); );
}, },
); );

View File

@@ -1048,9 +1048,13 @@ class _GradeCalculatorSheetContent extends StatefulWidget {
class _GradeCalculatorSheetContentState class _GradeCalculatorSheetContentState
extends State<_GradeCalculatorSheetContent> { extends State<_GradeCalculatorSheetContent> {
int selectedGrade = 3; int selectedGrade = 3;
int weightPercent = 100; int weightIndex = 1; // 0-based index into _snapPoints
final List<(int grade, int weight)> entries = []; final List<(int grade, int weight)> entries = [];
static const _snapPoints = [50, 100, 200, 300, 500];
int get weightPercent => _snapPoints[weightIndex];
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
@@ -1184,11 +1188,12 @@ class _GradeCalculatorSheetContentState
trackHeight: 8, trackHeight: 8,
), ),
child: Slider( child: Slider(
value: weightPercent.toDouble(), value: weightIndex.toDouble(),
min: 1, min: 0,
max: 500, max: (_snapPoints.length - 1).toDouble(),
divisions: 499, divisions: _snapPoints.length - 1,
onChanged: (v) => setState(() => weightPercent = v.round()), label: '${weightPercent}%',
onChanged: (v) => setState(() => weightIndex = v.round()),
), ),
), ),
), ),

View File

@@ -55,7 +55,7 @@ class _HomeMainScreen extends FirkaState<HomeMainScreen> {
} }
} }
Future<void> fetchData({bool cacheOnly = true}) async { Future<void> fetchData({bool cacheOnly = false}) async {
final midnight = now.getMidnight(); final midnight = now.getMidnight();
var lessonsFetched = 0; var lessonsFetched = 0;

View File

@@ -69,6 +69,7 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
"assets/images/carousel_dark/slide3.webp", "assets/images/carousel_dark/slide3.webp",
"assets/images/carousel_dark/slide4.webp", "assets/images/carousel_dark/slide4.webp",
"assets/images/logos/colored_logo.webp", "assets/images/logos/colored_logo.webp",
"assets/images/logos/loading.gif",
]; ];
try { try {
await ImagePreloader.preloadMultipleAssets(FirkaBundle(), imagePaths); await ImagePreloader.preloadMultipleAssets(FirkaBundle(), imagePaths);
@@ -816,32 +817,42 @@ class _FloatingCardsSlideState extends State<_FloatingCardsSlide>
double vx = _velocities[i].dx; double vx = _velocities[i].dx;
double vy = _velocities[i].dy; double vy = _velocities[i].dy;
// capture incoming velocity components before any bounce damping
final double preVx = vx;
final double preVy = vy;
double impactSpeed = 0.0; // normal to the wall
bool hit = false; bool hit = false;
if (pos.dx < minX) { if (pos.dx < minX) {
pos = Offset(minX, pos.dy); pos = Offset(minX, pos.dy);
vx = vx.abs() * _bounceDamping; vx = vx.abs() * _bounceDamping;
impactSpeed = math.max(impactSpeed, preVx.abs());
hit = true; hit = true;
} else if (pos.dx > maxX) { } else if (pos.dx > maxX) {
pos = Offset(maxX, pos.dy); pos = Offset(maxX, pos.dy);
vx = -vx.abs() * _bounceDamping; vx = -vx.abs() * _bounceDamping;
impactSpeed = math.max(impactSpeed, preVx.abs());
hit = true; hit = true;
} }
if (pos.dy < minY) { if (pos.dy < minY) {
pos = Offset(pos.dx, minY); pos = Offset(pos.dx, minY);
vy = vy.abs() * _bounceDamping; vy = vy.abs() * _bounceDamping;
impactSpeed = math.max(impactSpeed, preVy.abs());
hit = true; hit = true;
} else if (pos.dy > maxY) { } else if (pos.dy > maxY) {
pos = Offset(pos.dx, maxY); pos = Offset(pos.dx, maxY);
vy = -vy.abs() * _bounceDamping; vy = -vy.abs() * _bounceDamping;
impactSpeed = math.max(impactSpeed, preVy.abs());
hit = true; hit = true;
} }
_velocities[i] = _clampVel(Offset(vx, vy)); _velocities[i] = _clampVel(Offset(vx, vy));
_positions[i] = pos; _positions[i] = pos;
if (hit && _velocities[i].distance > _vibrateSpeedThreshold) { // vibrate only when the component toward the wall exceeded threshold
if (hit && impactSpeed > _vibrateSpeedThreshold) {
wallHitThisTick = true; wallHitThisTick = true;
} }
} }

View File

@@ -93,6 +93,27 @@ class _GradeChartState extends State<GradeChart> {
} }
} }
List<FlSpot> _smoothSpots(List<FlSpot> input) {
if (input.length < 3) return input;
final smoothed = <FlSpot>[];
for (var i = 0; i < input.length; i++) {
if (i == 0 || i == input.length - 1) {
smoothed.add(input[i]);
continue;
}
final prev = input[i - 1].y;
final curr = input[i].y;
final next = input[i + 1].y;
final blended = (0.25 * prev) + (0.5 * curr) + (0.25 * next);
smoothed.add(FlSpot(input[i].x, blended));
}
return smoothed;
}
@override @override
void didUpdateWidget(covariant GradeChart oldWidget) { void didUpdateWidget(covariant GradeChart oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
@@ -213,8 +234,10 @@ class _GradeChartState extends State<GradeChart> {
} }
LineChartData avgData() { LineChartData avgData() {
var firstX = spots.first.x; final smoothedSpots = _smoothSpots(spots);
var lastX = spots.last.x;
var firstX = smoothedSpots.first.x;
var lastX = smoothedSpots.last.x;
if (firstX == lastX) { if (firstX == lastX) {
lastX = firstX + 1; lastX = firstX + 1;
} }
@@ -359,12 +382,12 @@ class _GradeChartState extends State<GradeChart> {
lineBarsData: [ lineBarsData: [
LineChartBarData( LineChartBarData(
spots: spots, spots: smoothedSpots,
isCurved: true, isCurved: true,
curveSmoothness: 0.35, curveSmoothness: 0.5,
showingIndicators: _touchedIndex != null ? [_touchedIndex!] : [], showingIndicators: _touchedIndex != null ? [_touchedIndex!] : [],
gradient: LinearGradient( gradient: LinearGradient(
colors: [for (final s in spots) colorForY(s.y)], colors: [for (final s in smoothedSpots) colorForY(s.y)],
), ),
barWidth: 5, barWidth: 5,
isStrokeCapRound: true, isStrokeCapRound: true,
@@ -373,7 +396,8 @@ class _GradeChartState extends State<GradeChart> {
show: true, show: true,
gradient: LinearGradient( gradient: LinearGradient(
colors: [ colors: [
for (final s in spots) colorForY(s.y).withValues(alpha: 0.1), for (final s in smoothedSpots)
colorForY(s.y).withValues(alpha: 0.1),
], ],
), ),
), ),

View File

@@ -276,15 +276,10 @@ class _LoginWebviewWidgetState extends FirkaState<LoginWebviewWidget>
child: Container( child: Container(
color: appStyle.colors.background, color: appStyle.colors.background,
child: Center( child: Center(
child: SizedBox( child: Image.asset(
width: 32, "assets/images/logos/loading.gif",
height: 32, width: 50,
child: CircularProgressIndicator( height: 50,
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(
appStyle.colors.accent,
),
),
), ),
), ),
), ),

View File

@@ -81,6 +81,7 @@ flutter:
- .env - .env
- assets/images/logos/colored_logo.webp - assets/images/logos/colored_logo.webp
- assets/images/logos/dave.svg - assets/images/logos/dave.svg
- assets/images/logos/loading.gif
- assets/images/carousel/ - assets/images/carousel/
- assets/images/carousel_dark/ - assets/images/carousel_dark/
- assets/images/icons/ - assets/images/icons/