init
This commit is contained in:
85
filcnaplo/lib/api/client.dart
Normal file
85
filcnaplo/lib/api/client.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
110
filcnaplo/lib/api/login.dart
Normal file
110
filcnaplo/lib/api/login.dart
Normal 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;
|
||||
}
|
||||
25
filcnaplo/lib/api/nonce.dart
Normal file
25
filcnaplo/lib/api/nonce.dart
Normal 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",
|
||||
};
|
||||
}
|
||||
}
|
||||
20
filcnaplo/lib/api/providers/database_provider.dart
Normal file
20
filcnaplo/lib/api/providers/database_provider.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
82
filcnaplo/lib/api/providers/news_provider.dart
Normal file
82
filcnaplo/lib/api/providers/news_provider.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
41
filcnaplo/lib/api/providers/update_provider.dart
Normal file
41
filcnaplo/lib/api/providers/update_provider.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
41
filcnaplo/lib/api/providers/user_provider.dart
Normal file
41
filcnaplo/lib/api/providers/user_provider.dart
Normal 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
120
filcnaplo/lib/app.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
72
filcnaplo/lib/database/init.dart
Normal file
72
filcnaplo/lib/database/init.dart
Normal 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");
|
||||
}
|
||||
}
|
||||
114
filcnaplo/lib/database/query.dart
Normal file
114
filcnaplo/lib/database/query.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
85
filcnaplo/lib/database/store.dart
Normal file
85
filcnaplo/lib/database/store.dart
Normal 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]);
|
||||
}
|
||||
}
|
||||
29
filcnaplo/lib/database/struct.dart
Normal file
29
filcnaplo/lib/database/struct.dart
Normal 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(",");
|
||||
}
|
||||
}
|
||||
30
filcnaplo/lib/helpers/attachment_helper.dart
Normal file
30
filcnaplo/lib/helpers/attachment_helper.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
25
filcnaplo/lib/helpers/average_helper.dart
Normal file
25
filcnaplo/lib/helpers/average_helper.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
14
filcnaplo/lib/helpers/share_helper.dart
Normal file
14
filcnaplo/lib/helpers/share_helper.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
36
filcnaplo/lib/helpers/storage_helper.dart
Normal file
36
filcnaplo/lib/helpers/storage_helper.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
46
filcnaplo/lib/helpers/subject_icon.dart
Normal file
46
filcnaplo/lib/helpers/subject_icon.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
65
filcnaplo/lib/helpers/update_helper.dart
Normal file
65
filcnaplo/lib/helpers/update_helper.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
10
filcnaplo/lib/icons/filc_icons.dart
Normal file
10
filcnaplo/lib/icons/filc_icons.dart
Normal 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
71
filcnaplo/lib/main.dart
Normal 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();
|
||||
});
|
||||
}
|
||||
41
filcnaplo/lib/models/config.dart
Normal file
41
filcnaplo/lib/models/config.dart
Normal 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();
|
||||
}
|
||||
31
filcnaplo/lib/models/news.dart
Normal file
31
filcnaplo/lib/models/news.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
129
filcnaplo/lib/models/release.dart
Normal file
129
filcnaplo/lib/models/release.dart
Normal 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"];
|
||||
}
|
||||
246
filcnaplo/lib/models/settings.dart
Normal file
246
filcnaplo/lib/models/settings.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
64
filcnaplo/lib/models/user.dart
Normal file
64
filcnaplo/lib/models/user.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
376
filcnaplo/lib/modules/autoupdate/old/autoUpdater.dart
Normal file
376
filcnaplo/lib/modules/autoupdate/old/autoUpdater.dart
Normal 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(),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
106
filcnaplo/lib/modules/autoupdate/old/releaseSync.dart
Normal file
106
filcnaplo/lib/modules/autoupdate/old/releaseSync.dart
Normal 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"]);
|
||||
// }
|
||||
// }
|
||||
148
filcnaplo/lib/modules/printing/main.dart
Normal file
148
filcnaplo/lib/modules/printing/main.dart
Normal 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()))));
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
17
filcnaplo/lib/modules/printing/printerDebugScreen.dart
Normal file
17
filcnaplo/lib/modules/printing/printerDebugScreen.dart
Normal 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
145
filcnaplo/lib/theme.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
14
filcnaplo/lib/utils/color.dart
Normal file
14
filcnaplo/lib/utils/color.dart
Normal 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;
|
||||
}
|
||||
55
filcnaplo/lib/utils/format.dart
Normal file
55
filcnaplo/lib/utils/format.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
18
filcnaplo/lib/utils/jwt.dart
Normal file
18
filcnaplo/lib/utils/jwt.dart
Normal 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"];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user