Compare commits

...

87 Commits
3.0.4 ... 3.2.1

Author SHA1 Message Date
unknown
ff72d146c0 version bump 2022-01-14 23:50:11 +01:00
unknown
088b6e4580 mobile 2022-01-14 23:44:47 +01:00
Unknown
66793607e8 Update README.md 2022-01-14 23:19:04 +01:00
unknown
c91e792c6c changelog 2022-01-14 23:09:50 +01:00
unknown
16d0bd4163 mobile 2022-01-14 23:09:38 +01:00
Unknown
e559338483 Update README.md 2022-01-08 18:28:47 +01:00
unknown
f2c29aa81b 3.2.0 2022-01-08 15:42:49 +01:00
unknown
e44a3b7330 mobile 2022-01-07 07:05:54 +01:00
unknown
64606311bf mobile 2022-01-07 06:49:17 +01:00
unknown
43c4d2e454 mobile 2022-01-05 16:11:10 +01:00
unknown
9b579ad196 cleanup 2022-01-05 16:10:21 +01:00
unknown
425a4aaa91 cleanup 2022-01-05 15:39:24 +01:00
unknown
506c04aea9 build fix 2022-01-05 13:47:53 +01:00
unknown
cd18cfb220 mobile 2022-01-05 13:03:02 +01:00
unknown
f071e59e7a kreten 2022-01-05 13:02:57 +01:00
unknown
49f3a447b3 desktop 2022-01-05 13:02:52 +01:00
unknown
339dbea1ef flutter linting 2022-01-05 13:02:22 +01:00
unknown
79f6ef4c50 kreta 2022-01-05 12:32:26 +01:00
unknown
ae18fbab2e mobile 2022-01-05 12:32:18 +01:00
unknown
9003e13aa1 updater update 2022-01-05 12:30:43 +01:00
unknown
e58f4859b4 changelog 2022-01-05 12:30:07 +01:00
unknown
be809c6aaf Merge branch 'master' of ssh://github.com/filc/naplo 2022-01-05 12:18:48 +01:00
Brúnó Salomon
a6b012035b mobile 2021-12-30 17:26:43 +01:00
unknown
a0ef512c55 changelog 2021-12-03 23:02:12 +01:00
unknown
4e1c1be6e4 better updates 2021-12-03 23:00:13 +01:00
unknown
92a93941ab changelog 2021-12-03 22:08:27 +01:00
Unknown
ece621455c Update README.md 2021-11-20 15:55:44 +01:00
Unknown
0787f97a6f Update README.md 2021-11-17 22:52:29 +01:00
unknown
c74975e3bc kreten 2021-11-07 11:26:29 +01:00
unknown
4b669b0069 mobile 2021-11-07 11:26:21 +01:00
unknown
d24286b61c mobile 2021-11-07 10:35:57 +01:00
unknown
40f3d2159a revert build fixes, downgrade permission_handler (sdk 30) 2021-11-02 22:10:35 +01:00
unknown
8df77b5c06 3.1.1 changelog 2021-11-02 21:30:49 +01:00
unknown
fd88b5ee55 build fix (gradle ver) 2021-11-02 21:05:11 +01:00
unknown
ceaafb4897 build fix 2021-11-02 20:28:12 +01:00
unknown
6617882892 build fix 2021-11-02 19:57:56 +01:00
unknown
1b16b748d4 bump android sdk version 2021-11-02 19:57:28 +01:00
unknown
7014255b89 bump 3.1.1 2021-11-02 16:12:34 +01:00
unknown
d86f2fffe5 kreten 2021-11-02 16:07:38 +01:00
unknown
de1b0444b6 kreten 2021-11-02 14:12:33 +01:00
unknown
bd1a8358f6 mobile 2021-11-02 14:12:23 +01:00
unknown
8a670b3a4c bump 2021-10-03 20:14:02 +02:00
unknown
2917a2e913 mobile 2021-10-03 16:19:45 +02:00
unknown
414c183399 mobile 2021-10-03 15:13:29 +02:00
unknown
428d6ff975 wait for progress animation to finish 2021-10-03 15:04:47 +02:00
unknown
c418a61133 cleanup 2021-10-03 14:20:43 +02:00
unknown
7e83d7b969 sqflite fix 2021-10-03 12:17:32 +02:00
unknown
bdf6cc20c7 linux .desktop file 2021-10-03 01:11:02 +02:00
unknown
da089da22b add linux desktop support 2021-10-03 01:04:45 +02:00
unknown
fa3c0954c4 mobile 2021-10-03 00:44:50 +02:00
unknown
f4843211f5 changelog 2021-10-03 00:42:03 +02:00
unknown
5e1727eadd kreten 2021-10-03 00:28:35 +02:00
unknown
dd103e7474 mobile 2021-10-03 00:28:33 +02:00
unknown
aa61301b17 format weekday on current week 2021-10-03 00:27:58 +02:00
unknown
af360fda53 refactor 2021-10-03 00:27:40 +02:00
unknown
a56453ab9d status 2021-10-02 21:47:41 +02:00
unknown
c9aed14d7c changelog 2021-10-02 15:46:52 +02:00
unknown
0cef766ee6 bump 3.1.0 2021-10-02 15:45:43 +02:00
unknown
c8cd6bf9b8 subject icon fix 2021-10-02 15:44:33 +02:00
unknown
d9e8b4f4ed add tomorrow 2021-10-02 15:35:36 +02:00
unknown
8ab96a32c3 changelog 2021-10-02 00:14:11 +02:00
unknown
22c8a285ab kreten 2021-10-02 00:07:15 +02:00
unknown
d819245e31 bump 3.0.6 2021-09-30 23:09:45 +02:00
unknown
6b55721ec5 mobile 2021-09-30 22:58:57 +02:00
unknown
13062c4a9b kreten 2021-09-30 22:58:50 +02:00
unknown
2188eaf1c6 high refresh rate patch 2021-09-30 22:37:30 +02:00
unknown
922d252c57 mobile 2021-09-30 21:57:55 +02:00
unknown
7a5290efe5 kreten 2021-09-30 21:57:52 +02:00
unknown
d1e6cc1fbb connectivity testing 2021-09-30 21:57:45 +02:00
unknown
b6a8696911 fix network bug 2021-09-30 21:57:21 +02:00
unknown
f7efd65f5e mobile 2021-09-30 19:51:29 +02:00
unknown
9963f65ab2 mobile 2021-09-30 19:16:53 +02:00
unknown
8d8c3a54c2 use default user-agent for config 2021-09-29 18:18:50 +02:00
unknown
c49d93c7b1 mobile 2021-09-26 20:59:04 +02:00
unknown
b0a7ab20d9 changelog 2021-09-26 01:45:10 +02:00
unknown
59a9e0e236 bump 3.0.5 2021-09-26 01:40:29 +02:00
unknown
e83202ba29 cleanup 2021-09-26 01:39:41 +02:00
unknown
d4489dd9d4 kreten 2021-09-26 01:33:36 +02:00
Unknown
debb4cac5b Merge pull request #26 from filc/analytics
Analytics
2021-09-26 01:32:15 +02:00
unknown
acbdf2aecb Merge branch 'master' into analytics 2021-09-26 01:31:39 +02:00
unknown
5d9ea4311a mobile 2021-09-20 12:53:04 +02:00
unknown
8aa24d7a32 mobile 2021-09-20 12:39:07 +02:00
unknown
9fbcef357e error report 2021-09-20 12:38:06 +02:00
unknown
e9423a8535 mobile 2021-09-19 18:47:20 +02:00
unknown
2a7265256d analytics client 2021-09-19 17:56:38 +02:00
unknown
d5ba231fcc fix database migration 2021-09-18 16:30:17 +02:00
unknown
903cbba69a basics 2021-09-12 19:33:26 +02:00
51 changed files with 930 additions and 182 deletions

View File

@@ -9,16 +9,16 @@
## Setup ## Setup
### Clone the project: ### Clone the project
``` ```sh
$ git clone --recursive https://github.com/filc/naplo git clone --recursive https://github.com/filc/naplo
$ cd naplo cd naplo
``` ```
### Run the app: ### Run the app
``` ```sh
$ cd filcnaplo cd filcnaplo
$ flutter run flutter run
``` ```

View File

@@ -1,3 +1,3 @@
- Haptikus visszajelzés - Félévi, évvégi értesítő
- Animációk - Esztétikai újítások
- Hibajavítások - Hibajavítások

View File

@@ -0,0 +1,29 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View File

@@ -1,6 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="hu.filc.naplo"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="hu.filc.naplo">
<application android:label="Filc Napló" android:icon="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true"> <application android:label="Filc Napló" android:icon="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true">
<activity android:name=".MainActivity" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize"> <activity android:exported="true" android:name=".MainActivity" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
<meta-data android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme" /> <meta-data android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme" />
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>

View File

@@ -1,9 +1,9 @@
buildscript { buildscript {
ext.kotlin_version = '1.3.50' ext.kotlin_version = '1.6.10'
ext { ext {
compileSdkVersion = 30 compileSdkVersion = 31
targetSdkVersion = 30 targetSdkVersion = 31
appCompatVersion = "1.1.0" appCompatVersion = "1.1.0"
} }
@@ -33,8 +33,8 @@ subprojects {
afterEvaluate {project -> afterEvaluate {project ->
if (project.plugins.hasPlugin('android') || project.plugins.hasPlugin('android-library')) { if (project.plugins.hasPlugin('android') || project.plugins.hasPlugin('android-library')) {
android { android {
compileSdkVersion 30 compileSdkVersion 31
buildToolsVersion '30.0.3' buildToolsVersion '31.0.0'
} }
} }
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,23 +1,35 @@
// ignore_for_file: avoid_print
import 'dart:convert'; import 'dart:convert';
import 'package:filcnaplo/models/config.dart'; import 'package:filcnaplo/models/config.dart';
import 'package:filcnaplo/models/news.dart'; import 'package:filcnaplo/models/news.dart';
import 'package:filcnaplo/models/release.dart'; import 'package:filcnaplo/models/release.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/models/supporter.dart'; import 'package:filcnaplo/models/supporter.dart';
import 'package:filcnaplo_kreta_api/models/school.dart'; import 'package:filcnaplo_kreta_api/models/school.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:connectivity_plus/connectivity_plus.dart';
class FilcAPI { class FilcAPI {
static const SCHOOL_LIST = "https://filcnaplo.hu/v2/school_list.json"; // Public API
static const CONFIG = "https://filcnaplo.hu/v2/config.json"; static const schoolList = "https://filcnaplo.hu/v2/school_list.json";
static const NEWS = "https://filcnaplo.hu/v2/news.json"; static const news = "https://filcnaplo.hu/v2/news.json";
static const SUPPORTERS = "https://filcnaplo.hu/v2/supporters.json"; static const supporters = "https://filcnaplo.hu/v2/supporters.json";
static const REPO = "filc/naplo";
static const RELEASES = "https://api.github.com/repos/$REPO/releases"; // Private API
static const config = "https://api.filcnaplo.hu/config";
static const reportApi = "https://api.filcnaplo.hu/report";
// Updates
static const repo = "filc/naplo";
static const releases = "https://api.github.com/repos/$repo/releases";
static Future<bool> checkConnectivity() async => (await Connectivity().checkConnectivity()) != ConnectivityResult.none;
static Future<List<School>?> getSchools() async { static Future<List<School>?> getSchools() async {
try { try {
http.Response res = await http.get(Uri.parse(SCHOOL_LIST)); http.Response res = await http.get(Uri.parse(schoolList));
if (res.statusCode == 200) { if (res.statusCode == 200) {
List<School> schools = (jsonDecode(res.body) as List).cast<Map>().map((json) => School.fromJson(json)).toList(); List<School> schools = (jsonDecode(res.body) as List).cast<Map>().map((json) => School.fromJson(json)).toList();
@@ -35,15 +47,22 @@ class FilcAPI {
} }
} }
static Future<Config?> getConfig() async { static Future<Config?> getConfig(SettingsProvider settings) async {
Map<String, String> headers = {
"x-filc-id": settings.xFilcId,
"user-agent": SettingsProvider.defaultSettings().config.userAgent,
};
try { try {
http.Response res = await http.get(Uri.parse(CONFIG)); http.Response res = await http.get(Uri.parse(config), headers: headers);
if (res.statusCode == 200) { if (res.statusCode == 200) {
return Config.fromJson(jsonDecode(res.body)); return Config.fromJson(jsonDecode(res.body));
} else { } else if (res.statusCode == 429) {
throw "HTTP ${res.statusCode}: ${res.body}"; res = await http.get(Uri.parse(config));
if (res.statusCode == 200) return Config.fromJson(jsonDecode(res.body));
} }
throw "HTTP ${res.statusCode}: ${res.body}";
} catch (error) { } catch (error) {
print("ERROR: FilcAPI.getConfig: $error"); print("ERROR: FilcAPI.getConfig: $error");
} }
@@ -51,7 +70,7 @@ class FilcAPI {
static Future<List<News>?> getNews() async { static Future<List<News>?> getNews() async {
try { try {
http.Response res = await http.get(Uri.parse(NEWS)); http.Response res = await http.get(Uri.parse(news));
if (res.statusCode == 200) { if (res.statusCode == 200) {
return (jsonDecode(res.body) as List).cast<Map>().map((e) => News.fromJson(e)).toList(); return (jsonDecode(res.body) as List).cast<Map>().map((e) => News.fromJson(e)).toList();
@@ -65,7 +84,7 @@ class FilcAPI {
static Future<Supporters?> getSupporters() async { static Future<Supporters?> getSupporters() async {
try { try {
http.Response res = await http.get(Uri.parse(SUPPORTERS)); http.Response res = await http.get(Uri.parse(supporters));
if (res.statusCode == 200) { if (res.statusCode == 200) {
return Supporters.fromJson(jsonDecode(res.body)); return Supporters.fromJson(jsonDecode(res.body));
@@ -79,7 +98,7 @@ class FilcAPI {
static Future<List<Release>?> getReleases() async { static Future<List<Release>?> getReleases() async {
try { try {
http.Response res = await http.get(Uri.parse(RELEASES)); http.Response res = await http.get(Uri.parse(releases));
if (res.statusCode == 200) { if (res.statusCode == 200) {
return (jsonDecode(res.body) as List).cast<Map>().map((e) => Release.fromJson(e)).toList(); return (jsonDecode(res.body) as List).cast<Map>().map((e) => Release.fromJson(e)).toList();
@@ -92,7 +111,7 @@ class FilcAPI {
} }
static Future<http.StreamedResponse?> downloadRelease(Release release) { static Future<http.StreamedResponse?> downloadRelease(Release release) {
if (release.downloads.length > 0) { if (release.downloads.isNotEmpty) {
try { try {
var client = http.Client(); var client = http.Client();
var request = http.Request('GET', Uri.parse(release.downloads.first)); var request = http.Request('GET', Uri.parse(release.downloads.first));
@@ -104,4 +123,35 @@ class FilcAPI {
return Future.value(null); return Future.value(null);
} }
static Future<void> sendReport(ErrorReport report) async {
try {
http.Response res = await http.post(Uri.parse(reportApi), body: {
"os": report.os,
"version": report.version,
"error": report.error,
"stack_trace": report.stack,
});
if (res.statusCode != 200) {
throw "HTTP ${res.statusCode}: ${res.body}";
}
} catch (error) {
print("ERROR: FilcAPI.sendReport: $error");
}
}
}
class ErrorReport {
String stack;
String os;
String version;
String error;
ErrorReport({
required this.stack,
required this.os,
required this.version,
required this.error,
});
} }

View File

@@ -1,4 +1,4 @@
import 'dart:developer'; // ignore_for_file: avoid_print
import 'package:filcnaplo/utils/jwt.dart'; import 'package:filcnaplo/utils/jwt.dart';
import 'package:filcnaplo_kreta_api/providers/absence_provider.dart'; import 'package:filcnaplo_kreta_api/providers/absence_provider.dart';

View File

@@ -1,6 +1,9 @@
import 'dart:io';
import 'package:filcnaplo/database/query.dart'; import 'package:filcnaplo/database/query.dart';
import 'package:filcnaplo/database/store.dart'; import 'package:filcnaplo/database/store.dart';
import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sqflite.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
class DatabaseProvider { class DatabaseProvider {
// late Database _database; // late Database _database;
@@ -10,8 +13,14 @@ class DatabaseProvider {
late UserDatabaseStore userStore; late UserDatabaseStore userStore;
Future<void> init() async { Future<void> init() async {
var db = await openDatabase("app.db"); Database db;
// _database = db;
if (Platform.isLinux || Platform.isWindows) {
db = await databaseFactoryFfi.openDatabase("app.db");
} else {
db = await openDatabase("app.db");
}
query = DatabaseQuery(db: db); query = DatabaseQuery(db: db);
store = DatabaseStore(db: db); store = DatabaseStore(db: db);
userQuery = UserDatabaseQuery(db: db); userQuery = UserDatabaseQuery(db: db);

View File

@@ -72,10 +72,11 @@ class NewsProvider extends ChangeNotifier {
Provider.of<SettingsProvider>(_context, listen: false).update(_context, newsState: _state); Provider.of<SettingsProvider>(_context, listen: false).update(_context, newsState: _state);
if (_fresh > 0) if (_fresh > 0) {
show = true; show = true;
else } else {
show = false; show = false;
}
notifyListeners(); notifyListeners();
} }

View File

@@ -0,0 +1,75 @@
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/widgets.dart';
import 'package:http/http.dart' as http;
enum Status { network, maintenance, syncing }
class StatusProvider extends ChangeNotifier {
final List<Status> _stack = [];
double _progress = 0.0;
StatusProvider() {
_handleNetworkChanges();
}
Status? getStatus() => _stack.isNotEmpty ? _stack[0] : null;
// Status progress from 0.0 to 1.0
double get progress => _progress;
void _handleNetworkChanges() {
Connectivity().onConnectivityChanged.listen((event) {
if (event == ConnectivityResult.none) {
if (!_stack.contains(Status.network)) {
_stack.insert(0, Status.network);
notifyListeners();
}
} else {
if (_stack.contains(Status.network)) {
_stack.remove(Status.network);
notifyListeners();
}
}
});
}
void triggerRequest(http.Response res) {
if (res.headers.containsKey("x-maintenance-mode") || res.statusCode == 503) {
if (!_stack.contains(Status.maintenance)) {
_stack.insert(0, Status.maintenance);
notifyListeners();
}
} else {
if (_stack.contains(Status.maintenance)) {
_stack.remove(Status.maintenance);
notifyListeners();
}
}
}
void triggerSync({required int current, required int max}) {
double prev = _progress;
if (!_stack.contains(Status.syncing)) {
_stack.add(Status.syncing);
_progress = 0.0;
notifyListeners();
}
if (max == 0) {
_progress = 0.0;
} else {
_progress = current / max;
}
if (_progress == 1.0) {
notifyListeners();
// Wait for animation
Future.delayed(const Duration(milliseconds: 250), () {
_stack.remove(Status.syncing);
notifyListeners();
});
} else if (progress != prev) {
notifyListeners();
}
}
}

View File

@@ -0,0 +1,69 @@
import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/api/providers/status_provider.dart';
import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo_kreta_api/client/api.dart';
import 'package:filcnaplo_kreta_api/client/client.dart';
import 'package:filcnaplo_kreta_api/models/student.dart';
import 'package:filcnaplo_kreta_api/models/week.dart';
import 'package:filcnaplo_kreta_api/providers/absence_provider.dart';
import 'package:filcnaplo_kreta_api/providers/event_provider.dart';
import 'package:filcnaplo_kreta_api/providers/exam_provider.dart';
import 'package:filcnaplo_kreta_api/providers/grade_provider.dart';
import 'package:filcnaplo_kreta_api/providers/homework_provider.dart';
import 'package:filcnaplo_kreta_api/providers/message_provider.dart';
import 'package:filcnaplo_kreta_api/providers/note_provider.dart';
import 'package:filcnaplo_kreta_api/providers/timetable_provider.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
// Mutex
bool lock = false;
Future<void> syncAll(BuildContext context) {
if (lock) return Future.value();
// Lock
lock = true;
// ignore: avoid_print
print("INFO Syncing all");
UserProvider user = Provider.of<UserProvider>(context, listen: false);
StatusProvider statusProvider = Provider.of<StatusProvider>(context, listen: false);
List<Future<void>> tasks = [];
int taski = 0;
Future<void> _syncStatus(Future<void> future) async {
await future.onError((error, stackTrace) => null);
taski++;
statusProvider.triggerSync(current: taski, max: tasks.length);
}
tasks = [
_syncStatus(Provider.of<GradeProvider>(context, listen: false).fetch()),
_syncStatus(Provider.of<TimetableProvider>(context, listen: false).fetch(week: Week.current())),
_syncStatus(Provider.of<ExamProvider>(context, listen: false).fetch()),
_syncStatus(Provider.of<HomeworkProvider>(context, listen: false).fetch(from: DateTime.now().subtract(const Duration(days: 30)))),
_syncStatus(Provider.of<MessageProvider>(context, listen: false).fetchAll()),
_syncStatus(Provider.of<NoteProvider>(context, listen: false).fetch()),
_syncStatus(Provider.of<EventProvider>(context, listen: false).fetch()),
_syncStatus(Provider.of<AbsenceProvider>(context, listen: false).fetch()),
// Sync student
_syncStatus(() async {
if (user.user == null) return;
Map? studentJson = await Provider.of<KretaClient>(context, listen: false).getAPI(KretaAPI.student(user.instituteCode!));
if (studentJson == null) return;
Student student = Student.fromJson(studentJson);
user.user?.name = student.name;
// Store user
await Provider.of<DatabaseProvider>(context, listen: false).store.storeUser(user.user!);
}()),
];
return Future.wait(tasks)
// Unlock
.then((value) => lock = false);
}

View File

@@ -9,7 +9,7 @@ class UpdateProvider extends ChangeNotifier {
// Private // Private
late List<Release> _releases; late List<Release> _releases;
bool _available = false; bool _available = false;
bool get available => _available && _releases.length > 0; bool get available => _available && _releases.isNotEmpty;
PackageInfo? _packageInfo; PackageInfo? _packageInfo;
// Public // Public
@@ -23,6 +23,8 @@ class UpdateProvider extends ChangeNotifier {
PackageInfo.fromPlatform().then((value) => _packageInfo = value); PackageInfo.fromPlatform().then((value) => _packageInfo = value);
} }
String get currentVersion => _packageInfo?.version ?? "";
Future<void> fetch() async { Future<void> fetch() async {
if (!Platform.isAndroid) return; if (!Platform.isAndroid) return;
@@ -30,8 +32,9 @@ class UpdateProvider extends ChangeNotifier {
_releases.sort((a, b) => -a.version.compareTo(b.version)); _releases.sort((a, b) => -a.version.compareTo(b.version));
// Check for new releases // Check for new releases
if (_releases.length > 0) { if (_releases.isNotEmpty) {
_available = _packageInfo != null && _releases.first.version.compareTo(Version.fromString(_packageInfo?.version ?? "")) == 1; _available = _packageInfo != null && _releases.first.version.compareTo(Version.fromString(currentVersion)) == 1;
// ignore: avoid_print
if (_available) print("INFO: New update: ${releases.first.version}"); if (_available) print("INFO: New update: ${releases.first.version}");
notifyListeners(); notifyListeners();
} }

View File

@@ -3,7 +3,7 @@ import 'package:filcnaplo_kreta_api/models/student.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
class UserProvider with ChangeNotifier { class UserProvider with ChangeNotifier {
Map<String, User> _users = {}; final Map<String, User> _users = {};
String? _selectedUserId; String? _selectedUserId;
User? get user => _users[_selectedUserId]; User? get user => _users[_selectedUserId];
@@ -23,7 +23,9 @@ class UserProvider with ChangeNotifier {
void addUser(User user) { void addUser(User user) {
_users[user.id] = user; _users[user.id] = user;
print("DEBUG: Added User: ${user.id} ${user.name}"); if (kDebugMode) {
print("DEBUG: Added User: ${user.id} ${user.name}");
}
} }
void removeUser(String userId) { void removeUser(String userId) {

View File

@@ -1,8 +1,10 @@
import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:filcnaplo/api/client.dart'; import 'package:filcnaplo/api/client.dart';
import 'package:filcnaplo/api/providers/news_provider.dart'; import 'package:filcnaplo/api/providers/news_provider.dart';
import 'package:filcnaplo/api/providers/database_provider.dart'; import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/api/providers/status_provider.dart';
import 'package:filcnaplo/models/config.dart'; import 'package:filcnaplo/models/config.dart';
import 'package:filcnaplo/theme.dart'; import 'package:filcnaplo/theme.dart';
import 'package:filcnaplo_kreta_api/client/client.dart'; import 'package:filcnaplo_kreta_api/client/client.dart';
@@ -31,6 +33,7 @@ import 'package:filcnaplo_kreta_api/providers/timetable_provider.dart';
import 'package:filcnaplo/api/providers/user_provider.dart'; import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo/api/providers/update_provider.dart'; import 'package:filcnaplo/api/providers/update_provider.dart';
import 'package:filcnaplo_mobile_ui/pages/grades/calculator/grade_calculator_provider.dart'; import 'package:filcnaplo_mobile_ui/pages/grades/calculator/grade_calculator_provider.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart';
class App extends StatelessWidget { class App extends StatelessWidget {
final SettingsProvider settings; final SettingsProvider settings;
@@ -38,16 +41,19 @@ class App extends StatelessWidget {
final DatabaseProvider database; final DatabaseProvider database;
App({Key? key, required this.database, required this.settings, required this.user}) : super(key: key) { App({Key? key, required this.database, required this.settings, required this.user}) : super(key: key) {
if (user.getUsers().length > 0) user.setUser(user.getUsers().first.id); if (user.getUsers().isNotEmpty) user.setUser(user.getUsers().first.id);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
setSystemChrome(context); setSystemChrome(context);
// Set high refresh mode #28
if (Platform.isAndroid) FlutterDisplayMode.setHighRefreshRate();
WidgetsBinding.instance?.addPostFrameCallback((_) { WidgetsBinding.instance?.addPostFrameCallback((_) {
FilcAPI.getConfig().then((Config? config) { FilcAPI.getConfig(settings).then((Config? config) {
settings.update(context, database: database, config: config ?? Config.fromJson({})); if (config != null) settings.update(context, database: database, config: config);
}); });
}); });
@@ -57,6 +63,7 @@ class App extends StatelessWidget {
providers: [ providers: [
ChangeNotifierProvider<SettingsProvider>(create: (_) => settings), ChangeNotifierProvider<SettingsProvider>(create: (_) => settings),
ChangeNotifierProvider<UserProvider>(create: (_) => user), ChangeNotifierProvider<UserProvider>(create: (_) => user),
ChangeNotifierProvider<StatusProvider>(create: (context) => StatusProvider()),
Provider<KretaClient>(create: (context) => KretaClient(context: context, userAgent: settings.config.userAgent)), Provider<KretaClient>(create: (context) => KretaClient(context: context, userAgent: settings.config.userAgent)),
Provider<DatabaseProvider>(create: (context) => database), Provider<DatabaseProvider>(create: (context) => database),
ChangeNotifierProvider<ThemeModeObserver>(create: (context) => ThemeModeObserver(initialTheme: settings.theme)), ChangeNotifierProvider<ThemeModeObserver>(create: (context) => ThemeModeObserver(initialTheme: settings.theme)),
@@ -92,18 +99,18 @@ class App extends StatelessWidget {
theme: AppTheme.lightTheme(context), theme: AppTheme.lightTheme(context),
darkTheme: AppTheme.darkTheme(context), darkTheme: AppTheme.darkTheme(context),
themeMode: themeMode.themeMode, themeMode: themeMode.themeMode,
localizationsDelegates: [ localizationsDelegates: const [
GlobalMaterialLocalizations.delegate, GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate, GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
], ],
supportedLocales: [ supportedLocales: const [
const Locale('en', 'EN'), Locale('en', 'EN'),
const Locale('hu', 'HU'), Locale('hu', 'HU'),
const Locale('de', 'DE'), Locale('de', 'DE'),
], ],
localeListResolutionCallback: (locales, supported) { localeListResolutionCallback: (locales, supported) {
Locale locale = Locale('hu', 'HU'); Locale locale = const Locale('hu', 'HU');
for (var loc in locales ?? []) { for (var loc in locales ?? []) {
if (supported.contains(loc)) { if (supported.contains(loc)) {
@@ -115,7 +122,7 @@ class App extends StatelessWidget {
return locale; return locale;
}, },
onGenerateRoute: (settings) => rootNavigator(settings), onGenerateRoute: (settings) => rootNavigator(settings),
initialRoute: user.getUsers().length > 0 ? "navigation" : "login"); initialRoute: user.getUsers().isNotEmpty ? "navigation" : "login");
}, },
), ),
), ),
@@ -126,15 +133,15 @@ class App extends StatelessWidget {
// if platform == android || platform == ios // if platform == android || platform == ios
switch (route.name) { switch (route.name) {
case "login_back": case "login_back":
return CupertinoPageRoute(builder: (context) => LoginScreen(back: true)); return CupertinoPageRoute(builder: (context) => const LoginScreen(back: true));
case "login": case "login":
return _rootRoute(LoginScreen()); return _rootRoute(const LoginScreen());
case "navigation": case "navigation":
return _rootRoute(Navigation()); return _rootRoute(const NavigationScreen());
case "login_to_navigation": case "login_to_navigation":
return loginRoute(Navigation()); return loginRoute(const NavigationScreen());
case "settings": case "settings":
return settingsRoute(SettingsScreen()); return settingsRoute(const SettingsScreen());
} }
// else if platform == windows || ... // else if platform == windows || ...
} }

View File

@@ -1,10 +1,21 @@
// ignore_for_file: avoid_print
import 'dart:io';
import 'package:filcnaplo/database/struct.dart'; import 'package:filcnaplo/database/struct.dart';
import 'package:filcnaplo/models/settings.dart'; import 'package:filcnaplo/models/settings.dart';
import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sqflite.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
Future<Database> initDB() async { Future<Database> initDB() async {
// await deleteDatabase('app.db'); // for debugging Database db;
var db = await openDatabase('app.db');
if (Platform.isLinux || Platform.isWindows) {
sqfliteFfiInit();
db = await databaseFactoryFfi.openDatabase("app.db");
} else {
db = await openDatabase("app.db");
}
var settingsDB = await createSettingsTable(db); var settingsDB = await createSettingsTable(db);
@@ -32,6 +43,7 @@ Future<DatabaseStruct> createSettingsTable(Database db) async {
"grade_color1": int, "grade_color2": int, "grade_color3": int, "grade_color4": int, "grade_color5": int, // grade colors "grade_color1": int, "grade_color2": int, "grade_color3": int, "grade_color4": int, "grade_color5": int, // grade colors
"vibration_strength": int, "ab_weeks": int, "swap_ab_weeks": int, "vibration_strength": int, "ab_weeks": int, "swap_ab_weeks": int,
"notifications": int, "notifications_bitfield": int, "notification_poll_interval": int, // notifications "notifications": int, "notifications_bitfield": int, "notification_poll_interval": int, // notifications
"x_filc_id": String,
}); });
// Create table Settings // Create table Settings
@@ -42,7 +54,7 @@ Future<DatabaseStruct> createSettingsTable(Database db) async {
Future<DatabaseStruct> createUsersTable(Database db) async { Future<DatabaseStruct> createUsersTable(Database db) async {
var usersDB = DatabaseStruct( var usersDB = DatabaseStruct(
{"id": String, "name": String, "username": String, "password": String, "institute_code": String, "student": String, "role": String}); {"id": String, "name": String, "username": String, "password": String, "institute_code": String, "student": String, "role": int});
// Create table Users // Create table Users
await db.execute("CREATE TABLE IF NOT EXISTS users ($usersDB)"); await db.execute("CREATE TABLE IF NOT EXISTS users ($usersDB)");
@@ -57,14 +69,16 @@ Future<void> migrateDB(
Map<String, Object?> defaultValues, Map<String, Object?> defaultValues,
Future<DatabaseStruct> Function(Database) create, Future<DatabaseStruct> Function(Database) create,
) async { ) async {
var originalRows = (await db.query(table)); var originalRows = await db.query(table);
if (originalRows.length == 0) { if (originalRows.isEmpty) {
await db.execute("drop table $table"); await db.execute("drop table $table");
await create(db); await create(db);
return; return;
} }
List<Map<String, dynamic>> migrated = [];
await Future.forEach<Map<String, Object?>>(originalRows, (original) async { await Future.forEach<Map<String, Object?>>(originalRows, (original) async {
bool migrationRequired = keys.any((key) => !original.containsKey(key) || original[key] == null); bool migrationRequired = keys.any((key) => !original.containsKey(key) || original[key] == null);
@@ -72,11 +86,8 @@ Future<void> migrateDB(
print("INFO: Migrating $table"); print("INFO: Migrating $table");
var copy = Map<String, dynamic>.from(original); var copy = Map<String, dynamic>.from(original);
// Delete table
await db.execute("drop table $table");
// Fill missing columns // Fill missing columns
keys.forEach((key) { for (var key in keys) {
if (!keys.contains(key)) { if (!keys.contains(key)) {
print("DEBUG: dropping $key"); print("DEBUG: dropping $key");
copy.remove(key); copy.remove(key);
@@ -86,13 +97,22 @@ Future<void> migrateDB(
print("DEBUG: migrating $key"); print("DEBUG: migrating $key");
copy[key] = defaultValues[key]; copy[key] = defaultValues[key];
} }
}); }
// Recreate table migrated.add(copy);
await create(db);
await db.insert(table, copy);
print("INFO: Database migrated");
} }
}); });
if (migrated.isNotEmpty) {
// Delete table
await db.execute("drop table $table");
// Recreate table
await create(db);
await Future.forEach(migrated, (Map<String, dynamic> copy) async {
await db.insert(table, copy);
});
print("INFO: Database migrated");
}
} }

View File

@@ -1,6 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:filcnaplo/models/user.dart'; import 'package:filcnaplo/models/user.dart';
import 'package:sqflite/sqflite.dart'; import 'package:sqflite_common/sqlite_api.dart';
// Models // Models
import 'package:filcnaplo/models/settings.dart'; import 'package:filcnaplo/models/settings.dart';
@@ -28,9 +28,9 @@ class DatabaseQuery {
Future<UserProvider> getUsers() async { Future<UserProvider> getUsers() async {
var userProvider = UserProvider(); var userProvider = UserProvider();
List<Map> usersMap = await db.query("users"); List<Map> usersMap = await db.query("users");
usersMap.forEach((user) { for (var user in usersMap) {
userProvider.addUser(User.fromMap(user)); userProvider.addUser(User.fromMap(user));
}); }
return userProvider; return userProvider;
} }
} }
@@ -42,7 +42,7 @@ class UserDatabaseQuery {
Future<List<Grade>> getGrades({required String userId}) async { Future<List<Grade>> getGrades({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]); List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.length == 0) return []; if (userData.isEmpty) return [];
String? gradesJson = userData.elementAt(0)["grades"] as String?; String? gradesJson = userData.elementAt(0)["grades"] as String?;
if (gradesJson == null) return []; if (gradesJson == null) return [];
List<Grade> grades = (jsonDecode(gradesJson) as List).map((e) => Grade.fromJson(e)).toList(); List<Grade> grades = (jsonDecode(gradesJson) as List).map((e) => Grade.fromJson(e)).toList();
@@ -51,7 +51,7 @@ class UserDatabaseQuery {
Future<List<Lesson>> getLessons({required String userId}) async { Future<List<Lesson>> getLessons({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]); List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.length == 0) return []; if (userData.isEmpty) return [];
String? lessonsJson = userData.elementAt(0)["timetable"] as String?; String? lessonsJson = userData.elementAt(0)["timetable"] as String?;
if (lessonsJson == null) return []; if (lessonsJson == null) return [];
List<Lesson> lessons = (jsonDecode(lessonsJson) as List).map((e) => Lesson.fromJson(e)).toList(); List<Lesson> lessons = (jsonDecode(lessonsJson) as List).map((e) => Lesson.fromJson(e)).toList();
@@ -60,7 +60,7 @@ class UserDatabaseQuery {
Future<List<Exam>> getExams({required String userId}) async { Future<List<Exam>> getExams({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]); List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.length == 0) return []; if (userData.isEmpty) return [];
String? examsJson = userData.elementAt(0)["exams"] as String?; String? examsJson = userData.elementAt(0)["exams"] as String?;
if (examsJson == null) return []; if (examsJson == null) return [];
List<Exam> exams = (jsonDecode(examsJson) as List).map((e) => Exam.fromJson(e)).toList(); List<Exam> exams = (jsonDecode(examsJson) as List).map((e) => Exam.fromJson(e)).toList();
@@ -69,7 +69,7 @@ class UserDatabaseQuery {
Future<List<Homework>> getHomework({required String userId}) async { Future<List<Homework>> getHomework({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]); List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.length == 0) return []; if (userData.isEmpty) return [];
String? homeworkJson = userData.elementAt(0)["homework"] as String?; String? homeworkJson = userData.elementAt(0)["homework"] as String?;
if (homeworkJson == null) return []; if (homeworkJson == null) return [];
List<Homework> homework = (jsonDecode(homeworkJson) as List).map((e) => Homework.fromJson(e)).toList(); List<Homework> homework = (jsonDecode(homeworkJson) as List).map((e) => Homework.fromJson(e)).toList();
@@ -78,7 +78,7 @@ class UserDatabaseQuery {
Future<List<Message>> getMessages({required String userId}) async { Future<List<Message>> getMessages({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]); List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.length == 0) return []; if (userData.isEmpty) return [];
String? messagesJson = userData.elementAt(0)["messages"] as String?; String? messagesJson = userData.elementAt(0)["messages"] as String?;
if (messagesJson == null) return []; if (messagesJson == null) return [];
List<Message> messages = (jsonDecode(messagesJson) as List).map((e) => Message.fromJson(e)).toList(); List<Message> messages = (jsonDecode(messagesJson) as List).map((e) => Message.fromJson(e)).toList();
@@ -87,7 +87,7 @@ class UserDatabaseQuery {
Future<List<Note>> getNotes({required String userId}) async { Future<List<Note>> getNotes({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]); List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.length == 0) return []; if (userData.isEmpty) return [];
String? notesJson = userData.elementAt(0)["notes"] as String?; String? notesJson = userData.elementAt(0)["notes"] as String?;
if (notesJson == null) return []; if (notesJson == null) return [];
List<Note> notes = (jsonDecode(notesJson) as List).map((e) => Note.fromJson(e)).toList(); List<Note> notes = (jsonDecode(notesJson) as List).map((e) => Note.fromJson(e)).toList();
@@ -96,7 +96,7 @@ class UserDatabaseQuery {
Future<List<Event>> getEvents({required String userId}) async { Future<List<Event>> getEvents({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]); List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.length == 0) return []; if (userData.isEmpty) return [];
String? eventsJson = userData.elementAt(0)["events"] as String?; String? eventsJson = userData.elementAt(0)["events"] as String?;
if (eventsJson == null) return []; if (eventsJson == null) return [];
List<Event> events = (jsonDecode(eventsJson) as List).map((e) => Event.fromJson(e)).toList(); List<Event> events = (jsonDecode(eventsJson) as List).map((e) => Event.fromJson(e)).toList();
@@ -105,7 +105,7 @@ class UserDatabaseQuery {
Future<List<Absence>> getAbsences({required String userId}) async { Future<List<Absence>> getAbsences({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]); List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.length == 0) return []; if (userData.isEmpty) return [];
String? absebcesJson = userData.elementAt(0)["absences"] as String?; String? absebcesJson = userData.elementAt(0)["absences"] as String?;
if (absebcesJson == null) return []; if (absebcesJson == null) return [];
List<Absence> absebces = (jsonDecode(absebcesJson) as List).map((e) => Absence.fromJson(e)).toList(); List<Absence> absebces = (jsonDecode(absebcesJson) as List).map((e) => Absence.fromJson(e)).toList();

View File

@@ -1,5 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'package:sqflite/sqflite.dart'; import 'package:sqflite_common/sqlite_api.dart';
// Models // Models
import 'package:filcnaplo/models/settings.dart'; import 'package:filcnaplo/models/settings.dart';
@@ -24,7 +24,7 @@ class DatabaseStore {
Future<void> storeUser(User user) async { Future<void> storeUser(User user) async {
List userRes = await db.query("users", where: "id = ?", whereArgs: [user.id]); List userRes = await db.query("users", where: "id = ?", whereArgs: [user.id]);
if (userRes.length > 0) { if (userRes.isNotEmpty) {
await db.update("users", user.toMap(), where: "id = ?", whereArgs: [user.id]); await db.update("users", user.toMap(), where: "id = ?", whereArgs: [user.id]);
} else { } else {
await db.insert("users", user.toMap()); await db.insert("users", user.toMap());

View File

@@ -15,7 +15,7 @@ class DatabaseStruct {
break; break;
} }
return "${name} ${typeName.toUpperCase()} ${name == 'id' ? 'NOT NULL' : ''}"; return "$name ${typeName.toUpperCase()} ${name == 'id' ? 'NOT NULL' : ''}";
} }
@override @override

View File

@@ -6,14 +6,15 @@ class AverageHelper {
List<String> ignoreInFinal = ["5,SzorgalomErtek", "4,MagatartasErtek"]; List<String> ignoreInFinal = ["5,SzorgalomErtek", "4,MagatartasErtek"];
if (finalAvg) if (finalAvg) {
grades.removeWhere((e) => grades.removeWhere((e) =>
(e.value.value == 0) || (e.value.value == 0) ||
(ignoreInFinal.contains(e.gradeType?.id))); (ignoreInFinal.contains(e.gradeType?.id)));
}
grades.forEach((e) { for (var e in grades) {
average += e.value.value * ((finalAvg ? 100 : e.value.weight) / 100); average += e.value.value * ((finalAvg ? 100 : e.value.weight) / 100);
}); }
average = average / average = average /
grades grades

View File

@@ -1,3 +1,5 @@
// ignore_for_file: avoid_print
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';

View File

@@ -7,7 +7,7 @@ class SubjectIcon {
static IconData? lookup({Subject? subject, String? subjectName}) { static IconData? lookup({Subject? subject, String? subjectName}) {
assert(!(subject == null && subjectName == null)); assert(!(subject == null && subjectName == null));
String name = subject?.name.toLowerCase().specialChars().trim() ?? subjectName ?? ""; String name = (subject?.name ?? subjectName ?? "").toLowerCase().specialChars().trim();
String category = subject?.category.description.toLowerCase().specialChars() ?? ""; String category = subject?.category.description.toLowerCase().specialChars() ?? "";
// todo: check for categories // todo: check for categories
@@ -16,7 +16,7 @@ class SubjectIcon {
if (RegExp("irodalom").hasMatch(name)) return Icons.menu_book_outlined; if (RegExp("irodalom").hasMatch(name)) return Icons.menu_book_outlined;
if (RegExp("tor(i|tenelem)").hasMatch(name)) return Icons.hourglass_empty_outlined; if (RegExp("tor(i|tenelem)").hasMatch(name)) return Icons.hourglass_empty_outlined;
if (RegExp("foldrajz").hasMatch(name)) return Icons.public_outlined; if (RegExp("foldrajz").hasMatch(name)) return Icons.public_outlined;
if (RegExp("rajz|muvtori|muveszet|kultura").hasMatch(name)) return Icons.palette_outlined; if (RegExp("rajz|muvtori|muveszet|vizualis").hasMatch(name)) return Icons.palette_outlined;
if (RegExp("fizika").hasMatch(name)) return Icons.emoji_objects_outlined; if (RegExp("fizika").hasMatch(name)) return Icons.emoji_objects_outlined;
if (RegExp("^enek|zene|szolfezs|zongora|korus").hasMatch(name)) return Icons.music_note_outlined; if (RegExp("^enek|zene|szolfezs|zongora|korus").hasMatch(name)) return Icons.music_note_outlined;
if (RegExp("^tes(i|tneveles)|sport").hasMatch(name)) return Icons.sports_soccer_outlined; if (RegExp("^tes(i|tneveles)|sport").hasMatch(name)) return Icons.sports_soccer_outlined;
@@ -25,7 +25,7 @@ class SubjectIcon {
if (RegExp("kornyezet|termeszet(tudomany|ismeret)|hon( es nep)?ismeret").hasMatch(name)) return Icons.eco_outlined; if (RegExp("kornyezet|termeszet(tudomany|ismeret)|hon( es nep)?ismeret").hasMatch(name)) return Icons.eco_outlined;
if (RegExp("(hit|erkolcs)tan|vallas|etika").hasMatch(name)) return Icons.favorite_border_outlined; if (RegExp("(hit|erkolcs)tan|vallas|etika").hasMatch(name)) return Icons.favorite_border_outlined;
if (RegExp("penzugy").hasMatch(name)) return Icons.savings_outlined; if (RegExp("penzugy").hasMatch(name)) return Icons.savings_outlined;
if (RegExp("informatika|szoftver|iroda").hasMatch(name)) return Icons.computer_outlined; if (RegExp("informatika|szoftver|iroda|digitalis").hasMatch(name)) return Icons.computer_outlined;
if (RegExp("prog").hasMatch(name)) return Icons.code_outlined; if (RegExp("prog").hasMatch(name)) return Icons.code_outlined;
if (RegExp("halozat").hasMatch(name)) return Icons.wifi_tethering_outlined; if (RegExp("halozat").hasMatch(name)) return Icons.wifi_tethering_outlined;
if (RegExp("szinhaz").hasMatch(name)) return Icons.theater_comedy_outlined; if (RegExp("szinhaz").hasMatch(name)) return Icons.theater_comedy_outlined;

View File

@@ -7,33 +7,36 @@ import 'package:filcnaplo/helpers/storage_helper.dart';
import 'package:filcnaplo/models/release.dart'; import 'package:filcnaplo/models/release.dart';
import 'package:open_file/open_file.dart'; import 'package:open_file/open_file.dart';
enum UpdateState { prepare, downloading, installing } enum UpdateState { none, preparing, downloading, installing }
typedef UpdateCallback = Function(double progress, UpdateState state); typedef UpdateCallback = Function(double progress, UpdateState state);
// TODO: cleanup old apk files // TODO: cleanup old apk files
extension UpdateHelper on Release { extension UpdateHelper on Release {
Future<void> install({UpdateCallback? updateCallback}) async { Future<void> install({UpdateCallback? updateCallback}) async {
updateCallback!(-1, UpdateState.preparing);
String downloads = await StorageHelper.downloadsPath(); String downloads = await StorageHelper.downloadsPath();
File apk = File("$downloads/filcnaplo-${version}.apk"); File apk = File("$downloads/filcnaplo-$version.apk");
if (!await apk.exists()) { if (!await apk.exists()) {
updateCallback!(-1, UpdateState.downloading); updateCallback(-1, UpdateState.downloading);
var bytes = await download(updateCallback: updateCallback); var bytes = await download(updateCallback: updateCallback);
if (!await StorageHelper.write(apk.path, bytes)) throw "failed to write apk: permission denied"; if (!await StorageHelper.write(apk.path, bytes)) throw "failed to write apk: permission denied";
} }
updateCallback!(-1, UpdateState.installing); updateCallback(-1, UpdateState.installing);
var result = await OpenFile.open(apk.path); var result = await OpenFile.open(apk.path);
if (result.type != ResultType.done) { if (result.type != ResultType.done) {
// ignore: avoid_print
print("ERROR: installUpdate.openFile: " + result.message); print("ERROR: installUpdate.openFile: " + result.message);
throw result.message; throw result.message;
} }
updateCallback(-1, UpdateState.prepare); updateCallback(-1, UpdateState.none);
} }
Future<Uint8List> download({UpdateCallback? updateCallback}) async { Future<Uint8List> download({UpdateCallback? updateCallback}) async {

View File

@@ -1,8 +1,8 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class FilcIcons { class FilcIcons {
static const IconData home = const FilcIconData(0x41); static const IconData home = FilcIconData(0x41);
static const IconData linux = const FilcIconData(0x42); static const IconData linux = FilcIconData(0x42);
} }
class FilcIconData extends IconData { class FilcIconData extends IconData {

View File

@@ -2,10 +2,12 @@ import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo/api/providers/database_provider.dart'; import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/database/init.dart'; import 'package:filcnaplo/database/init.dart';
import 'package:filcnaplo/models/settings.dart'; import 'package:filcnaplo/models/settings.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:filcnaplo/app.dart'; import 'package:filcnaplo/app.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:filcnaplo_mobile_ui/screens/error_screen.dart'; import 'package:filcnaplo_mobile_ui/screens/error_screen.dart';
import 'package:filcnaplo_mobile_ui/screens/error_report_screen.dart';
void main() async { void main() async {
// Initalize // Initalize
@@ -39,12 +41,25 @@ class Startup {
} }
} }
bool errorShown = false;
String lastException = '';
Widget errorBuilder(FlutterErrorDetails details) { Widget errorBuilder(FlutterErrorDetails details) {
return Builder(builder: (context) { return Builder(builder: (context) {
if (Navigator.of(context).canPop()) Navigator.pop(context); if (Navigator.of(context).canPop()) Navigator.pop(context);
WidgetsBinding.instance?.addPostFrameCallback((_) { WidgetsBinding.instance?.addPostFrameCallback((_) {
Navigator.of(context, rootNavigator: true).push(MaterialPageRoute(builder: (ctx) => ErrorScreen(details))); if (!errorShown && details.exceptionAsString() != lastException) {
errorShown = true;
lastException = details.exceptionAsString();
Navigator.of(context, rootNavigator: true).push(MaterialPageRoute(builder: (context) {
if (kReleaseMode) {
return ErrorReportScreen(details);
} else {
return ErrorScreen(details);
}
})).then((_) => errorShown = false);
}
}); });
return Container(); return Container();

View File

@@ -58,10 +58,11 @@ class Version {
// check for valid prerelease name // check for valid prerelease name
if (p[0] != "") { if (p[0] != "") {
if (prereleases.contains(p[0].toLowerCase().trim())) if (prereleases.contains(p[0].toLowerCase().trim())) {
pre = p[0]; pre = p[0];
else } else {
throw "invalid prerelease name: ${p[0]}"; throw "invalid prerelease name: ${p[0]}";
}
} }
// core // core
@@ -74,6 +75,7 @@ class Version {
return Version(x, y, z, prerelease: pre, prever: prev); return Version(x, y, z, prerelease: pre, prever: prev);
} catch (error) { } catch (error) {
// ignore: avoid_print
print("WARNING: Failed to parse version ($o): $error"); print("WARNING: Failed to parse version ($o): $error");
return Version.zero; return Version.zero;
} }
@@ -126,4 +128,7 @@ class Version {
static const zero = Version(0, 0, 0); static const zero = Version(0, 0, 0);
static const List<String> prereleases = ["dev", "pre", "alpha", "beta", "rc"]; static const List<String> prereleases = ["dev", "pre", "alpha", "beta", "rc"];
@override
int get hashCode => toString().hashCode;
} }

View File

@@ -6,6 +6,7 @@ import 'package:filcnaplo/theme.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:uuid/uuid.dart';
enum Pages { home, grades, timetable, messages, absences } enum Pages { home, grades, timetable, messages, absences }
enum UpdateChannel { stable, beta, dev } enum UpdateChannel { stable, beta, dev }
@@ -43,10 +44,11 @@ class SettingsProvider extends ChangeNotifier {
int _notificationPollInterval; int _notificationPollInterval;
bool _developerMode; bool _developerMode;
VibrationStrength _vibrate; VibrationStrength _vibrate;
bool _ABweeks; bool _abWeeks;
bool _swapABweeks; bool _swapABweeks;
UpdateChannel _updateChannel; UpdateChannel _updateChannel;
Config _config; Config _config;
String _xFilcId;
SettingsProvider({ SettingsProvider({
required String language, required String language,
@@ -62,10 +64,11 @@ class SettingsProvider extends ChangeNotifier {
required bool developerMode, required bool developerMode,
required int notificationPollInterval, required int notificationPollInterval,
required VibrationStrength vibrate, required VibrationStrength vibrate,
required bool ABweeks, required bool abWeeks,
required bool swapABweeks, required bool swapABweeks,
required UpdateChannel updateChannel, required UpdateChannel updateChannel,
required Config config, required Config config,
required String xFilcId,
}) : _language = language, }) : _language = language,
_startPage = startPage, _startPage = startPage,
_rounding = rounding, _rounding = rounding,
@@ -79,10 +82,11 @@ class SettingsProvider extends ChangeNotifier {
_developerMode = developerMode, _developerMode = developerMode,
_notificationPollInterval = notificationPollInterval, _notificationPollInterval = notificationPollInterval,
_vibrate = vibrate, _vibrate = vibrate,
_ABweeks = ABweeks, _abWeeks = abWeeks,
_swapABweeks = swapABweeks, _swapABweeks = swapABweeks,
_updateChannel = updateChannel, _updateChannel = updateChannel,
_config = config { _config = config,
_xFilcId = xFilcId {
PackageInfo.fromPlatform().then((PackageInfo packageInfo) { PackageInfo.fromPlatform().then((PackageInfo packageInfo) {
_packageInfo = packageInfo; _packageInfo = packageInfo;
}); });
@@ -109,10 +113,11 @@ class SettingsProvider extends ChangeNotifier {
notificationPollInterval: map["notification_poll_interval"], notificationPollInterval: map["notification_poll_interval"],
developerMode: map["developer_mode"] == 1 ? true : false, developerMode: map["developer_mode"] == 1 ? true : false,
vibrate: VibrationStrength.values[map["vibration_strength"]], vibrate: VibrationStrength.values[map["vibration_strength"]],
ABweeks: map["ab_weeks"] == 1 ? true : false, abWeeks: map["ab_weeks"] == 1 ? true : false,
swapABweeks: map["swap_ab_weeks"] == 1 ? true : false, swapABweeks: map["swap_ab_weeks"] == 1 ? true : false,
updateChannel: UpdateChannel.values[map["update_channel"]], updateChannel: UpdateChannel.values[map["update_channel"]],
config: Config.fromJson(jsonDecode(map["config"] ?? "{}")), config: Config.fromJson(jsonDecode(map["config"] ?? "{}")),
xFilcId: map["x_filc_id"],
); );
} }
@@ -135,10 +140,11 @@ class SettingsProvider extends ChangeNotifier {
"grade_color5": _gradeColors[4].value, "grade_color5": _gradeColors[4].value,
"update_channel": _updateChannel.index, "update_channel": _updateChannel.index,
"vibration_strength": _vibrate.index, "vibration_strength": _vibrate.index,
"ab_weeks": _ABweeks ? 1 : 0, "ab_weeks": _abWeeks ? 1 : 0,
"swap_ab_weeks": _swapABweeks ? 1 : 0, "swap_ab_weeks": _swapABweeks ? 1 : 0,
"notification_poll_interval": _notificationPollInterval, "notification_poll_interval": _notificationPollInterval,
"config": jsonEncode(config.json), "config": jsonEncode(config.json),
"x_filc_id": _xFilcId,
}; };
} }
@@ -163,10 +169,11 @@ class SettingsProvider extends ChangeNotifier {
developerMode: false, developerMode: false,
notificationPollInterval: 1, notificationPollInterval: 1,
vibrate: VibrationStrength.medium, vibrate: VibrationStrength.medium,
ABweeks: false, abWeeks: false,
swapABweeks: false, swapABweeks: false,
updateChannel: UpdateChannel.stable, updateChannel: UpdateChannel.stable,
config: Config.fromJson({}), config: Config.fromJson({}),
xFilcId: const Uuid().v4(),
); );
} }
@@ -184,11 +191,12 @@ class SettingsProvider extends ChangeNotifier {
bool get developerMode => _developerMode; bool get developerMode => _developerMode;
int get notificationPollInterval => _notificationPollInterval; int get notificationPollInterval => _notificationPollInterval;
VibrationStrength get vibrate => _vibrate; VibrationStrength get vibrate => _vibrate;
bool get ABweeks => _ABweeks; bool get abWeeks => _abWeeks;
bool get swapABweeks => _swapABweeks; bool get swapABweeks => _swapABweeks;
UpdateChannel get updateChannel => _updateChannel; UpdateChannel get updateChannel => _updateChannel;
PackageInfo? get packageInfo => _packageInfo; PackageInfo? get packageInfo => _packageInfo;
Config get config => _config; Config get config => _config;
String get xFilcId => _xFilcId;
Future<void> update( Future<void> update(
BuildContext context, { BuildContext context, {
@@ -206,10 +214,11 @@ class SettingsProvider extends ChangeNotifier {
bool? developerMode, bool? developerMode,
int? notificationPollInterval, int? notificationPollInterval,
VibrationStrength? vibrate, VibrationStrength? vibrate,
bool? ABweeks, bool? abWeeks,
bool? swapABweeks, bool? swapABweeks,
UpdateChannel? updateChannel, UpdateChannel? updateChannel,
Config? config, Config? config,
String? xFilcId,
}) async { }) async {
if (language != null && language != _language) _language = language; if (language != null && language != _language) _language = language;
if (startPage != null && startPage != _startPage) _startPage = startPage; if (startPage != null && startPage != _startPage) _startPage = startPage;
@@ -222,15 +231,17 @@ class SettingsProvider extends ChangeNotifier {
if (notificationsEnabled != null && notificationsEnabled != _notificationsEnabled) _notificationsEnabled = notificationsEnabled; if (notificationsEnabled != null && notificationsEnabled != _notificationsEnabled) _notificationsEnabled = notificationsEnabled;
if (notificationsBitfield != null && notificationsBitfield != _notificationsBitfield) _notificationsBitfield = notificationsBitfield; if (notificationsBitfield != null && notificationsBitfield != _notificationsBitfield) _notificationsBitfield = notificationsBitfield;
if (developerMode != null && developerMode != _developerMode) _developerMode = developerMode; if (developerMode != null && developerMode != _developerMode) _developerMode = developerMode;
if (notificationPollInterval != null && notificationPollInterval != _notificationPollInterval) if (notificationPollInterval != null && notificationPollInterval != _notificationPollInterval) {
_notificationPollInterval = notificationPollInterval; _notificationPollInterval = notificationPollInterval;
}
if (vibrate != null && vibrate != _vibrate) _vibrate = vibrate; if (vibrate != null && vibrate != _vibrate) _vibrate = vibrate;
if (ABweeks != null && ABweeks != _ABweeks) _ABweeks = ABweeks; if (abWeeks != null && abWeeks != _abWeeks) _abWeeks = abWeeks;
if (swapABweeks != null && swapABweeks != _swapABweeks) _swapABweeks = swapABweeks; if (swapABweeks != null && swapABweeks != _swapABweeks) _swapABweeks = swapABweeks;
if (updateChannel != null && updateChannel != _updateChannel) _updateChannel = updateChannel; if (updateChannel != null && updateChannel != _updateChannel) _updateChannel = updateChannel;
if (config != null && config != _config) _config = config; if (config != null && config != _config) _config = config;
if (xFilcId != null && xFilcId != _xFilcId) _xFilcId = xFilcId;
if (database == null) database = Provider.of<DatabaseProvider>(context, listen: false); database ??= Provider.of<DatabaseProvider>(context, listen: false);
await database.store.storeSettings(this); await database.store.storeSettings(this);
notifyListeners(); notifyListeners();
} }

View File

@@ -26,7 +26,7 @@ class User {
if (id != null) { if (id != null) {
this.id = id; this.id = id;
} else { } else {
this.id = Uuid().v4(); this.id = const Uuid().v4();
} }
} }
@@ -67,7 +67,7 @@ class User {
"password": password, "password": password,
"institute_code": instituteCode, "institute_code": instituteCode,
"grant_type": "password", "grant_type": "password",
"client_id": KretaAPI.CLIENT_ID, "client_id": KretaAPI.clientId,
}; };
} }
} }

View File

@@ -11,14 +11,14 @@ class AppTheme {
// Light Theme // Light Theme
static ThemeData lightTheme(BuildContext context) { static ThemeData lightTheme(BuildContext context) {
var lightColors = LightAppColors(); var lightColors = LightAppColors();
Color accent = accentColorMap[Provider.of<SettingsProvider>(context, listen: false).accentColor] ?? Color(0); Color accent = accentColorMap[Provider.of<SettingsProvider>(context, listen: false).accentColor] ?? const Color(0x00000000);
return ThemeData( return ThemeData(
brightness: Brightness.light, brightness: Brightness.light,
fontFamily: _fontFamily, fontFamily: _fontFamily,
scaffoldBackgroundColor: lightColors.background, scaffoldBackgroundColor: lightColors.background,
backgroundColor: lightColors.highlight, backgroundColor: lightColors.highlight,
primaryColor: lightColors.filc, primaryColor: lightColors.filc,
dividerColor: Color(0), dividerColor: const Color(0x00000000),
colorScheme: ColorScheme.fromSwatch( colorScheme: ColorScheme.fromSwatch(
accentColor: accent, accentColor: accent,
backgroundColor: lightColors.background, backgroundColor: lightColors.background,
@@ -37,14 +37,14 @@ class AppTheme {
// Dark Theme // Dark Theme
static ThemeData darkTheme(BuildContext context) { static ThemeData darkTheme(BuildContext context) {
var darkColors = DarkAppColors(); var darkColors = DarkAppColors();
Color accent = accentColorMap[Provider.of<SettingsProvider>(context, listen: false).accentColor] ?? Color(0); Color accent = accentColorMap[Provider.of<SettingsProvider>(context, listen: false).accentColor] ?? const Color(0x00000000);
return ThemeData( return ThemeData(
brightness: Brightness.dark, brightness: Brightness.dark,
fontFamily: _fontFamily, fontFamily: _fontFamily,
scaffoldBackgroundColor: darkColors.background, scaffoldBackgroundColor: darkColors.background,
backgroundColor: darkColors.highlight, backgroundColor: darkColors.highlight,
primaryColor: darkColors.filc, primaryColor: darkColors.filc,
dividerColor: Color(0), dividerColor: const Color(0x00000000),
colorScheme: ColorScheme.fromSwatch( colorScheme: ColorScheme.fromSwatch(
accentColor: accent, accentColor: accent,
backgroundColor: darkColors.background, backgroundColor: darkColors.background,
@@ -70,7 +70,7 @@ class AppColors {
enum AccentColor { filc, blue, green, lime, yellow, orange, red, pink, purple } enum AccentColor { filc, blue, green, lime, yellow, orange, red, pink, purple }
Map<AccentColor, Color> accentColorMap = { Map<AccentColor, Color> accentColorMap = {
AccentColor.filc: Color(0xff20AC9B), AccentColor.filc: const Color(0xff20AC9B),
AccentColor.blue: Colors.blue.shade300, AccentColor.blue: Colors.blue.shade300,
AccentColor.green: Colors.green.shade300, AccentColor.green: Colors.green.shade300,
AccentColor.lime: Colors.lime.shade300, AccentColor.lime: Colors.lime.shade300,
@@ -82,54 +82,82 @@ Map<AccentColor, Color> accentColorMap = {
}; };
abstract class ThemeAppColors { abstract class ThemeAppColors {
final Color shadow = Color(0); final Color shadow = const Color(0x00000000);
final Color text = Color(0); final Color text = const Color(0x00000000);
final Color background = Color(0); final Color background = const Color(0x00000000);
final Color highlight = Color(0); final Color highlight = const Color(0x00000000);
final Color red = Color(0); final Color red = const Color(0x00000000);
final Color orange = Color(0); final Color orange = const Color(0x00000000);
final Color yellow = Color(0); final Color yellow = const Color(0x00000000);
final Color green = Color(0); final Color green = const Color(0x00000000);
final Color filc = Color(0); final Color filc = const Color(0x00000000);
final Color teal = Color(0); final Color teal = const Color(0x00000000);
final Color blue = Color(0); final Color blue = const Color(0x00000000);
final Color indigo = Color(0); final Color indigo = const Color(0x00000000);
final Color purple = Color(0); final Color purple = const Color(0x00000000);
final Color pink = Color(0); final Color pink = const Color(0x00000000);
} }
class LightAppColors implements ThemeAppColors { class LightAppColors implements ThemeAppColors {
final shadow = Color(0xffE8E8E8); @override
final shadow = const Color(0xffE8E8E8);
@override
final text = Colors.black; final text = Colors.black;
final background = Color(0xffF4F9FF); @override
final highlight = Color(0xffFFFFFF); final background = const Color(0xffF4F9FF);
final red = Color(0xffFF3B30); @override
final orange = Color(0xffFF9500); final highlight = const Color(0xffFFFFFF);
final yellow = Color(0xffFFCC00); @override
final green = Color(0xff34C759); final red = const Color(0xffFF3B30);
final filc = Color(0xff247665); @override
final teal = Color(0xff5AC8FA); final orange = const Color(0xffFF9500);
final blue = Color(0xff007AFF); @override
final indigo = Color(0xff5856D6); final yellow = const Color(0xffFFCC00);
final purple = Color(0xffAF52DE); @override
final pink = Color(0xffFF2D55); final green = const Color(0xff34C759);
@override
final filc = const Color(0xff247665);
@override
final teal = const Color(0xff5AC8FA);
@override
final blue = const Color(0xff007AFF);
@override
final indigo = const Color(0xff5856D6);
@override
final purple = const Color(0xffAF52DE);
@override
final pink = const Color(0xffFF2D55);
} }
class DarkAppColors implements ThemeAppColors { class DarkAppColors implements ThemeAppColors {
final shadow = Color(0); @override
final shadow = const Color(0x00000000);
@override
final text = Colors.white; final text = Colors.white;
final background = Color(0xff000000); @override
final highlight = Color(0xff141516); final background = const Color(0xff000000);
final red = Color(0xffFF453A); @override
final orange = Color(0xffFF9F0A); final highlight = const Color(0xff141516);
final yellow = Color(0xffFFD60A); @override
final green = Color(0xff32D74B); final red = const Color(0xffFF453A);
final filc = Color(0xff29826F); @override
final teal = Color(0xff64D2FF); final orange = const Color(0xffFF9F0A);
final blue = Color(0xff0A84FF); @override
final indigo = Color(0xff5E5CE6); final yellow = const Color(0xffFFD60A);
final purple = Color(0xffBF5AF2); @override
final pink = Color(0xffFF375F); final green = const Color(0xff32D74B);
@override
final filc = const Color(0xff29826F);
@override
final teal = const Color(0xff64D2FF);
@override
final blue = const Color(0xff0A84FF);
@override
final indigo = const Color(0xff5E5CE6);
@override
final purple = const Color(0xffBF5AF2);
@override
final pink = const Color(0xffFF375F);
} }
class ThemeModeObserver extends ChangeNotifier { class ThemeModeObserver extends ChangeNotifier {

View File

@@ -1,3 +1,4 @@
import 'package:filcnaplo_kreta_api/models/week.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:i18n_extension/i18n_widget.dart'; import 'package:i18n_extension/i18n_widget.dart';
@@ -5,8 +6,7 @@ import 'package:html/parser.dart';
import 'format.i18n.dart'; import 'format.i18n.dart';
extension StringFormatUtils on String { extension StringFormatUtils on String {
String specialChars() => this String specialChars() => replaceAll("é", "e")
.replaceAll("é", "e")
.replaceAll("á", "a") .replaceAll("á", "a")
.replaceAll("ó", "o") .replaceAll("ó", "o")
.replaceAll("ő", "o") .replaceAll("ő", "o")
@@ -16,9 +16,9 @@ extension StringFormatUtils on String {
.replaceAll("ü", "u") .replaceAll("ü", "u")
.replaceAll("í", "i"); .replaceAll("í", "i");
String capital() => this.length > 0 ? this[0].toUpperCase() + this.substring(1) : ""; String capital() => isNotEmpty ? this[0].toUpperCase() + substring(1) : "";
String capitalize() => this.split(" ").map((w) => w.capital()).join(" "); String capitalize() => split(" ").map((w) => w.capital()).join(" ");
String escapeHtml() { String escapeHtml() {
String htmlString = this; String htmlString = this;
@@ -32,25 +32,32 @@ extension StringFormatUtils on String {
} }
extension DateFormatUtils on DateTime { extension DateFormatUtils on DateTime {
String format(BuildContext context, {bool timeOnly = false, bool weekday = false}) { String format(BuildContext context, {bool timeOnly = false, bool forceToday = false, bool weekday = false}) {
// Time only // Time only
if (timeOnly) return DateFormat("HH:mm").format(this); if (timeOnly) return DateFormat("HH:mm").format(this);
DateTime now = DateTime.now(); DateTime now = DateTime.now();
if (now.year == this.year && now.month == this.month && now.day == this.day) { if (now.year == year && now.month == month && now.day == day) {
if (this.hour == 0 && this.minute == 0 && this.second == 0) return "Today".i18n; if (hour == 0 && minute == 0 && second == 0 || forceToday) return "Today".i18n;
return DateFormat("HH:mm").format(this); return DateFormat("HH:mm").format(this);
} }
if (now.year == this.year && now.month == this.month && now.subtract(Duration(days: 1)).day == this.day) return "Yesterday".i18n; if (now.year == year && now.month == month && now.subtract(const Duration(days: 1)).day == day) return "Yesterday".i18n;
if (now.year == year && now.month == month && now.add(const Duration(days: 1)).day == day) return "Tomorrow".i18n;
String formatString; String formatString;
if (this.year == now.year)
formatString = "MMM dd.";
else
formatString = "yy/MM/dd";
if (weekday) formatString += " (EEEE)"; // If date is current week, show only weekday
if (Week.current().start.isBefore(this) && Week.current().end.isAfter(this)) {
formatString = "EEEE";
} else {
if (year == now.year) {
formatString = "MMM dd.";
} else {
formatString = "yy/MM/dd";
} // ex. 21/01/01
return DateFormat(formatString, I18n.of(context).locale.toString()).format(this); if (weekday) formatString += " (EEEE)"; // ex. (monday)
}
return DateFormat(formatString, I18n.of(context).locale.toString()).format(this).capital();
} }
} }

View File

@@ -6,14 +6,17 @@ extension Localization on String {
"en_en": { "en_en": {
"Today": "Today", "Today": "Today",
"Yesterday": "Yesterday", "Yesterday": "Yesterday",
"Tomorrow": "Tomorrow",
}, },
"hu_hu": { "hu_hu": {
"Today": "Ma", "Today": "Ma",
"Yesterday": "Tegnap", "Yesterday": "Tegnap",
"Tomorrow": "Holnap",
}, },
"de_de": { "de_de": {
"Today": "Heute", "Today": "Heute",
"Yesterday": "Gestern", "Yesterday": "Gestern",
"Tomorrow": "Morgen",
} }
}; };

View File

@@ -17,6 +17,7 @@ class JwtUtils {
var payload = utf8.decode(base64Url.decode(parts[1])); var payload = utf8.decode(base64Url.decode(parts[1]));
return jsonDecode(payload); return jsonDecode(payload);
} catch (error) { } catch (error) {
// ignore: avoid_print
print("ERROR: JwtUtils.decodeJwt: $error"); print("ERROR: JwtUtils.decodeJwt: $error");
} }
} }

1
filcnaplo/linux/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
flutter/ephemeral

View File

@@ -0,0 +1,116 @@
cmake_minimum_required(VERSION 3.10)
project(runner LANGUAGES CXX)
set(BINARY_NAME "filcnaplo")
set(APPLICATION_ID "hu.filc.filcnaplo")
cmake_policy(SET CMP0063 NEW)
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Configure build options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Compilation settings that should be applied to most targets.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
# Flutter library and tool build rules.
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
# Application build
add_executable(${BINARY_NAME}
"main.cc"
"my_application.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)
apply_standard_settings(${BINARY_NAME})
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
if(PLUGIN_BUNDLED_LIBRARIES)
install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()

View File

@@ -0,0 +1,8 @@
[Desktop Entry]
Name=Filc Napló
Comment=Nem hivatalos e-napló alkalmazás az eKRÉTA rendszerhez
Exec=filcnaplo
Icon=icon.png
Terminal=false
Type=Application
Categories=Education;

View File

@@ -0,0 +1,87 @@
cmake_minimum_required(VERSION 3.10)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
# which isn't available in 3.10.
function(list_prepend LIST_NAME PREFIX)
set(NEW_LIST "")
foreach(element ${${LIST_NAME}})
list(APPEND NEW_LIST "${PREFIX}${element}")
endforeach(element)
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
endfunction()
# === Flutter Library ===
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"fl_basic_message_channel.h"
"fl_binary_codec.h"
"fl_binary_messenger.h"
"fl_dart_project.h"
"fl_engine.h"
"fl_json_message_codec.h"
"fl_json_method_codec.h"
"fl_message_codec.h"
"fl_method_call.h"
"fl_method_channel.h"
"fl_method_codec.h"
"fl_method_response.h"
"fl_plugin_registrar.h"
"fl_plugin_registry.h"
"fl_standard_message_codec.h"
"fl_standard_method_codec.h"
"fl_string_codec.h"
"fl_value.h"
"fl_view.h"
"flutter_linux.h"
)
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
target_link_libraries(flutter INTERFACE
PkgConfig::GTK
PkgConfig::GLIB
PkgConfig::GIO
)
add_dependencies(flutter flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/_phony_
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
)

View File

@@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
}

View File

@@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View File

@@ -0,0 +1,16 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
url_launcher_linux
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)

BIN
filcnaplo/linux/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

6
filcnaplo/linux/main.cc Normal file
View File

@@ -0,0 +1,6 @@
#include "my_application.h"
int main(int argc, char** argv) {
g_autoptr(MyApplication) app = my_application_new();
return g_application_run(G_APPLICATION(app), argc, argv);
}

View File

@@ -0,0 +1,104 @@
#include "my_application.h"
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#include "flutter/generated_plugin_registrant.h"
struct _MyApplication {
GtkApplication parent_instance;
char** dart_entrypoint_arguments;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// Use a header bar when running in GNOME as this is the common style used
// by applications and is the setup most users will be using (e.g. Ubuntu
// desktop).
// If running on X and not using GNOME then just use a traditional title bar
// in case the window manager does more exotic layout, e.g. tiling.
// If running on Wayland assume the header bar will work (may need changing
// if future cases occur).
gboolean use_header_bar = TRUE;
#ifdef GDK_WINDOWING_X11
GdkScreen* screen = gtk_window_get_screen(window);
if (GDK_IS_X11_SCREEN(screen)) {
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
use_header_bar = FALSE;
}
}
#endif
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "Filc Napló");
gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else {
gtk_window_set_title(window, "Filc Napló");
}
gtk_window_set_default_size(window, 1280, 720);
gtk_widget_show(GTK_WIDGET(window));
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
gtk_widget_grab_focus(GTK_WIDGET(view));
}
// Implements GApplication::local_command_line.
static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
MyApplication* self = MY_APPLICATION(application);
// Strip out the first argument as it is the binary name.
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
g_autoptr(GError) error = nullptr;
if (!g_application_register(application, nullptr, &error)) {
g_warning("Failed to register: %s", error->message);
*exit_status = 1;
return TRUE;
}
g_application_activate(application);
*exit_status = 0;
return TRUE;
}
// Implements GObject::dispose.
static void my_application_dispose(GObject* object) {
MyApplication* self = MY_APPLICATION(object);
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
}
static void my_application_class_init(MyApplicationClass* klass) {
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
}
static void my_application_init(MyApplication* self) {}
MyApplication* my_application_new() {
return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID,
"flags", G_APPLICATION_NON_UNIQUE,
nullptr));
}

View File

@@ -0,0 +1,18 @@
#ifndef FLUTTER_MY_APPLICATION_H_
#define FLUTTER_MY_APPLICATION_H_
#include <gtk/gtk.h>
G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,
GtkApplication)
/**
* my_application_new:
*
* Creates a new Flutter-based application.
*
* Returns: a new #MyApplication.
*/
MyApplication* my_application_new();
#endif // FLUTTER_MY_APPLICATION_H_

View File

@@ -3,10 +3,10 @@ description: "Nem hivatalos e-napló alkalmazás az e-Kréta rendszerhez"
homepage: https://filcnaplo.hu homepage: https://filcnaplo.hu
publish_to: "none" publish_to: "none"
version: 3.0.4+133 version: 3.2.1+143
environment: environment:
sdk: ">=2.12.0 <3.0.0" sdk: ">=2.16.0-80.1.beta <3.0.0"
dependencies: dependencies:
flutter: flutter:
@@ -32,15 +32,19 @@ dependencies:
html: ^0.15.0 html: ^0.15.0
open_file: ^3.2.1 open_file: ^3.2.1
path_provider: ^2.0.2 path_provider: ^2.0.2
permission_handler: ^8.1.4+2 permission_handler: ^8.3.0
share_plus: ^2.1.4 share_plus: ^3.0.4
package_info_plus: ^1.0.6 package_info_plus: ^1.0.6
connectivity_plus: ^2.0.2
flutter_displaymode: ^0.3.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^1.0.0
# flutter_launcher_icons: ^0.9.0 # flutter_launcher_icons: ^0.9.0
# flutter_native_splash: ^1.2.0 # flutter_native_splash: ^1.2.0
sqflite_common_ffi: ^2.0.0+3
flutter: flutter:
uses-material-design: true uses-material-design: true
@@ -101,6 +105,17 @@ flutter:
weight: 100 weight: 100
style: italic style: italic
- family: SpaceMono
fonts:
- asset: assets/fonts/SpaceMono/SpaceMono-Regular.ttf
- asset: assets/fonts/SpaceMono/SpaceMono-Bold.ttf
weight: 700
- asset: assets/fonts/SpaceMono/SpaceMono-Italic.ttf
style: italic
- asset: assets/fonts/SpaceMono/SpaceMono-BoldItalic.ttf
weight: 700
style: italic
flutter_icons: flutter_icons:
image_path: "assets/icons/ic_launcher.png" image_path: "assets/icons/ic_launcher.png"
adaptive_icon_background: "#1F5B50" adaptive_icon_background: "#1F5B50"

6
fix-d8dx.sh Normal file
View File

@@ -0,0 +1,6 @@
#!/bin/sh
cd $ANDROID_SDK/build-tools/31.0.0 &&
mv -v d8 dx &&
cd lib &&
mv -v d8.jar dx.jar