Files
firka/firka/lib/main.dart
2025-09-11 12:47:43 +02:00

428 lines
12 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'dart:ui' as ui;
import 'package:dio/dio.dart';
import 'package:firka/helpers/api/client/kreta_client.dart';
import 'package:firka/helpers/db/models/app_settings_model.dart';
import 'package:firka/helpers/db/models/generic_cache_model.dart';
import 'package:firka/helpers/db/models/timetable_cache_model.dart';
import 'package:firka/helpers/db/models/token_model.dart';
import 'package:firka/helpers/db/widget.dart';
import 'package:firka/helpers/extensions.dart';
import 'package:firka/helpers/firka_bundle.dart';
import 'package:firka/helpers/settings/setting.dart';
import 'package:firka/l10n/app_localizations_hu.dart';
import 'package:firka/ui/model/style.dart';
import 'package:firka/ui/phone/pages/error/error_page.dart';
import 'package:firka/ui/phone/screens/debug/debug_screen.dart';
import 'package:firka/ui/phone/screens/home/home_screen.dart';
import 'package:firka/ui/phone/screens/login/login_screen.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:isar/isar.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:watch_connectivity/watch_connectivity.dart';
import 'helpers/db/models/homework_cache_model.dart';
import 'helpers/update_notifier.dart';
import 'l10n/app_localizations.dart';
import 'l10n/app_localizations_de.dart';
import 'l10n/app_localizations_en.dart';
Isar? isarInit;
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
late AppInitialization initData;
bool initDone = false;
final dio = Dio();
final isBeta = true;
class DeviceInfo {
String model;
String versionRelease;
String versionSdkInt;
DeviceInfo(this.model, this.versionRelease, this.versionSdkInt);
@override
String toString() {
return "DeviceInfo(model = \"$model\", versionRelease = \"$versionRelease\""
", versionSdkInt = \"$versionSdkInt\"";
}
}
class AppInitialization {
final Isar isar;
final PackageInfo packageInfo;
final DeviceInfo devInfo;
late KretaClient client;
List<TokenModel> tokens;
bool hasWatchListener = false;
Uint8List? profilePicture;
SettingsStore settings;
UpdateNotifier settingsUpdateNotifier = UpdateNotifier();
AppLocalizations l10n;
AppInitialization({
required this.isar,
required this.devInfo,
required this.packageInfo,
required this.tokens,
required this.settings,
required this.l10n,
});
}
Future<Isar> initDB() async {
if (isarInit != null) return isarInit!;
final dir = await getApplicationDocumentsDirectory();
isarInit = await Isar.open(
[
TokenModelSchema,
GenericCacheModelSchema,
TimetableCacheModelSchema,
HomeworkCacheModelSchema,
AppSettingsModelSchema,
],
inspector: true,
directory: dir.path,
);
return isarInit!;
}
void initLang(AppInitialization data) {
switch ((data.settings.group("settings").subGroup("application")["language"]
as SettingsItemsRadio)
.activeIndex) {
case 1: // hu
data.l10n = AppLocalizationsHu();
break;
case 2: // en
data.l10n = AppLocalizationsEn();
break;
case 3: // de
data.l10n = AppLocalizationsDe();
break;
default: // auto
switch (ui.window.locale.languageCode) {
case 'hu':
data.l10n = AppLocalizationsHu();
break;
case 'en':
data.l10n = AppLocalizationsEn();
break;
case 'de':
data.l10n = AppLocalizationsDe();
break;
}
break;
}
}
void initTheme(AppInitialization data) {
final brightness =
SchedulerBinding.instance.platformDispatcher.platformBrightness;
switch ((data.settings.group("settings").subGroup("customization")["theme"]
as SettingsItemsRadio)
.activeIndex) {
case 1:
appStyle = lightStyle;
isLightMode.value = true;
break;
case 2:
appStyle = darkStyle;
isLightMode.value = false;
break;
default:
if (brightness == Brightness.dark) {
appStyle = darkStyle;
isLightMode.value = false;
} else {
appStyle = lightStyle;
isLightMode.value = true;
}
}
}
Future<void> _initData(AppInitialization init) async {
await init.settings.load(init.isar.appSettingsModels);
initLang(init);
initTheme(init);
init.settings = SettingsStore(init.l10n);
await init.settings.load(init.isar.appSettingsModels);
var dispatcher = SchedulerBinding.instance.platformDispatcher;
dispatcher.onPlatformBrightnessChanged = () {
globalUpdate.update();
initTheme(init);
};
resetOldTimeTableCache(init.isar);
resetOldHomeworkCache(init.isar);
if (init.tokens.isNotEmpty) {
final i = (init.settings.group("profile_settings")["e_kreta_account_picker"]
as SettingsKretenAccountPicker)
.accountIndex;
init.client = KretaClient(
(await init.isar.tokenModels.where().findAll())[i], init.isar);
await WidgetCacheHelper.updateWidgetCache(appStyle, init.client);
}
final dataDir = await getApplicationDocumentsDirectory();
var pfpFile = File(p.join(dataDir.path, "profile.webp"));
if (await pfpFile.exists()) {
init.profilePicture = await pfpFile.readAsBytes();
}
}
Future<AppInitialization> initializeApp() async {
if (initDone) {
await _initData(initData);
return initData;
}
final isar = await initDB();
final tokens = await isar.tokenModels.where().findAll();
if (kDebugMode) {
print('Token count: ${tokens.length}');
}
var devInfoFetched = false;
var devInfo = DeviceInfo("SM-A705FN", "11", "30");
try {
if (Platform.isAndroid) {
const channel = MethodChannel("firka.app/main");
final rawInfo =
((await channel.invokeMethod("get_info")) as String).split(";");
devInfo = DeviceInfo(rawInfo[0], rawInfo[1], rawInfo[2]);
devInfoFetched = true;
}
} catch (e) {
if (e is Error) {
debugPrintStack(stackTrace: e.stackTrace, label: e.toString());
} else {
debugPrint(e.toString());
}
}
debugPrint("Fetched device info: ${devInfoFetched ? "yes" : "no"}");
debugPrint("Using device info: ${devInfo.toString()}");
var init = AppInitialization(
isar: isar,
devInfo: devInfo,
packageInfo: await PackageInfo.fromPlatform(),
tokens: tokens,
settings: SettingsStore(AppLocalizationsHu()),
l10n: AppLocalizationsHu(),
);
await _initData(init);
init.settingsUpdateNotifier.addListener(() {
debugPrint("Settings updated");
});
return init;
}
void main() async {
dio.options.connectTimeout = Duration(seconds: 5);
dio.options.receiveTimeout = Duration(seconds: 3);
dio.options.validateStatus = (status) => status != null && status < 500;
runZonedGuarded(() async {
WidgetsFlutterBinding.ensureInitialized();
// Run App Initialization
runApp(InitializationScreen());
}, (error, stackTrace) {
debugPrint('Caught error: $error');
debugPrint('Stack trace: $stackTrace');
navigatorKey.currentState?.push(
MaterialPageRoute(
builder: (context) =>
ErrorPage(key: ValueKey('errorPage'), exception: error.toString()),
),
);
});
}
final ValueNotifier<bool> isLightMode = ValueNotifier<bool>(true);
final UpdateNotifier globalUpdate = UpdateNotifier();
class InitializationScreen extends StatelessWidget {
InitializationScreen({super.key});
final Future<AppInitialization> _init = initializeApp();
@override
Widget build(BuildContext context) {
return FutureBuilder<AppInitialization>(
future: _init,
builder: (context, snapshot) {
// Check if initialization is complete
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
debugPrintStack(
stackTrace: snapshot.stackTrace,
label: snapshot.error.toString());
// Handle initialization error
return MaterialApp(
key: ValueKey('errorPage'),
home: DefaultAssetBundle(
bundle: FirkaBundle(),
child: Scaffold(
body: Center(
child: Text(
'Error initializing app: ${snapshot.error}',
style: TextStyle(color: Colors.red),
),
),
)),
);
}
// Initialization successful, determine which screen to show
Widget screen;
assert(snapshot.data != null);
initData = snapshot.data!;
initDone = true;
var watch = WatchConnectivity();
if (!initData.hasWatchListener) {
initData.hasWatchListener = true;
watch.messageStream.listen((e) {
var msg = e.entries.toMap();
debugPrint("[Watch -> Phone]: ${msg["id"]}");
switch (msg["id"]) {
case "ping":
if (initData.tokens.isNotEmpty) {
debugPrint("[Phone -> Watch]: pong");
watch.sendMessage({"id": "pong"});
navigatorKey.currentState?.push(
MaterialPageRoute(
builder: (context) => HomeScreen(initData, true,
model: msg["model"] as String),
),
);
}
}
});
}
if (snapshot.data!.tokens.isEmpty) {
screen = LoginScreen(
initData,
key: ValueKey('loginScreen'),
);
} else {
screen = HomeScreen(
initData,
false,
key: ValueKey('homeScreen'),
);
}
return MaterialApp(
title: 'Firka',
key: ValueKey('firkaApp'),
navigatorKey: navigatorKey,
// Use the global navigator key
theme: ThemeData(
primarySwatch: Colors.lightGreen,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
localizationsDelegates: [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
home: DefaultAssetBundle(
bundle: FirkaBundle(),
child: ValueListenableBuilder<bool>(
valueListenable: isLightMode,
builder: (context, isLight, _) {
final overlay = SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness:
isLight ? Brightness.dark : Brightness.light,
statusBarBrightness:
isLight ? Brightness.light : Brightness.dark,
systemStatusBarContrastEnforced: false,
);
// Ensure system is updated immediately
SystemChrome.setSystemUIOverlayStyle(overlay);
return AnnotatedRegion<SystemUiOverlayStyle>(
value: overlay,
child: screen,
);
},
),
),
routes: {
'/login': (context) => DefaultAssetBundle(
bundle: FirkaBundle(),
child: LoginScreen(
initData,
key: ValueKey('loginScreen'),
),
),
'/debug': (context) => DefaultAssetBundle(
bundle: FirkaBundle(),
child: DebugScreen(
initData,
key: ValueKey('debugScreen'),
),
),
},
);
}
return MaterialApp(
home: DefaultAssetBundle(
bundle: FirkaBundle(),
child: Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
color: const Color(0xFF7CA021),
)
],
),
),
),
),
);
},
);
}
}