This commit is contained in:
unknown
2021-08-30 22:38:58 +02:00
parent 9544d89993
commit 46fa86f989
152 changed files with 4074 additions and 0 deletions

View File

@@ -0,0 +1,85 @@
import 'dart:convert';
import 'package:filcnaplo/models/config.dart';
import 'package:filcnaplo/models/news.dart';
import 'package:filcnaplo/models/release.dart';
import 'package:filcnaplo_kreta_api/models/school.dart';
import 'package:http/http.dart' as http;
class FilcAPI {
static const SCHOOL_LIST = "https://filcnaplo.hu/v2/school_list.json";
static const CONFIG = "https://filcnaplo.hu/v2/config.json";
static const NEWS = "https://filcnaplo.hu/v2/news.json";
static const REPO = "filc/naplo";
static const RELEASES = "https://api.github.com/repos/$REPO/releases";
static Future<List<School>?> getSchools() async {
try {
http.Response res = await http.get(Uri.parse(SCHOOL_LIST));
if (res.statusCode == 200) {
return (jsonDecode(res.body) as List).cast<Map>().map((json) => School.fromJson(json)).toList();
} else {
throw "HTTP ${res.statusCode}: ${res.body}";
}
} catch (error) {
print("ERROR: FilcAPI.getSchools: $error");
}
}
static Future<Config?> getConfig() async {
try {
http.Response res = await http.get(Uri.parse(CONFIG));
if (res.statusCode == 200) {
return Config.fromJson(jsonDecode(res.body));
} else {
throw "HTTP ${res.statusCode}: ${res.body}";
}
} catch (error) {
print("ERROR: FilcAPI.getConfig: $error");
}
}
static Future<List<News>?> getNews() async {
try {
http.Response res = await http.get(Uri.parse(NEWS));
if (res.statusCode == 200) {
return (jsonDecode(res.body) as List).cast<Map>().map((e) => News.fromJson(e)).toList();
} else {
throw "HTTP ${res.statusCode}: ${res.body}";
}
} catch (error) {
print("ERROR: FilcAPI.getNews: $error");
}
}
static Future<List<Release>?> getReleases() async {
try {
http.Response res = await http.get(Uri.parse(RELEASES));
if (res.statusCode == 200) {
return (jsonDecode(res.body) as List).cast<Map>().map((e) => Release.fromJson(e)).toList();
} else {
throw "HTTP ${res.statusCode}: ${res.body}";
}
} catch (error) {
print("ERROR: FilcAPI.getReleases: $error");
}
}
static Future<http.StreamedResponse?> downloadRelease(Release release) {
if (release.downloads.length > 0) {
try {
var client = http.Client();
var request = http.Request('GET', Uri.parse(release.downloads.first));
return client.send(request);
} catch (error) {
print("ERROR: FilcAPI.downloadRelease: $error");
}
}
return Future.value(null);
}
}

View File

@@ -0,0 +1,110 @@
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:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/models/user.dart';
import 'package:filcnaplo/utils/jwt.dart';
import 'package:filcnaplo_kreta_api/client/api.dart';
import 'package:filcnaplo_kreta_api/client/client.dart';
import 'package:filcnaplo_kreta_api/models/message.dart';
import 'package:filcnaplo_kreta_api/models/student.dart';
import 'package:filcnaplo_kreta_api/models/week.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:filcnaplo/api/nonce.dart';
enum LoginState { normal, inProgress, missingFields, invalidGrant, success, failed }
Nonce getNonce(BuildContext context, String nonce, String username, String instituteCode) {
Nonce nonceEncoder = Nonce(key: [53, 75, 109, 112, 109, 103, 100, 53, 102, 74], nonce: nonce);
nonceEncoder.encode(username.toLowerCase() + instituteCode.toLowerCase() + nonce);
return nonceEncoder;
}
Future loginApi({
required String username,
required String password,
required String instituteCode,
required BuildContext context,
void Function(User)? onLogin,
void Function()? onSuccess,
}) async {
Provider.of<KretaClient>(context, listen: false).userAgent = Provider.of<SettingsProvider>(context, listen: false).config.userAgent;
Map<String, String> headers = {
"content-type": "application/x-www-form-urlencoded",
};
String nonceStr = await Provider.of<KretaClient>(context, listen: false).getAPI(KretaAPI.nonce, json: false);
Nonce nonce = getNonce(context, nonceStr, username, instituteCode);
headers.addAll(nonce.header());
Map? res = await Provider.of<KretaClient>(context, listen: false).postAPI(KretaAPI.login,
headers: headers,
body: User.loginBody(
username: username,
password: password,
instituteCode: instituteCode,
));
if (res != null) {
if (res.containsKey("error")) {
if (res["error"] == "invalid_grant") {
return LoginState.invalidGrant;
}
} else {
if (res.containsKey("access_token")) {
try {
Provider.of<KretaClient>(context, listen: false).accessToken = res["access_token"];
Map? studentJson = await Provider.of<KretaClient>(context, listen: false).getAPI(KretaAPI.student(instituteCode));
var user = User(
username: username,
password: password,
instituteCode: instituteCode,
name: JwtUtils.getNameFromJWT(res["access_token"]) ?? "?",
student: Student.fromJson(studentJson!),
);
if (onLogin != null) onLogin(user);
// Store User in the database
await Provider.of<DatabaseProvider>(context, listen: false).store.storeUser(user);
Provider.of<UserProvider>(context, listen: false).addUser(user);
Provider.of<UserProvider>(context, listen: false).setUser(user.id);
// Get user data
try {
await Provider.of<GradeProvider>(context, listen: false).fetch();
await Provider.of<TimetableProvider>(context, listen: false).fetch(week: Week.current());
await Provider.of<ExamProvider>(context, listen: false).fetch();
await Provider.of<HomeworkProvider>(context, listen: false).fetch();
await Provider.of<MessageProvider>(context, listen: false).fetch(type: MessageType.inbox);
await Provider.of<NoteProvider>(context, listen: false).fetch();
await Provider.of<EventProvider>(context, listen: false).fetch();
await Provider.of<AbsenceProvider>(context, listen: false).fetch();
} catch (error) {
print("WARNING: failed to fetch user data: $error");
}
if (onSuccess != null) onSuccess();
return LoginState.success;
} catch (error) {
print("ERROR: loginApi: $error");
// maybe check debug mode
// ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("ERROR: $error")));
return LoginState.failed;
}
}
}
}
return LoginState.failed;
}

View File

@@ -0,0 +1,25 @@
import 'dart:convert';
import 'package:crypto/crypto.dart';
class Nonce {
String nonce;
List<int> key;
String? encoded;
Nonce({required this.nonce, required this.key});
Future encode(String message) async {
List<int> messageBytes = utf8.encode(message);
Hmac hmac = Hmac(sha512, key);
Digest digest = hmac.convert(messageBytes);
encoded = base64.encode(digest.bytes);
}
Map<String, String> header() {
return {
"X-Authorizationpolicy-Nonce": nonce,
"X-Authorizationpolicy-Key": encoded ?? "",
"X-Authorizationpolicy-Version": "v1",
};
}
}

View File

@@ -0,0 +1,20 @@
import 'package:filcnaplo/database/query.dart';
import 'package:filcnaplo/database/store.dart';
import 'package:sqflite/sqflite.dart';
class DatabaseProvider {
// late Database _database;
late DatabaseQuery query;
late UserDatabaseQuery userQuery;
late DatabaseStore store;
late UserDatabaseStore userStore;
Future<void> init() async {
var db = await openDatabase("app.db");
// _database = db;
query = DatabaseQuery(db: db);
store = DatabaseStore(db: db);
userQuery = UserDatabaseQuery(db: db);
userStore = UserDatabaseStore(db: db);
}
}

View File

@@ -0,0 +1,82 @@
import 'dart:math';
import 'package:filcnaplo/api/client.dart';
import 'package:filcnaplo/models/news.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class NewsProvider extends ChangeNotifier {
// Private
late List<News> _news;
late int _state;
late int _fresh;
bool show = false;
late BuildContext _context;
// Public
List<News> get news => _news;
int get state => _fresh - 1;
NewsProvider({
List<News> initialNews = const [],
required BuildContext context,
}) {
_news = List.castFrom(initialNews);
_context = context;
}
Future<void> restore() async {
// Load news state from the database
var state_ = Provider.of<SettingsProvider>(_context, listen: false).newsState;
if (state_ == -1) {
var news_ = await FilcAPI.getNews();
if (news_ != null) {
state_ = news_.length;
_news = news_;
}
}
_state = state_;
Provider.of<SettingsProvider>(_context, listen: false).update(_context, newsState: _state);
}
Future<void> fetch() async {
var news_ = await FilcAPI.getNews();
if (news_ == null) return;
_news = news_;
_fresh = news_.length - _state;
if (_fresh < 0) {
_state = news_.length;
Provider.of<SettingsProvider>(_context, listen: false).update(_context, newsState: _state);
}
_fresh = max(_fresh, 0);
if (_fresh > 0) {
show = true;
notifyListeners();
}
}
void lock() => show = false;
void release() {
if (_fresh == 0) return;
_fresh--;
_state++;
Provider.of<SettingsProvider>(_context, listen: false).update(_context, newsState: _state);
if (_fresh > 0)
show = true;
else
show = false;
notifyListeners();
}
}

View File

@@ -0,0 +1,41 @@
import 'dart:io';
import 'package:filcnaplo/api/client.dart';
import 'package:filcnaplo/models/release.dart';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
class UpdateProvider extends ChangeNotifier {
// Private
late List<Release> _releases;
late BuildContext _context;
bool _available = false;
bool get available => _available && _releases.length > 0;
PackageInfo? _packageInfo;
// Public
List<Release> get releases => _releases;
UpdateProvider({
List<Release> initialReleases = const [],
required BuildContext context,
}) {
_releases = List.castFrom(initialReleases);
_context = context;
PackageInfo.fromPlatform().then((value) => _packageInfo = value);
}
Future<void> fetch() async {
if (!Platform.isAndroid) return;
_releases = await FilcAPI.getReleases() ?? [];
_releases.sort((a, b) => -a.version.compareTo(b.version));
// Check for new releases
if (_releases.length > 0) {
print("INFO: New update: ${releases.first.version}");
_available = _packageInfo != null && _releases.first.version.compareTo(Version.fromString(_packageInfo?.version ?? "")) == 1;
notifyListeners();
}
}
}

View File

@@ -0,0 +1,41 @@
import 'package:filcnaplo/models/user.dart';
import 'package:filcnaplo_kreta_api/models/student.dart';
import 'package:flutter/foundation.dart';
class UserProvider with ChangeNotifier {
Map<String, User> _users = {};
String? _selectedUserId;
User? get user => _users[_selectedUserId];
// _user properties
String? get instituteCode => user?.instituteCode;
String? get id => user?.id;
String? get name => user?.name;
String? get username => user?.username;
String? get password => user?.password;
Student? get student => user?.student;
void setUser(String userId) {
_selectedUserId = userId;
notifyListeners();
}
void addUser(User user) {
_users[user.id] = user;
print("DEBUG: Added User: ${user.id} ${user.name}");
}
void removeUser(String userId) {
_users.removeWhere((key, value) => key == userId);
if (_users.isNotEmpty) _selectedUserId = _users.keys.first;
notifyListeners();
}
User getUser(String userId) {
return _users[userId]!;
}
List<User> getUsers() {
return _users.values.toList();
}
}

120
filcnaplo/lib/app.dart Normal file
View File

@@ -0,0 +1,120 @@
import 'package:filcnaplo/api/client.dart';
import 'package:filcnaplo/api/providers/news_provider.dart';
import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/models/config.dart';
import 'package:filcnaplo/theme.dart';
import 'package:filcnaplo_kreta_api/client/client.dart';
import 'package:filcnaplo_mobile_ui/common/system_chrome.dart';
import 'package:filcnaplo_mobile_ui/screens/login/login_route.dart';
import 'package:filcnaplo_mobile_ui/screens/login/login_screen.dart';
import 'package:filcnaplo_mobile_ui/screens/navigation/navigation_screen.dart';
import 'package:filcnaplo_mobile_ui/screens/settings/settings_route.dart';
import 'package:filcnaplo_mobile_ui/screens/settings/settings_screen.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:i18n_extension/i18n_widget.dart';
import 'package:provider/provider.dart';
// Providers
import 'package:filcnaplo/models/settings.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:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo/api/providers/update_provider.dart';
import 'package:filcnaplo_mobile_ui/pages/grades/calculator/grade_calculator_provider.dart';
class App extends StatelessWidget {
final SettingsProvider settings;
final UserProvider user;
final DatabaseProvider database;
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);
}
@override
Widget build(BuildContext context) {
setSystemChrome(context);
WidgetsBinding.instance?.addPostFrameCallback((_) {
FilcAPI.getConfig().then((Config? config) {
settings.update(context, database: database, config: config ?? Config.fromJson({}));
});
});
return I18n(
initialLocale: Locale(settings.language, settings.language),
child: MultiProvider(
providers: [
ChangeNotifierProvider<SettingsProvider>(create: (_) => settings),
ChangeNotifierProvider<UserProvider>(create: (_) => user),
Provider<KretaClient>(create: (context) => KretaClient(context: context, userAgent: settings.config.userAgent)),
Provider<DatabaseProvider>(create: (context) => database),
ChangeNotifierProvider<ThemeModeObserver>(create: (context) => ThemeModeObserver(initialTheme: settings.theme)),
ChangeNotifierProvider<NewsProvider>(create: (context) => NewsProvider(context: context)),
ChangeNotifierProvider<UpdateProvider>(create: (context) => UpdateProvider(context: context)),
// User data providers
ChangeNotifierProvider<GradeProvider>(create: (context) => GradeProvider(context: context)),
ChangeNotifierProvider<TimetableProvider>(create: (context) => TimetableProvider(context: context)),
ChangeNotifierProvider<ExamProvider>(create: (context) => ExamProvider(context: context)),
ChangeNotifierProvider<HomeworkProvider>(create: (context) => HomeworkProvider(context: context)),
ChangeNotifierProvider<MessageProvider>(create: (context) => MessageProvider(context: context)),
ChangeNotifierProvider<NoteProvider>(create: (context) => NoteProvider(context: context)),
ChangeNotifierProvider<EventProvider>(create: (context) => EventProvider(context: context)),
ChangeNotifierProvider<AbsenceProvider>(create: (context) => AbsenceProvider(context: context)),
ChangeNotifierProvider<GradeCalculatorProvider>(create: (context) => GradeCalculatorProvider(context)),
],
child: Consumer<ThemeModeObserver>(
builder: (context, themeMode, child) => MaterialApp(
title: "Filc Napló",
debugShowCheckedModeBanner: false,
theme: AppTheme.lightTheme(context),
darkTheme: AppTheme.darkTheme(context),
themeMode: themeMode.themeMode,
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: [
const Locale('en'),
const Locale('hu'),
const Locale('de'),
],
onGenerateRoute: (settings) => rootNavigator(settings),
initialRoute: user.getUsers().length > 0 ? "navigation" : "login"),
),
),
);
}
Route? rootNavigator(RouteSettings route) {
// if platform == android || platform == ios
switch (route.name) {
case "login_back":
return CupertinoPageRoute(builder: (context) => LoginScreen(back: true));
case "login":
return _rootRoute(LoginScreen());
case "navigation":
return _rootRoute(Navigation());
case "login_to_navigation":
return loginRoute(Navigation());
case "settings":
return settingsRoute(SettingsScreen());
}
// else if platform == windows || ...
}
Route _rootRoute(Widget widget) {
return PageRouteBuilder(pageBuilder: (context, _, __) => widget);
}
}

View File

@@ -0,0 +1,72 @@
import 'package:filcnaplo/database/struct.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:sqflite/sqflite.dart';
Future<Database> initDB() async {
// await deleteDatabase('app.db'); // for debugging
var db = await openDatabase('app.db');
var settingsDB = await createSettingsTable(db);
// Create table Users
await db.execute("CREATE TABLE IF NOT EXISTS users (id TEXT NOT NULL, name TEXT, username TEXT, password TEXT, institute_code TEXT, student TEXT)");
await db.execute("CREATE TABLE IF NOT EXISTS user_data ("
"id TEXT NOT NULL, grades TEXT, timetable TEXT, exams TEXT, homework TEXT, messages TEXT, notes TEXT, events TEXT, absences TEXT)");
if ((await db.rawQuery("SELECT COUNT(*) FROM settings"))[0].values.first == 0) {
// Set default values for table Settings
await db.insert("settings", SettingsProvider.defaultSettings().toMap());
}
await migrateDB(db, settingsDB.struct.keys);
return db;
}
Future<DatabaseStruct> createSettingsTable(Database db) async {
var settingsDB = DatabaseStruct({
"language": String, "start_page": int, "rounding": int, "theme": int, "accent_color": int, "news": int, "news_state": int, "developer_mode": int,
"update_channel": int, "config": String, // general
"grade_color1": int, "grade_color2": int, "grade_color3": int, "grade_color4": int, "grade_color5": int, // grade colors
"vibrate": int, "vibration_strength": int, "ab_weeks": int, "swap_ab_weeks": int,
"notifications": int, "notifications_bitfield": int, "notification_poll_interval": int, // notifications
});
// Create table Settings
await db.execute("CREATE TABLE IF NOT EXISTS settings ($settingsDB)");
return settingsDB;
}
Future<void> migrateDB(Database db, Iterable<String> keys) async {
var settings = (await db.query("settings"))[0];
bool migrationRequired = keys.any((key) => !settings.containsKey(key) || settings[key] == null);
if (migrationRequired) {
var defaultSettings = SettingsProvider.defaultSettings();
var settingsCopy = Map<String, dynamic>.from(settings);
// Delete settings
await db.execute("drop table settings");
// Fill missing columns
keys.forEach((key) {
if (!keys.contains(key)) {
print("debug: dropping $key");
settingsCopy.remove(key);
}
if (!settings.containsKey(key) || settings[key] == null) {
print("DEBUG: migrating $key");
settingsCopy[key] = defaultSettings.toMap()[key];
}
});
// Recreate settings
await createSettingsTable(db);
await db.insert("settings", settingsCopy);
print("INFO: Database migrated");
}
}

View File

@@ -0,0 +1,114 @@
import 'dart:convert';
import 'package:filcnaplo/models/user.dart';
import 'package:sqflite/sqflite.dart';
// Models
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo_kreta_api/models/lesson.dart';
import 'package:filcnaplo_kreta_api/models/exam.dart';
import 'package:filcnaplo_kreta_api/models/homework.dart';
import 'package:filcnaplo_kreta_api/models/message.dart';
import 'package:filcnaplo_kreta_api/models/note.dart';
import 'package:filcnaplo_kreta_api/models/event.dart';
import 'package:filcnaplo_kreta_api/models/absence.dart';
class DatabaseQuery {
DatabaseQuery({required this.db});
final Database db;
Future<SettingsProvider> getSettings() async {
Map settingsMap = (await db.query("settings")).elementAt(0);
SettingsProvider settings = SettingsProvider.fromMap(settingsMap);
return settings;
}
Future<UserProvider> getUsers() async {
var userProvider = UserProvider();
List<Map> usersMap = await db.query("users");
usersMap.forEach((user) {
userProvider.addUser(User.fromMap(user));
});
return userProvider;
}
}
class UserDatabaseQuery {
UserDatabaseQuery({required this.db});
final Database db;
Future<List<Grade>> getGrades({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.length == 0) return [];
String? gradesJson = userData.elementAt(0)["grades"] as String?;
if (gradesJson == null) return [];
List<Grade> grades = (jsonDecode(gradesJson) as List).map((e) => Grade.fromJson(e)).toList();
return grades;
}
Future<List<Lesson>> getLessons({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.length == 0) return [];
String? lessonsJson = userData.elementAt(0)["timetable"] as String?;
if (lessonsJson == null) return [];
List<Lesson> lessons = (jsonDecode(lessonsJson) as List).map((e) => Lesson.fromJson(e)).toList();
return lessons;
}
Future<List<Exam>> getExams({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.length == 0) return [];
String? examsJson = userData.elementAt(0)["exams"] as String?;
if (examsJson == null) return [];
List<Exam> exams = (jsonDecode(examsJson) as List).map((e) => Exam.fromJson(e)).toList();
return exams;
}
Future<List<Homework>> getHomework({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.length == 0) return [];
String? homeworkJson = userData.elementAt(0)["homework"] as String?;
if (homeworkJson == null) return [];
List<Homework> homework = (jsonDecode(homeworkJson) as List).map((e) => Homework.fromJson(e)).toList();
return homework;
}
Future<List<Message>> getMessages({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.length == 0) return [];
String? messagesJson = userData.elementAt(0)["messages"] as String?;
if (messagesJson == null) return [];
List<Message> messages = (jsonDecode(messagesJson) as List).map((e) => Message.fromJson(e)).toList();
return messages;
}
Future<List<Note>> getNotes({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.length == 0) return [];
String? notesJson = userData.elementAt(0)["notes"] as String?;
if (notesJson == null) return [];
List<Note> notes = (jsonDecode(notesJson) as List).map((e) => Note.fromJson(e)).toList();
return notes;
}
Future<List<Event>> getEvents({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.length == 0) return [];
String? eventsJson = userData.elementAt(0)["events"] as String?;
if (eventsJson == null) return [];
List<Event> events = (jsonDecode(eventsJson) as List).map((e) => Event.fromJson(e)).toList();
return events;
}
Future<List<Absence>> getAbsences({required String userId}) async {
List<Map> userData = await db.query("user_data", where: "id = ?", whereArgs: [userId]);
if (userData.length == 0) return [];
String? absebcesJson = userData.elementAt(0)["absences"] as String?;
if (absebcesJson == null) return [];
List<Absence> absebces = (jsonDecode(absebcesJson) as List).map((e) => Absence.fromJson(e)).toList();
return absebces;
}
}

View File

@@ -0,0 +1,85 @@
import 'dart:convert';
import 'package:sqflite/sqflite.dart';
// Models
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/models/user.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo_kreta_api/models/lesson.dart';
import 'package:filcnaplo_kreta_api/models/exam.dart';
import 'package:filcnaplo_kreta_api/models/homework.dart';
import 'package:filcnaplo_kreta_api/models/message.dart';
import 'package:filcnaplo_kreta_api/models/note.dart';
import 'package:filcnaplo_kreta_api/models/event.dart';
import 'package:filcnaplo_kreta_api/models/absence.dart';
class DatabaseStore {
DatabaseStore({required this.db});
final Database db;
Future<void> storeSettings(SettingsProvider settings) async {
await db.update("settings", settings.toMap());
}
Future<void> storeUser(User user) async {
List userRes = await db.query("users", where: "id = ?", whereArgs: [user.id]);
if (userRes.length > 0) {
await db.update("users", user.toMap(), where: "id = ?", whereArgs: [user.id]);
} else {
await db.insert("users", user.toMap());
await db.insert("user_data", {"id": user.id});
}
}
Future<void> removeUser(String userId) async {
await db.delete("users", where: "id = ?", whereArgs: [userId]);
await db.delete("user_data", where: "id = ?", whereArgs: [userId]);
}
}
class UserDatabaseStore {
UserDatabaseStore({required this.db});
final Database db;
Future storeGrades(List<Grade> grades, {required String userId}) async {
String gradesJson = jsonEncode(grades.map((e) => e.json).toList());
await db.update("user_data", {"grades": gradesJson}, where: "id = ?", whereArgs: [userId]);
}
Future storeLessons(List<Lesson> lessons, {required String userId}) async {
String lessonsJson = jsonEncode(lessons.map((e) => e.json).toList());
await db.update("user_data", {"timetable": lessonsJson}, where: "id = ?", whereArgs: [userId]);
}
Future storeExams(List<Exam> exams, {required String userId}) async {
String examsJson = jsonEncode(exams.map((e) => e.json).toList());
await db.update("user_data", {"exams": examsJson}, where: "id = ?", whereArgs: [userId]);
}
Future storeHomework(List<Homework> homework, {required String userId}) async {
String homeworkJson = jsonEncode(homework.map((e) => e.json).toList());
await db.update("user_data", {"homework": homeworkJson}, where: "id = ?", whereArgs: [userId]);
}
Future storeMessages(List<Message> messages, {required String userId}) async {
String messagesJson = jsonEncode(messages.map((e) => e.json).toList());
await db.update("user_data", {"messages": messagesJson}, where: "id = ?", whereArgs: [userId]);
}
Future storeNotes(List<Note> notes, {required String userId}) async {
String notesJson = jsonEncode(notes.map((e) => e.json).toList());
await db.update("user_data", {"notes": notesJson}, where: "id = ?", whereArgs: [userId]);
}
Future storeEvents(List<Event> events, {required String userId}) async {
String eventsJson = jsonEncode(events.map((e) => e.json).toList());
await db.update("user_data", {"events": eventsJson}, where: "id = ?", whereArgs: [userId]);
}
Future storeAbsences(List<Absence> absences, {required String userId}) async {
String absencesJson = jsonEncode(absences.map((e) => e.json).toList());
await db.update("user_data", {"absences": absencesJson}, where: "id = ?", whereArgs: [userId]);
}
}

View File

@@ -0,0 +1,29 @@
class DatabaseStruct {
final Map<String, dynamic> struct;
DatabaseStruct(this.struct);
String _toDBfield(String name, dynamic type) {
String typeName = "";
switch (type.runtimeType) {
case int:
typeName = "integer";
break;
case String:
typeName = "text";
break;
}
return "${name} ${typeName.toUpperCase()}";
}
@override
String toString() {
List<String> columns = [];
struct.forEach((key, value) {
columns.add(_toDBfield(key, value));
});
return columns.join(",");
}
}

View File

@@ -0,0 +1,30 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:filcnaplo/helpers/storage_helper.dart';
import 'package:filcnaplo_kreta_api/client/client.dart';
import 'package:filcnaplo_kreta_api/models/attachment.dart';
import 'package:flutter/widgets.dart';
import 'package:open_file/open_file.dart';
import 'package:provider/provider.dart';
extension AttachmentHelper on Attachment {
Future<String> download(BuildContext context, {bool overwrite = false}) async {
String downloads = await StorageHelper.downloadsPath();
if (!overwrite && await File("$downloads/$name").exists()) return "$downloads/$name";
Uint8List data = await Provider.of<KretaClient>(context, listen: false).getAPI(downloadUrl, rawResponse: true);
if (!await StorageHelper.write("$downloads/$name", data)) return "";
return "$downloads/$name";
}
Future<bool> open(BuildContext context) async {
String downloads = await StorageHelper.downloadsPath();
if (!await File("$downloads/$name").exists()) await download(context);
var result = await OpenFile.open("$downloads/$name");
return result.type == ResultType.done;
}
}

View File

@@ -0,0 +1,25 @@
import 'package:filcnaplo_kreta_api/models/grade.dart';
class AverageHelper {
static double averageEvals(List<Grade> grades, {bool finalAvg = false}) {
double average = 0.0;
List<String> ignoreInFinal = ["5,SzorgalomErtek", "4,MagatartasErtek"];
if (finalAvg)
grades.removeWhere((e) =>
(e.value.value == 0) ||
(ignoreInFinal.contains(e.gradeType?.id)));
grades.forEach((e) {
average += e.value.value * ((finalAvg ? 100 : e.value.weight) / 100);
});
average = average /
grades
.map((e) => (finalAvg ? 100 : e.value.weight) / 100)
.fold(0.0, (a, b) => a + b);
return average.isNaN ? 0.0 : average;
}
}

View File

@@ -0,0 +1,14 @@
import 'package:filcnaplo/helpers/attachment_helper.dart';
import 'package:filcnaplo_kreta_api/models/attachment.dart';
import 'package:flutter/widgets.dart';
import 'package:share_plus/share_plus.dart';
class ShareHelper {
static Future<void> shareText(String text, {String? subject}) => Share.share(text, subject: subject);
static Future<void> shareFile(String path, {String? text, String? subject}) => Share.shareFiles([path], text: text, subject: subject);
static Future<void> shareAttachment(Attachment attachment, {required BuildContext context}) async {
String path = await attachment.download(context);
await shareFile(path);
}
}

View File

@@ -0,0 +1,36 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
class StorageHelper {
static Future<bool> write(String path, Uint8List data) async {
try {
if (await Permission.storage.request().isGranted) {
await File(path).writeAsBytes(data);
return true;
} else {
if (await Permission.storage.isPermanentlyDenied) {
openAppSettings();
}
return false;
}
} catch (error) {
print("ERROR: StorageHelper.write: $error");
return false;
}
}
static Future<String> downloadsPath() async {
String downloads;
if (Platform.isAndroid) {
downloads = "/storage/self/primary/Download";
} else {
downloads = (await getTemporaryDirectory()).path;
}
return downloads;
}
}

View File

@@ -0,0 +1,46 @@
import 'package:filcnaplo/icons/filc_icons.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo_kreta_api/models/subject.dart';
import 'package:flutter/material.dart';
class SubjectIcon {
static IconData? lookup({Subject? subject, String? subjectName}) {
assert(!(subject == null && subjectName == null));
String name = subject?.name.toLowerCase().specialChars() ?? subjectName ?? "";
String category = subject?.category.description.toLowerCase().specialChars() ?? "";
// TODO: check for categories
if (RegExp("mate(k|matika)").hasMatch(name) || category == "matematika") return Icons.calculate_outlined;
if (RegExp("magyar nyelv|nyelvtan").hasMatch(name)) return Icons.spellcheck_outlined;
if (RegExp("irodalom").hasMatch(name)) return Icons.menu_book_outlined;
if (RegExp("rajz|muvtori|muveszet|kultura").hasMatch(name)) return Icons.palette_outlined;
if (RegExp("tor(i|tenelem)").hasMatch(name)) return Icons.hourglass_empty_outlined;
if (RegExp("foldrajz").hasMatch(name)) return Icons.public_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("^tes(i|tneveles)|sport").hasMatch(name)) return Icons.sports_soccer_outlined;
if (RegExp("kemia").hasMatch(name)) return Icons.science_outlined;
if (RegExp("biologia").hasMatch(name)) return Icons.pets_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("penzugy").hasMatch(name)) return Icons.savings_outlined;
if (RegExp("informatika|szoftver|iroda").hasMatch(name)) return Icons.computer_outlined;
if (RegExp("prog").hasMatch(name)) return Icons.code_outlined;
if (RegExp("halozat").hasMatch(name)) return Icons.wifi_tethering_outlined;
if (RegExp("szinhaz").hasMatch(name)) return Icons.theater_comedy_outlined;
if (RegExp("film|media").hasMatch(name)) return Icons.theaters_outlined;
if (RegExp("elektro(tech)?nika").hasMatch(name)) return Icons.electrical_services_outlined;
if (RegExp("gepesz|mernok|ipar").hasMatch(name)) return Icons.precision_manufacturing_outlined;
if (RegExp("technika").hasMatch(name)) return Icons.build_outlined;
if (RegExp("tanc").hasMatch(name)) return Icons.speaker_outlined;
if (RegExp("filozofia").hasMatch(name)) return Icons.psychology_outlined;
if (RegExp("osztaly(fonoki|kozosseg)").hasMatch(name)) return Icons.groups_outlined;
if (RegExp("gazdasag").hasMatch(name)) return Icons.account_balance_outlined;
if (RegExp("szorgalom").hasMatch(name)) return Icons.verified_outlined;
if (RegExp("magatartas").hasMatch(name)) return Icons.emoji_people_outlined;
if (RegExp("angol|nemet|francia|olasz|orosz|spanyol|latin|kinai|nyelv").hasMatch(name)) return Icons.translate_outlined;
if (RegExp("linux").hasMatch(name)) return FilcIcons.linux;
return Icons.widgets_outlined;
}
}

View File

@@ -0,0 +1,65 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:filcnaplo/api/client.dart';
import 'package:filcnaplo/helpers/storage_helper.dart';
import 'package:filcnaplo/models/release.dart';
import 'package:open_file/open_file.dart';
enum UpdateState { prepare, downloading, installing }
typedef UpdateCallback = Function(double progress, UpdateState state);
extension UpdateHelper on Release {
Future<void> install({UpdateCallback? updateCallback}) async {
String downloads = await StorageHelper.downloadsPath();
File apk = File("$downloads/filcnaplo-${version}.apk");
if (!await apk.exists()) {
updateCallback!(-1, UpdateState.downloading);
var bytes = await download(updateCallback: updateCallback);
if (!await StorageHelper.write(apk.path, bytes)) throw "failed to write apk: permission denied";
}
updateCallback!(-1, UpdateState.installing);
var result = await OpenFile.open(apk.path);
if (result.type != ResultType.done) {
print("ERROR: installUpdate.openFile: " + result.message);
throw result.message;
}
updateCallback(-1, UpdateState.prepare);
}
Future<Uint8List> download({UpdateCallback? updateCallback}) async {
var response = await FilcAPI.downloadRelease(this);
List<List<int>> chunks = [];
int downloaded = 0;
var completer = Completer<Uint8List>();
response?.stream.listen((List<int> chunk) {
updateCallback!(downloaded / (response.contentLength ?? 0), UpdateState.downloading);
chunks.add(chunk);
downloaded += chunk.length;
}, onDone: () {
// Save the file
final Uint8List bytes = Uint8List(response.contentLength ?? 0);
int offset = 0;
for (List<int> chunk in chunks) {
bytes.setRange(offset, offset + chunk.length, chunk);
offset += chunk.length;
}
completer.complete(bytes);
});
return completer.future;
}
}

View File

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

71
filcnaplo/lib/main.dart Normal file
View File

@@ -0,0 +1,71 @@
import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/database/init.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:flutter/material.dart';
import 'package:filcnaplo/app.dart';
import 'package:flutter/services.dart';
import 'package:filcnaplo_mobile_ui/screens/error_screen.dart';
/*
* TODO: public beta checklist
*
* Pages:
* - [x] Home
* ~~- [ ] search~~
* - [x] user data
* - [x] greeting
* - [x] Grades
* - [x] Timetable
* - [x] Messages
* - [x] Absences
*
* - [ ] i18n
* - [ ] auto updater
* - [ ] news WIP
* - [ ] settings (about)
*/
void main() async {
// Initalize
WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized();
binding.renderView.automaticSystemUiAdjustment = false;
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
// Startup
Startup startup = Startup();
await startup.start();
// Custom error page
ErrorWidget.builder = errorBuilder;
// Run App
runApp(App(database: startup.database, settings: startup.settings, user: startup.user));
}
class Startup {
late SettingsProvider settings;
late UserProvider user;
late DatabaseProvider database;
Future<void> start() async {
var db = await initDB();
await db.close();
database = DatabaseProvider();
await database.init();
settings = await database.query.getSettings();
user = await database.query.getUsers();
}
}
Widget errorBuilder(FlutterErrorDetails details) {
return Builder(builder: (context) {
if (Navigator.of(context).canPop()) Navigator.pop(context);
WidgetsBinding.instance?.addPostFrameCallback((_) {
Navigator.of(context, rootNavigator: true).push(MaterialPageRoute(builder: (ctx) => ErrorScreen(details)));
});
return Container();
});
}

View File

@@ -0,0 +1,41 @@
import 'dart:io';
import 'package:package_info_plus/package_info_plus.dart';
class Config {
String _userAgent;
String? _version;
Map? json;
Config({required String userAgent, this.json}) : _userAgent = userAgent {
PackageInfo.fromPlatform().then((value) => _version = value.version);
}
factory Config.fromJson(Map json) {
return Config(
userAgent: json["user_agent"] ?? "hu.filc.naplo/\$0/\$1/\$2",
json: json,
);
}
String get userAgent => _userAgent.replaceAll("\$0", _version ?? "0").replaceAll("\$1", platform).replaceAll("\$2", "0");
static String get platform {
if (Platform.isAndroid) {
return "Android";
} else if (Platform.isIOS) {
return "iOS";
} else if (Platform.isLinux) {
return "Linux";
} else if (Platform.isWindows) {
return "Windows";
} else if (Platform.isMacOS) {
return "MacOS";
} else {
return "Unknown";
}
}
@override
String toString() => json.toString();
}

View File

@@ -0,0 +1,31 @@
class News {
String title;
String content;
String link;
String openLabel;
String platform;
bool emergency;
Map? json;
News({
required this.title,
required this.content,
required this.link,
required this.openLabel,
required this.platform,
required this.emergency,
this.json,
});
factory News.fromJson(Map json) {
return News(
title: json["title"] ?? "",
content: json["content"] ?? "",
link: json["link"] ?? "",
openLabel: json["open_label"] ?? "",
platform: json["platform"] ?? "",
emergency: json["emergency"] ?? false,
json: json,
);
}
}

View File

@@ -0,0 +1,129 @@
class Release {
String tag;
Version version;
String author;
String body;
List<String> downloads;
bool prerelease;
Release({
required this.tag,
required this.author,
required this.body,
required this.downloads,
required this.prerelease,
required this.version,
});
factory Release.fromJson(Map json) {
return Release(
tag: json["tag_name"] ?? Version.zero.toString(),
author: json["author"] != null ? json["author"]["login"] ?? "" : "",
body: json["body"] ?? "",
downloads: json["assets"] != null ? json["assets"].map((a) => a["browser_download_url"] ?? "").toList().cast<String>() : [],
prerelease: json["prerelease"] ?? false,
version: Version.fromString(json["tag_name"] ?? ""),
);
}
}
class Version {
final int major;
final int minor;
final int patch;
final String prerelease;
final int prever;
const Version(this.major, this.minor, this.patch, {this.prerelease = "", this.prever = 0});
factory Version.fromString(String o) {
String string = o;
int x = 0, y = 0, z = 0; // major, minor, patch (1.1.1)
String pre = ""; // prerelease string (-beta)
int prev = 0; // prerelease version (.1)
try {
// cut build
string = string.split("+")[0];
// cut prerelease
var p = string.split("-");
string = p[0];
if (p.length > 1) pre = p[1];
// prerelease
p = pre.split(".");
if (p.length > 1) prev = int.tryParse(p[1]) ?? 0;
// check for valid prerelease name
if (p[0] != "") {
if (prereleases.contains(p[0].toLowerCase().trim()))
pre = p[0];
else
throw "invalid prerelease name: ${p[0]}";
}
// core
p = string.split(".");
if (p.length != 3) throw "invalid core length: ${p.length}";
x = int.tryParse(p[0]) ?? 0;
y = int.tryParse(p[1]) ?? 0;
z = int.tryParse(p[2]) ?? 0;
return Version(x, y, z, prerelease: pre, prever: prev);
} catch (error) {
print("WARNING: Failed to parse version ($o): $error");
return Version.zero;
}
}
@override
bool operator ==(other) {
if (other is! Version) return false;
return other.major == major && other.minor == minor && other.patch == patch && other.prei == prei && other.prever == prever;
}
int compareTo(Version other) {
if (other == this) return 0;
if (major > other.major) {
return 1;
} else if (major == other.major) {
if (minor > other.minor) {
return 1;
} else if (minor == other.minor) {
if (patch > other.patch) {
return 1;
} else if (patch == other.patch) {
if (prei > other.prei) {
return 1;
} else if (other.prei == prei) {
if (prever > other.prever) {
return 1;
}
}
}
}
}
return -1;
}
@override
String toString() {
String str = "$major.$minor.$patch";
if (prerelease != "") str += "-$prerelease";
if (prever != 0) str += ".$prever";
return str;
}
int get prei {
if (prerelease != "") return prereleases.indexOf(prerelease);
return prereleases.length;
}
static const zero = Version(0, 0, 0);
static const List<String> prereleases = ["dev", "pre", "alpha", "beta", "rc"];
}

View File

@@ -0,0 +1,246 @@
import 'dart:convert';
import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/models/config.dart';
import 'package:filcnaplo/theme.dart';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart';
enum Pages { home, grades, timetable, messages, absences }
enum UpdateChannel { stable, beta, dev }
enum VibrationStrength { light, medium, strong }
class SettingsProvider extends ChangeNotifier {
PackageInfo? _packageInfo;
// en_en, hu_hu, de_de
String _language;
Pages _startPage;
// divide by 10
int _rounding;
ThemeMode _theme;
AccentColor _accentColor;
// zero is one, ...
List<Color> _gradeColors;
bool _newsEnabled;
int _newsState;
bool _notificationsEnabled;
/*
notificationsBitfield values:
1 << 0 current lesson
1 << 1 newsletter
1 << 2 grades
1 << 3 notes and events
1 << 4 inbox messages
1 << 5 substituted lessons and cancelled lessons
1 << 6 absences and misses
1 << 7 exams and homework
*/
int _notificationsBitfield;
// minutes: times 15
int _notificationPollInterval;
bool _developerMode;
bool _vibrate;
VibrationStrength _vibrationStrength;
bool _ABweeks;
bool _swapABweeks;
UpdateChannel _updateChannel;
Config _config;
SettingsProvider({
required String language,
required Pages startPage,
required int rounding,
required ThemeMode theme,
required AccentColor accentColor,
required List<Color> gradeColors,
required bool newsEnabled,
required int newsState,
required bool notificationsEnabled,
required int notificationsBitfield,
required bool developerMode,
required int notificationPollInterval,
required bool vibrate,
required VibrationStrength vibrationStrength,
required bool ABweeks,
required bool swapABweeks,
required UpdateChannel updateChannel,
required Config config,
}) : _language = language,
_startPage = startPage,
_rounding = rounding,
_theme = theme,
_accentColor = accentColor,
_gradeColors = gradeColors,
_newsEnabled = newsEnabled,
_newsState = newsState,
_notificationsEnabled = notificationsEnabled,
_notificationsBitfield = notificationsBitfield,
_developerMode = developerMode,
_notificationPollInterval = notificationPollInterval,
_vibrate = vibrate,
_vibrationStrength = vibrationStrength,
_ABweeks = ABweeks,
_swapABweeks = swapABweeks,
_updateChannel = updateChannel,
_config = config {
PackageInfo.fromPlatform().then((PackageInfo packageInfo) {
_packageInfo = packageInfo;
});
}
factory SettingsProvider.fromMap(Map map) {
return SettingsProvider(
language: map["language"],
startPage: Pages.values[map["start_page"]],
rounding: map["rounding"],
theme: ThemeMode.values[map["theme"]],
accentColor: AccentColor.values[map["accent_color"]],
gradeColors: [
Color(map["grade_color1"]),
Color(map["grade_color2"]),
Color(map["grade_color3"]),
Color(map["grade_color4"]),
Color(map["grade_color5"]),
],
newsEnabled: map["news"] == 1 ? true : false,
newsState: map["news_state"],
notificationsEnabled: map["notifications"] == 1 ? true : false,
notificationsBitfield: map["notifications_bitfield"],
notificationPollInterval: map["notification_poll_interval"],
developerMode: map["developer_mode"] == 1 ? true : false,
vibrate: map["vibrate"] == 1 ? true : false,
vibrationStrength: VibrationStrength.values[map["vibration_strength"]],
ABweeks: map["ab_weeks"] == 1 ? true : false,
swapABweeks: map["swap_ab_weeks"] == 1 ? true : false,
updateChannel: UpdateChannel.values[map["update_channel"]],
config: Config.fromJson(jsonDecode(map["config"] ?? "{}")),
);
}
Map<String, Object?> toMap() {
return {
"language": _language,
"start_page": _startPage.index,
"rounding": _rounding,
"theme": _theme.index,
"accent_color": _accentColor.index,
"news": _newsEnabled ? 1 : 0,
"news_state": _newsState,
"notifications": _notificationsEnabled ? 1 : 0,
"notifications_bitfield": _notificationsBitfield,
"developer_mode": _developerMode ? 1 : 0,
"grade_color1": _gradeColors[0].value,
"grade_color2": _gradeColors[1].value,
"grade_color3": _gradeColors[2].value,
"grade_color4": _gradeColors[3].value,
"grade_color5": _gradeColors[4].value,
"update_channel": _updateChannel.index,
"vibrate": _vibrate ? 1 : 0,
"vibration_strength": _vibrationStrength.index,
"ab_weeks": _ABweeks ? 1 : 0,
"swap_ab_weeks": _swapABweeks ? 1 : 0,
"notification_poll_interval": _notificationPollInterval,
"config": jsonEncode(config.json),
};
}
factory SettingsProvider.defaultSettings() {
return SettingsProvider(
language: "hu",
startPage: Pages.home,
rounding: 5,
theme: ThemeMode.system,
accentColor: AccentColor.filc,
gradeColors: [
DarkAppColors().red,
DarkAppColors().orange,
DarkAppColors().yellow,
DarkAppColors().green,
DarkAppColors().filc,
],
newsEnabled: true,
newsState: -1,
notificationsEnabled: true,
notificationsBitfield: 255,
developerMode: false,
notificationPollInterval: 1,
vibrate: true,
vibrationStrength: VibrationStrength.medium,
ABweeks: false,
swapABweeks: false,
updateChannel: UpdateChannel.stable,
config: Config.fromJson({}),
);
}
// Getters
String get language => _language;
Pages get startPage => _startPage;
int get rounding => _rounding;
ThemeMode get theme => _theme;
AccentColor get accentColor => _accentColor;
List<Color> get gradeColors => _gradeColors;
bool get newsEnabled => _newsEnabled;
int get newsState => _newsState;
bool get notificationsEnabled => _notificationsEnabled;
int get notificationsBitfield => _notificationsBitfield;
bool get developerMode => _developerMode;
int get notificationPollInterval => _notificationPollInterval;
bool get vibrate => _vibrate;
VibrationStrength get vibrationStrength => _vibrationStrength;
bool get ABweeks => _ABweeks;
bool get swapABweeks => _swapABweeks;
UpdateChannel get updateChannel => _updateChannel;
PackageInfo? get packageInfo => _packageInfo;
Config get config => _config;
Future<void> update(
BuildContext context, {
DatabaseProvider? database,
String? language,
Pages? startPage,
int? rounding,
ThemeMode? theme,
AccentColor? accentColor,
List<Color>? gradeColors,
bool? newsEnabled,
int? newsState,
bool? notificationsEnabled,
int? notificationsBitfield,
bool? developerMode,
int? notificationPollInterval,
bool? vibrate,
VibrationStrength? vibrationStrength,
bool? ABweeks,
bool? swapABweeks,
UpdateChannel? updateChannel,
Config? config,
}) async {
if (language != null && language != _language) _language = language;
if (startPage != null && startPage != _startPage) _startPage = startPage;
if (rounding != null && rounding != _rounding) _rounding = rounding;
if (theme != null && theme != _theme) _theme = theme;
if (accentColor != null && accentColor != _accentColor) _accentColor = accentColor;
if (gradeColors != null && gradeColors != _gradeColors) _gradeColors = gradeColors;
if (newsEnabled != null && newsEnabled != _newsEnabled) _newsEnabled = newsEnabled;
if (newsState != null && newsState != _newsState) _newsState = newsState;
if (notificationsEnabled != null && notificationsEnabled != _notificationsEnabled) _notificationsEnabled = notificationsEnabled;
if (notificationsBitfield != null && notificationsBitfield != _notificationsBitfield) _notificationsBitfield = notificationsBitfield;
if (developerMode != null && developerMode != _developerMode) _developerMode = developerMode;
if (notificationPollInterval != null && notificationPollInterval != _notificationPollInterval)
_notificationPollInterval = notificationPollInterval;
if (vibrate != null && vibrate != _vibrate) _vibrate = vibrate;
if (vibrationStrength != null && vibrationStrength != _vibrationStrength) _vibrationStrength = vibrationStrength;
if (ABweeks != null && ABweeks != _ABweeks) _ABweeks = ABweeks;
if (swapABweeks != null && swapABweeks != _swapABweeks) _swapABweeks = swapABweeks;
if (updateChannel != null && updateChannel != _updateChannel) _updateChannel = updateChannel;
if (config != null && config != _config) _config = config;
if (database == null) database = Provider.of<DatabaseProvider>(context, listen: false);
await database.store.storeSettings(this);
notifyListeners();
}
}

View File

@@ -0,0 +1,64 @@
import 'dart:convert';
import 'package:filcnaplo_kreta_api/client/api.dart';
import 'package:filcnaplo_kreta_api/models/student.dart';
import 'package:uuid/uuid.dart';
class User {
late String id;
String username;
String password;
String instituteCode;
String name;
Student student;
User({
String? id,
required this.name,
required this.username,
required this.password,
required this.instituteCode,
required this.student,
}) {
if (id != null) {
this.id = id;
} else {
this.id = Uuid().v4();
}
}
factory User.fromMap(Map map) {
return User(
id: map["id"],
instituteCode: map["institute_code"],
username: map["username"],
password: map["password"],
name: map["name"],
student: Student.fromJson(jsonDecode(map["student"])),
);
}
Map<String, Object?> toMap() {
return {
"id": id,
"username": username,
"password": password,
"institute_code": instituteCode,
"name": name,
"student": jsonEncode(student.json),
};
}
static Map<String, Object?> loginBody({
required String username,
required String password,
required String instituteCode,
}) {
return {
"userName": username,
"password": password,
"institute_code": instituteCode,
"grant_type": "password",
"client_id": KretaAPI.CLIENT_ID,
};
}
}

View File

@@ -0,0 +1,376 @@
// import 'dart:io';
// import 'dart:typed_data';
// import 'package:feather_icons_flutter/feather_icons_flutter.dart';
// import 'package:filcnaplo/data/context/app.dart';
// import 'package:filcnaplo/ui/common/bottom_card.dart';
// import 'package:filcnaplo/utils/colors.dart';
// import 'package:flutter/cupertino.dart';
// import 'package:flutter/material.dart';
// import 'package:filcnaplo/generated/i18n.dart';
// import 'package:flutter_markdown/flutter_markdown.dart';
// import 'package:http/http.dart' as http;
// import 'package:path_provider/path_provider.dart';
// import 'package:open_file/open_file.dart';
// import 'package:filcnaplo/data/context/theme.dart';
// import '../../ui/common/custom_snackbar.dart';
// enum InstallState { update, downloading, saving, installing }
// class AutoUpdater extends StatefulWidget {
// @override
// _AutoUpdaterState createState() => _AutoUpdaterState();
// }
// class _AutoUpdaterState extends State<AutoUpdater> {
// bool buttonPressed = false;
// double progress;
// bool displayProgress = false;
// InstallState installState;
// void downloadCallback(
// double progress, bool displayProgress, InstallState installState) {
// if (mounted) {
// setState(() {
// this.progress = progress;
// this.displayProgress = displayProgress;
// this.installState = installState;
// });
// }
// }
// @override
// Widget build(BuildContext context) {
// if (!buttonPressed) installState = InstallState.update;
// String buttonText;
// switch (installState) {
// case InstallState.update:
// buttonText = I18n.of(context).update;
// break;
// case InstallState.downloading:
// buttonText = I18n.of(context).updateDownloading;
// break;
// case InstallState.saving:
// buttonText = I18n.of(context).updateSaving;
// break;
// case InstallState.installing:
// buttonText = I18n.of(context).updateInstalling;
// break;
// default:
// buttonText = I18n.of(context).error;
// }
// return BottomCard(
// child: Padding(
// padding: EdgeInsets.only(top: 13),
// child: Column(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Expanded(
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Row(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Expanded(
// child: ListTile(
// contentPadding: EdgeInsets.only(left: 8.0),
// title: Text(
// I18n.of(context).updateNewVersion,
// style: TextStyle(
// fontSize: 23,
// fontWeight: FontWeight.bold,
// ),
// ),
// subtitle: Text(
// app.user.sync.release.latestRelease.version,
// style: TextStyle(
// fontWeight: FontWeight.bold,
// fontSize: 18,
// ),
// ),
// ),
// ),
// ClipRRect(
// borderRadius: BorderRadius.circular(12.0),
// child: Image.asset(
// "assets/logo.png",
// width: 60,
// ),
// ),
// ],
// ),
// Padding(
// padding: EdgeInsets.all(8),
// child: Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Text(
// I18n.of(context).updateChanges + ":",
// style: TextStyle(fontSize: 18),
// ),
// Text(
// I18n.of(context).updateCurrentVersion +
// ": " +
// app.currentAppVersion,
// style:
// TextStyle(color: Colors.white.withAlpha(180)),
// ),
// ],
// ),
// ),
// Expanded(
// child: Padding(
// padding: EdgeInsets.all(8.0),
// child: Builder(builder: (context) {
// try {
// return Markdown(
// shrinkWrap: true,
// data: app.user.sync.release.latestRelease.notes,
// padding: EdgeInsets.all(0),
// physics: BouncingScrollPhysics(),
// styleSheet: MarkdownStyleSheet(
// p: TextStyle(
// fontSize: 15,
// color: app.settings.theme.textTheme
// .bodyText1.color,
// ),
// ),
// );
// } catch (e) {
// print(
// "ERROR: autoUpdater.dart failed to show markdown release notes: " +
// e.toString());
// return Text(
// app.user.sync.release.latestRelease.notes);
// }
// })),
// )
// ],
// ),
// ),
// Stack(
// children: [
// Row(
// mainAxisSize: MainAxisSize.max,
// mainAxisAlignment: MainAxisAlignment.end,
// crossAxisAlignment: CrossAxisAlignment.center,
// children: [
// IconButton(
// icon: Icon(FeatherIcons.helpCircle),
// onPressed: () {
// showDialog(
// context: context,
// builder: (context) => HelpDialog());
// })
// ],
// ),
// Center(
// child: MaterialButton(
// color: ThemeContext.filcGreen,
// elevation: 0,
// highlightElevation: 0,
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(45.0)),
// child: Padding(
// padding: EdgeInsets.symmetric(vertical: 9),
// child: Row(
// mainAxisSize: MainAxisSize.min,
// children: [
// if (displayProgress)
// Container(
// margin: EdgeInsets.only(right: 10),
// height: 19,
// width: 19,
// child: CircularProgressIndicator(
// value: progress,
// valueColor: AlwaysStoppedAnimation<Color>(
// Colors.white),
// strokeWidth: 3.2,
// ),
// ),
// Text(
// buttonText.toUpperCase(),
// style: TextStyle(
// fontSize: 17,
// fontWeight: FontWeight.bold,
// color: Colors.white),
// )
// ],
// ),
// ),
// onPressed: () {
// if (!buttonPressed)
// installUpdate(context, downloadCallback);
// buttonPressed = true;
// },
// ),
// ),
// ],
// ),
// ]),
// ),
// );
// }
// Future installUpdate(BuildContext context, Function updateDisplay) async {
// updateDisplay(null, true, InstallState.downloading);
// String dir = (await getApplicationDocumentsDirectory()).path;
// String latestVersion = app.user.sync.release.latestRelease.version;
// String filename = "filcnaplo-$latestVersion.apk";
// File apk = File("$dir/$filename");
// var httpClient = http.Client();
// var request = new http.Request(
// 'GET', Uri.parse(app.user.sync.release.latestRelease.url));
// var response = httpClient.send(request);
// List<List<int>> chunks = [];
// int downloaded = 0;
// response.asStream().listen((http.StreamedResponse r) {
// r.stream.listen((List<int> chunk) {
// // Display percentage of completion
// updateDisplay(
// downloaded / r.contentLength, true, InstallState.downloading);
// chunks.add(chunk);
// downloaded += chunk.length;
// }, onDone: () async {
// // Display percentage of completion
// updateDisplay(null, true, InstallState.saving);
// // Save the file
// final Uint8List bytes = Uint8List(r.contentLength);
// int offset = 0;
// for (List<int> chunk in chunks) {
// bytes.setRange(offset, offset + chunk.length, chunk);
// offset += chunk.length;
// }
// await apk.writeAsBytes(bytes);
// updateDisplay(null, true, InstallState.installing);
// if (mounted) {
// OpenFile.open(apk.path).then((result) {
// if (result.type != ResultType.done) {
// print("ERROR: installUpdate.openFile: " + result.message);
// ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar(
// message: I18n.of(context).error,
// color: Colors.red,
// ));
// }
// Navigator.pop(context);
// });
// }
// });
// });
// }
// }
// class HelpDialog extends StatelessWidget {
// @override
// Widget build(BuildContext context) {
// return Scaffold(
// backgroundColor: Colors.transparent,
// body: Container(
// decoration: BoxDecoration(
// color: app.settings.theme.backgroundColor,
// borderRadius: BorderRadius.circular(4.0),
// ),
// padding: EdgeInsets.symmetric(vertical: 15, horizontal: 10),
// margin: EdgeInsets.all(32.0),
// child: Column(children: [
// Padding(
// padding: EdgeInsets.all(8.0),
// child: Center(
// child: Icon(FeatherIcons.helpCircle),
// ),
// ),
// Expanded(
// child: Markdown(
// shrinkWrap: true,
// data: helpText,
// padding: EdgeInsets.all(0),
// physics: BouncingScrollPhysics(),
// styleSheet: MarkdownStyleSheet(
// p: TextStyle(
// fontSize: 15,
// color: Colors.white,
// ),
// ),
// ),
// ),
// MaterialButton(
// child: Text(I18n.of(context).dialogDone),
// onPressed: () {
// Navigator.of(context).pop();
// })
// ])));
// }
// }
// const String helpText =
// """A **FRISSÍTÉS** gombot megnyomva az app automatikusan letölti a GitHubról a legfrissebb Filc telepítőt.\\
// Ez kb. 50 MB adatforgalommal jár.\\
// A letöltött telepítőt ezután megnyitja az app.
// Telefonmárkától és Android verziótól függően nagyon különböző a telepítés folyamata, de ezekre figyelj:
// - Ha kérdezi a telefon, **engedélyezd a Filctől származó appok telepítését**, majd nyomd meg a vissza gombot.\\
// _(Újabb Android verziók)_
// - Ha szól a telefon hogy a külső appok telepítése biztonsági okokból tiltott, a megjelenő gombbal **ugorj a beállításokba és kapcsold be az Ismeretlen források lehetőséget.**\\
// _(Régi Android verziók)_
// A telepítés után újra megnyílik az app, immár a legfrissebb verzióval. Az indítás során törli a telepítéshez használt .apk fájlokat, így a tárhelyed miatt nem kell aggódnod.
// """;
// class AutoUpdateButton extends StatelessWidget {
// @override
// Widget build(BuildContext context) {
// return Padding(
// padding: EdgeInsets.only(bottom: 5.0),
// child: Container(
// margin: EdgeInsets.symmetric(horizontal: 14.0),
// child: MaterialButton(
// color: textColor(Theme.of(context).backgroundColor).withAlpha(25),
// elevation: 0,
// highlightElevation: 0,
// shape:
// RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
// child: ListTile(
// contentPadding: EdgeInsets.zero,
// leading: Icon(FeatherIcons.download, color: app.settings.appColor),
// title: Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Flexible(
// child: Text(
// I18n.of(context).updateAvailable,
// softWrap: false,
// overflow: TextOverflow.fade,
// )),
// Text(
// app.user.sync.release.latestRelease.version,
// style: TextStyle(
// color: app.settings.appColor,
// fontWeight: FontWeight.bold),
// )
// ],
// ),
// ),
// onPressed: () {
// showModalBottomSheet(
// context: context,
// backgroundColor: Colors.transparent,
// builder: (BuildContext context) => AutoUpdater(),
// );
// },
// ),
// ),
// );
// }
// }

View File

@@ -0,0 +1,106 @@
// import 'dart:io';
// import 'package:filcnaplo/data/context/app.dart';
// class ReleaseSync {
// Release latestRelease;
// bool isNew = false;
// Future sync() async {
// if (!Platform.isAndroid) return;
// var releasesJson = await app.user.kreta.getReleases();
// if (!app.settings.preUpdates) {
// for (Map r in releasesJson) {
// if (!r["prerelease"]) {
// latestRelease = Release.fromJson(r);
// break;
// }
// }
// } else {
// // List<Map> releases = [];
// latestRelease = Release.fromJson(releasesJson.first);
// }
// isNew = compareVersions(latestRelease.version, app.currentAppVersion);
// }
// bool compareVersions(String gitHub, String existing) {
// try {
// bool stableGitHub = false;
// List<String> gitHubPartsStrings = gitHub.split(RegExp(r"[.-]"));
// List<int> gitHubParts = [];
// for (String s in gitHubPartsStrings) {
// if (s.startsWith("beta")) {
// s = s.replaceAll("beta", "");
// } else if (s.startsWith("pre")) {
// //! pre versions have lower priority than beta
// s = s.replaceAll("pre", "");
// gitHubParts.add(0);
// } else {
// stableGitHub = true;
// }
// try {
// gitHubParts.add(int.parse(s));
// } catch (e) {
// print("ERROR: ReleaseSync.compareVersions: " + e.toString());
// }
// }
// if (stableGitHub) gitHubParts.add(1000);
// bool stableExisting = false;
// List<String> existingPartsStrings = existing.split(RegExp(r"[.-]"));
// List<int> existingParts = [];
// for (String s in existingPartsStrings) {
// if (s.startsWith("beta")) {
// s = s.replaceAll("beta", "");
// } else if (s.startsWith("pre")) {
// //! pre versions have lower priority than beta
// s = s.replaceAll("pre", "");
// existingParts.add(0);
// } else {
// stableExisting = true;
// }
// try {
// existingParts.add(int.parse(s));
// } catch (e) {
// print("ERROR: ReleaseSync.compareVersions: " + e.toString());
// }
// }
// // what even
// if (stableExisting) existingParts.add(1000);
// int i = 0;
// for (var gitHubPart in gitHubParts) {
// if (gitHubPart > existingParts[i])
// return true;
// else if (existingParts[i] > gitHubPart) return false;
// i++;
// }
// return false;
// } catch (e) {
// print("ERROR: ReleaseSync.compareVersions: " + e.toString());
// return false;
// }
// }
// }
// class Release {
// String version;
// String notes;
// String url;
// bool isExperimental;
// Release(this.version, this.notes, this.url, this.isExperimental);
// factory Release.fromJson(Map json) {
// List<Map> assets = [];
// json["assets"].forEach((json) {
// assets.add(json);
// });
// String url = assets[0]["browser_download_url"];
// return Release(json["tag_name"], json["body"], url, json["prerelease"]);
// }
// }

View File

@@ -0,0 +1,148 @@
// import 'package:filcnaplo/data/context/app.dart';
// import 'package:filcnaplo/data/models/lesson.dart';
// import 'package:filcnaplo/generated/i18n.dart';
// import 'package:filcnaplo/ui/common/custom_snackbar.dart';
// import 'package:filcnaplo/ui/pages/planner/timetable/day.dart';
// import 'package:filcnaplo/utils/format.dart';
// import 'package:filcnaplo/modules/printing/printerDebugScreen.dart';
// import 'package:flutter/material.dart';
// import 'package:flutter/services.dart' show rootBundle;
// import 'package:pdf/pdf.dart';
// import 'package:pdf/widgets.dart' as pw;
// import 'package:printing/printing.dart';
// import 'package:filcnaplo/ui/pages/planner/timetable/builder.dart';
// import 'package:flutter/foundation.dart';
// /*
// Author: daaniiieel
// Name: Timetable Printer (Experimental)
// Description: This module prints out the timetable for the selected user on the
// current week.
// */
// class TimetablePrinter {
// pw.Document build(
// BuildContext context, pw.Document pdf, List<Day> days, int min, int max) {
// List rows = <pw.TableRow>[];
// // build header row
// List<pw.Widget> headerChildren = <pw.Widget>[pw.Container()];
// days.forEach((day) => headerChildren.add(pw.Padding(
// padding: pw.EdgeInsets.all(5),
// child:
// pw.Center(child: pw.Text(weekdayStringShort(context, day.date.weekday))))));
// pw.TableRow headerRow = pw.TableRow(
// children: headerChildren,
// verticalAlignment: pw.TableCellVerticalAlignment.middle);
// rows.add(headerRow);
// // for each row
// for (int i = min; i < max; i++) {
// var children = <pw.Widget>[];
// var row = pw.TableRow(children: children);
// children.add(pw.Padding(
// padding: pw.EdgeInsets.all(5),
// child: pw.Center(child: pw.Text('$i. '))));
// days.forEach((Day day) {
// var lesson = day.lessons.firstWhere(
// (element) => element.lessonIndex != '+'
// ? int.parse(element.lessonIndex) == i
// : false,
// orElse: () => null);
// children.add(lesson != null
// ? pw.Padding(
// padding: pw.EdgeInsets.fromLTRB(5, 10, 5, 5),
// child: pw.Column(children: [
// pw.Text(lesson.name ?? lesson.subject.name),
// pw.Footer(
// leading: pw.Text(lesson.room),
// trailing: pw.Text(monogram(lesson.teacher))),
// ]))
// : pw.Padding(padding: pw.EdgeInsets.all(5)));
// });
// rows.add(row);
// }
// // add timetable to pdf
// pw.Table table = pw.Table(
// children: rows,
// border: pw.TableBorder.all(),
// defaultVerticalAlignment: pw.TableCellVerticalAlignment.middle,
// );
// // header and footer
// pw.Footer footer = pw.Footer(
// trailing: pw.Text('filcnaplo.hu'),
// margin: pw.EdgeInsets.only(top: 12.0),
// );
// String className = app.user.sync.student.student.className;
// pw.Footer header = pw.Footer(
// margin: pw.EdgeInsets.all(5),
// title: pw.Text(className, style: pw.TextStyle(fontSize: 30)),
// );
// pdf.addPage(pw.Page(
// pageFormat: PdfPageFormat.a4
// .landscape, // so the page looks normal both in portrait and landscape
// orientation: pw.PageOrientation.landscape,
// build: (pw.Context context) =>
// pw.Column(children: <pw.Widget>[header, table, footer])));
// return pdf;
// }
// void printPDF(final _scaffoldKey, BuildContext context) {
// // pdf theme (for unicode support)
// rootBundle.load("assets/Roboto-Regular.ttf").then((font) {
// pw.ThemeData myTheme = pw.ThemeData.withFont(base: pw.Font.ttf(font));
// pw.Document pdf = pw.Document(theme: myTheme);
// // sync indicator
// ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar(
// message: I18n.of(context).syncTimetable,
// ));
// // get a builder and build current week
// final timetableBuilder = TimetableBuilder();
// timetableBuilder.build(timetableBuilder.getCurrentWeek());
// int minLessonIndex = 1;
// int maxLessonIndex = 1;
// List<Day> weekDays = timetableBuilder.week.days;
// for (Day day in weekDays) {
// for (Lesson lesson in day.lessons) {
// if (lesson.lessonIndex == '+') {
// continue;
// }
// if (int.parse(lesson.lessonIndex) < minLessonIndex) {
// minLessonIndex = int.parse(lesson.lessonIndex);
// }
// if (int.parse(lesson.lessonIndex) > maxLessonIndex) {
// maxLessonIndex = int.parse(lesson.lessonIndex);
// }
// }
// }
// pdf = build(context, pdf, weekDays, minLessonIndex, maxLessonIndex);
// // print pdf
// if (kReleaseMode) {
// Printing.layoutPdf(onLayout: (format) => pdf.save()).then((success) {
// if (success)
// ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar(
// message: I18n.of(context).settingsExportExportTimetableSuccess,
// ));
// });
// } else {
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (c) =>
// PrintingDebugScreen((format) => Future.value(pdf.save()))));
// }
// });
// }
// }

View File

@@ -0,0 +1,17 @@
// import 'dart:typed_data';
// import 'package:flutter/material.dart';
// import 'package:printing/printing.dart';
// import 'package:pdf/pdf.dart';
// class PrintingDebugScreen extends StatelessWidget {
// final Future<Uint8List> Function(PdfPageFormat) builder;
// PrintingDebugScreen(
// this.builder,
// );
// @override
// Widget build(BuildContext context) {
// return Scaffold(body: Center(child: PdfPreview(build: this.builder)));
// }
// }

145
filcnaplo/lib/theme.dart Normal file
View File

@@ -0,0 +1,145 @@
import 'package:filcnaplo/models/settings.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class AppTheme {
// Dev note: All of these could be constant variables, but this is better for
// development (you don't have to hot-restart)
static const String _fontFamily = "Montserrat";
// Light Theme
static ThemeData lightTheme(BuildContext context) {
var lightColors = LightAppColors();
Color accent = accentColorMap[Provider.of<SettingsProvider>(context, listen: false).accentColor] ?? Color(0);
return ThemeData(
brightness: Brightness.light,
fontFamily: _fontFamily,
scaffoldBackgroundColor: lightColors.background,
backgroundColor: lightColors.highlight,
primaryColor: lightColors.filc,
dividerColor: Color(0),
colorScheme: ColorScheme.fromSwatch(
accentColor: accent,
backgroundColor: lightColors.background,
brightness: Brightness.light,
cardColor: lightColors.highlight,
errorColor: lightColors.red,
primaryColorDark: lightColors.filc,
primarySwatch: Colors.teal,
),
shadowColor: lightColors.shadow,
appBarTheme: AppBarTheme(backgroundColor: lightColors.background),
indicatorColor: accent,
iconTheme: IconThemeData(color: lightColors.text.withOpacity(.75)));
}
// Dark Theme
static ThemeData darkTheme(BuildContext context) {
var darkColors = DarkAppColors();
Color accent = accentColorMap[Provider.of<SettingsProvider>(context, listen: false).accentColor] ?? Color(0);
return ThemeData(
brightness: Brightness.dark,
fontFamily: _fontFamily,
scaffoldBackgroundColor: darkColors.background,
backgroundColor: darkColors.highlight,
primaryColor: darkColors.filc,
dividerColor: Color(0),
colorScheme: ColorScheme.fromSwatch(
accentColor: accent,
backgroundColor: darkColors.background,
brightness: Brightness.dark,
cardColor: darkColors.highlight,
errorColor: darkColors.red,
primaryColorDark: darkColors.filc,
primarySwatch: Colors.teal,
),
shadowColor: darkColors.shadow,
appBarTheme: AppBarTheme(backgroundColor: darkColors.background),
indicatorColor: accent,
iconTheme: IconThemeData(color: darkColors.text.withOpacity(.75)));
}
}
class AppColors {
static ThemeAppColors of(BuildContext context) {
return Theme.of(context).brightness == Brightness.light ? LightAppColors() : DarkAppColors();
}
}
enum AccentColor { filc, blue, green, lime, yellow, orange, red, pink, purple }
Map<AccentColor, Color> accentColorMap = {
AccentColor.filc: Color(0xff20AC9B),
AccentColor.blue: Colors.blue.shade300,
AccentColor.green: Colors.green.shade300,
AccentColor.lime: Colors.lime.shade300,
AccentColor.yellow: Colors.yellow.shade300,
AccentColor.orange: Colors.deepOrange.shade300,
AccentColor.red: Colors.red.shade300,
AccentColor.pink: Colors.pink.shade300,
AccentColor.purple: Colors.purple.shade300,
};
abstract class ThemeAppColors {
final Color shadow = Color(0);
final Color text = Color(0);
final Color background = Color(0);
final Color highlight = Color(0);
final Color red = Color(0);
final Color orange = Color(0);
final Color yellow = Color(0);
final Color green = Color(0);
final Color filc = Color(0);
final Color teal = Color(0);
final Color blue = Color(0);
final Color indigo = Color(0);
final Color purple = Color(0);
final Color pink = Color(0);
}
class LightAppColors implements ThemeAppColors {
final shadow = Color(0xffE8E8E8);
final text = Colors.black;
final background = Color(0xffF4F9FF);
final highlight = Color(0xffFFFFFF);
final red = Color(0xffFF3B30);
final orange = Color(0xffFF9500);
final yellow = Color(0xffFFCC00);
final green = Color(0xff34C759);
final filc = Color(0xff247665);
final teal = Color(0xff5AC8FA);
final blue = Color(0xff007AFF);
final indigo = Color(0xff5856D6);
final purple = Color(0xffAF52DE);
final pink = Color(0xffFF2D55);
}
class DarkAppColors implements ThemeAppColors {
final shadow = Color(0);
final text = Colors.white;
final background = Color(0xff000000);
final highlight = Color(0xff141516);
final red = Color(0xffFF453A);
final orange = Color(0xffFF9F0A);
final yellow = Color(0xffFFD60A);
final green = Color(0xff32D74B);
final filc = Color(0xff29826F);
final teal = Color(0xff64D2FF);
final blue = Color(0xff0A84FF);
final indigo = Color(0xff5E5CE6);
final purple = Color(0xffBF5AF2);
final pink = Color(0xffFF375F);
}
class ThemeModeObserver extends ChangeNotifier {
ThemeMode _themeMode;
ThemeMode get themeMode => _themeMode;
ThemeModeObserver({ThemeMode initialTheme = ThemeMode.system}) : _themeMode = initialTheme;
void changeTheme(ThemeMode mode) {
_themeMode = mode;
notifyListeners();
}
}

View File

@@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
class ColorUtils {
static Color stringToColor(String str) {
int hash = 0;
for (var i = 0; i < str.length; i++) {
hash = str.codeUnitAt(i) + ((hash << 5) - hash);
}
return HSLColor.fromAHSL(1, hash % 360, .8, .75).toColor();
}
static Color foregroundColor(Color color) => color.computeLuminance() >= .5 ? Colors.black : Colors.white;
}

View File

@@ -0,0 +1,55 @@
import 'package:flutter/widgets.dart';
import 'package:intl/intl.dart';
import 'package:i18n_extension/i18n_widget.dart';
import 'package:html/parser.dart';
extension StringFormatUtils on String {
String specialChars() => this
.replaceAll("é", "e")
.replaceAll("á", "a")
.replaceAll("ó", "o")
.replaceAll("ő", "o")
.replaceAll("ö", "o")
.replaceAll("ú", "u")
.replaceAll("ű", "u")
.replaceAll("ü", "u")
.replaceAll("í", "i");
String capital() => this.length > 0 ? this[0].toUpperCase() + this.substring(1) : "";
String capitalize() => this.split(" ").map((w) => this.capital()).join(" ");
String escapeHtml() {
String htmlString = this;
htmlString = htmlString.replaceAll("\r", "");
htmlString = htmlString.replaceAll(RegExp(r'<br ?/?>'), "\n");
htmlString = htmlString.replaceAll(RegExp(r'<p ?>'), "");
htmlString = htmlString.replaceAll(RegExp(r'</p ?>'), "\n");
var document = parse(htmlString);
return document.body?.text.trim() ?? "";
}
}
extension DateFormatUtils on DateTime {
String format(BuildContext context, {bool timeOnly = false, bool weekday = false}) {
// Time only
if (timeOnly) return DateFormat("HH:mm").format(this);
DateTime now = DateTime.now();
if (this.difference(now).inDays == 0) {
if (this.hour == 0 && this.minute == 0 && this.second == 0) return "Today"; // TODO: i18n
return DateFormat("HH:mm").format(this);
}
if (this.difference(now).inDays == 1) return "Yesterday"; // TODO: i18n
String formatString;
if (this.year == now.year)
formatString = "MMM dd.";
else
formatString = "yy/MM/dd";
if (weekday) formatString += " (EEEE)";
return DateFormat(formatString, I18n.of(context).locale.toString()).format(this);
}
}

View File

@@ -0,0 +1,18 @@
import 'dart:convert';
class JwtUtils {
static String? getNameFromJWT(String jwt) {
var parts = jwt.split(".");
if (parts.length != 3) return null;
if (parts[1].length % 4 == 2) {
parts[1] += "==";
} else if (parts[1].length % 4 == 3) {
parts[1] += "=";
}
var payload = utf8.decode(base64Url.decode(parts[1]));
var jwtData = jsonDecode(payload);
return jwtData["name"];
}
}