changed everything from filcnaplo to refilc finally
This commit is contained in:
357
refilc/lib/api/client.dart
Normal file
357
refilc/lib/api/client.dart
Normal file
@@ -0,0 +1,357 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:refilc/models/ad.dart';
|
||||
import 'package:refilc/models/config.dart';
|
||||
import 'package:refilc/models/news.dart';
|
||||
import 'package:refilc/models/release.dart';
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/models/shared_theme.dart';
|
||||
import 'package:refilc/models/supporter.dart';
|
||||
import 'package:refilc_kreta_api/models/school.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
|
||||
class FilcAPI {
|
||||
// API base
|
||||
static const baseUrl = "https://api.refilc.hu";
|
||||
|
||||
// Public API
|
||||
static const schoolList = "$baseUrl/v1/public/school-list";
|
||||
static const news = "$baseUrl/v1/public/news";
|
||||
static const supporters = "$baseUrl/v1/public/supporters";
|
||||
|
||||
// Private API
|
||||
static const ads = "$baseUrl/v1/private/ads";
|
||||
static const config = "$baseUrl/v1/private/config";
|
||||
static const reportApi = "$baseUrl/v1/private/crash-report";
|
||||
static const rfPlus = "$baseUrl/v2/rf-plus";
|
||||
static const plusAuthLogin = "$rfPlus/auth/login";
|
||||
static const plusAuthCallback = "$rfPlus/auth/callback";
|
||||
static const plusActivation = "$rfPlus/activate";
|
||||
static const plusScopes = "$rfPlus/scopes";
|
||||
|
||||
// Updates
|
||||
static const repo = "refilc/naplo";
|
||||
static const releases = "https://api.github.com/repos/$repo/releases";
|
||||
|
||||
// Share API
|
||||
static const themeShare = "$baseUrl/v2/shared/theme/add";
|
||||
static const themeGet = "$baseUrl/v2/shared/theme/get";
|
||||
static const allThemes = "$themeGet/all";
|
||||
static const themeByID = "$themeGet/";
|
||||
|
||||
static const gradeColorsShare = "$baseUrl/v2/shared/grade-colors/add";
|
||||
static const gradeColorsGet = "$baseUrl/v2/shared/grade-colors/get";
|
||||
static const allGradeColors = "$gradeColorsGet/all";
|
||||
static const gradeColorsByID = "$gradeColorsGet/";
|
||||
|
||||
static Future<bool> checkConnectivity() async =>
|
||||
(await Connectivity().checkConnectivity()) != ConnectivityResult.none;
|
||||
|
||||
static Future<List<School>?> getSchools() async {
|
||||
try {
|
||||
http.Response res = await http.get(Uri.parse(schoolList));
|
||||
|
||||
if (res.statusCode == 200) {
|
||||
List<School> schools = (jsonDecode(res.body) as List)
|
||||
.cast<Map>()
|
||||
.map((json) => School.fromJson(json))
|
||||
.toList();
|
||||
schools.add(School(
|
||||
city: "Stockholm",
|
||||
instituteCode: "refilc-test-sweden",
|
||||
name: "reFilc Test SE - Leo Ekström High School",
|
||||
));
|
||||
schools.add(School(
|
||||
city: "Madrid",
|
||||
instituteCode: "refilc-test-spain",
|
||||
name: "reFilc Test ES - Emilio Obrero University",
|
||||
));
|
||||
return schools;
|
||||
} else {
|
||||
throw "HTTP ${res.statusCode}: ${res.body}";
|
||||
}
|
||||
} on Exception catch (error, stacktrace) {
|
||||
log("ERROR: FilcAPI.getSchools: $error $stacktrace");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Future<Config?> getConfig(SettingsProvider settings) async {
|
||||
final userAgent = SettingsProvider.defaultSettings().config.userAgent;
|
||||
|
||||
Map<String, String> headers = {
|
||||
"x-filc-id": settings.xFilcId,
|
||||
"user-agent": userAgent,
|
||||
// platform things
|
||||
"rf-platform": Platform.operatingSystem,
|
||||
"rf-platform-version": Platform.operatingSystemVersion,
|
||||
"rf-app-version":
|
||||
const String.fromEnvironment("APPVER", defaultValue: "?"),
|
||||
"rf-uinid": settings.xFilcId,
|
||||
};
|
||||
|
||||
log("[CONFIG] x-filc-id: \"${settings.xFilcId}\"");
|
||||
log("[CONFIG] user-agent: \"$userAgent\"");
|
||||
|
||||
try {
|
||||
http.Response res = await http.get(Uri.parse(config), headers: headers);
|
||||
|
||||
if (res.statusCode == 200) {
|
||||
if (kDebugMode) {
|
||||
print(jsonDecode(res.body));
|
||||
}
|
||||
return Config.fromJson(jsonDecode(res.body));
|
||||
} else if (res.statusCode == 429) {
|
||||
res = await http.get(Uri.parse(config));
|
||||
if (res.statusCode == 200) return Config.fromJson(jsonDecode(res.body));
|
||||
}
|
||||
throw "HTTP ${res.statusCode}: ${res.body}";
|
||||
} on Exception catch (error, stacktrace) {
|
||||
log("ERROR: FilcAPI.getConfig: $error $stacktrace");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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}";
|
||||
}
|
||||
} on Exception catch (error, stacktrace) {
|
||||
log("ERROR: FilcAPI.getNews: $error $stacktrace");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Future<Supporters?> getSupporters() async {
|
||||
try {
|
||||
http.Response res = await http.get(Uri.parse(supporters));
|
||||
|
||||
if (res.statusCode == 200) {
|
||||
return Supporters.fromJson(jsonDecode(res.body));
|
||||
} else {
|
||||
throw "HTTP ${res.statusCode}: ${res.body}";
|
||||
}
|
||||
} on Exception catch (error, stacktrace) {
|
||||
log("ERROR: FilcAPI.getSupporters: $error $stacktrace");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Future<List<Ad>?> getAds() async {
|
||||
try {
|
||||
http.Response res = await http.get(Uri.parse(ads));
|
||||
|
||||
if (res.statusCode == 200) {
|
||||
return (jsonDecode(res.body) as List)
|
||||
.cast<Map>()
|
||||
.map((e) => Ad.fromJson(e))
|
||||
.toList();
|
||||
} else {
|
||||
throw "HTTP ${res.statusCode}: ${res.body}";
|
||||
}
|
||||
} on Exception catch (error, stacktrace) {
|
||||
log("ERROR: FilcAPI.getAds: $error $stacktrace");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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}";
|
||||
}
|
||||
} on Exception catch (error, stacktrace) {
|
||||
log("ERROR: FilcAPI.getReleases: $error $stacktrace");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Future<http.StreamedResponse?> downloadRelease(
|
||||
ReleaseDownload release) {
|
||||
try {
|
||||
var client = http.Client();
|
||||
var request = http.Request('GET', Uri.parse(release.url));
|
||||
return client.send(request);
|
||||
} on Exception catch (error, stacktrace) {
|
||||
log("ERROR: FilcAPI.downloadRelease: $error $stacktrace");
|
||||
return Future.value(null);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> sendReport(ErrorReport report) async {
|
||||
try {
|
||||
Map body = {
|
||||
"os": report.os,
|
||||
"version": report.version,
|
||||
"error": report.error,
|
||||
"stack_trace": report.stack,
|
||||
};
|
||||
|
||||
var client = http.Client();
|
||||
|
||||
http.Response res = await client.post(
|
||||
Uri.parse(reportApi),
|
||||
body: body,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
);
|
||||
|
||||
if (res.statusCode != 200) {
|
||||
throw "HTTP ${res.statusCode}: ${res.body}";
|
||||
}
|
||||
} on Exception catch (error, stacktrace) {
|
||||
log("ERROR: FilcAPI.sendReport: $error $stacktrace");
|
||||
}
|
||||
}
|
||||
|
||||
// sharing
|
||||
static Future<void> addSharedTheme(SharedTheme theme) async {
|
||||
try {
|
||||
theme.json.remove('json');
|
||||
theme.json['is_public'] = theme.isPublic.toString();
|
||||
theme.json['background_color'] = theme.backgroundColor.value.toString();
|
||||
theme.json['panels_color'] = theme.panelsColor.value.toString();
|
||||
theme.json['accent_color'] = theme.accentColor.value.toString();
|
||||
theme.json['icon_color'] = theme.iconColor.value.toString();
|
||||
theme.json['shadow_effect'] = theme.shadowEffect.toString();
|
||||
|
||||
// set theme mode or remove if unneccessary
|
||||
switch (theme.themeMode) {
|
||||
case ThemeMode.dark:
|
||||
theme.json['theme_mode'] = 'dark';
|
||||
break;
|
||||
case ThemeMode.light:
|
||||
theme.json['theme_mode'] = 'light';
|
||||
break;
|
||||
default:
|
||||
theme.json.remove('theme_mode');
|
||||
break;
|
||||
}
|
||||
|
||||
// set linked grade colors
|
||||
theme.json['grade_colors_id'] = theme.gradeColors.id;
|
||||
|
||||
http.Response res = await http.post(
|
||||
Uri.parse(themeShare),
|
||||
body: theme.json,
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
);
|
||||
|
||||
if (res.statusCode != 201) {
|
||||
throw "HTTP ${res.statusCode}: ${res.body}";
|
||||
}
|
||||
|
||||
log('Shared theme successfully with ID: ${theme.id}');
|
||||
} on Exception catch (error, stacktrace) {
|
||||
log("ERROR: FilcAPI.addSharedTheme: $error $stacktrace");
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Map?> getSharedTheme(String id) async {
|
||||
try {
|
||||
http.Response res = await http.get(Uri.parse(themeByID + id));
|
||||
|
||||
if (res.statusCode == 200) {
|
||||
return (jsonDecode(res.body) as Map);
|
||||
} else {
|
||||
throw "HTTP ${res.statusCode}: ${res.body}";
|
||||
}
|
||||
} on Exception catch (error, stacktrace) {
|
||||
log("ERROR: FilcAPI.getSharedTheme: $error $stacktrace");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Future<List?> getAllSharedThemes(int count) async {
|
||||
try {
|
||||
http.Response res = await http.get(Uri.parse(allThemes));
|
||||
|
||||
if (res.statusCode == 200) {
|
||||
return (jsonDecode(res.body) as List);
|
||||
} else {
|
||||
throw "HTTP ${res.statusCode}: ${res.body}";
|
||||
}
|
||||
} on Exception catch (error, stacktrace) {
|
||||
log("ERROR: FilcAPI.getAllSharedThemes: $error $stacktrace");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Future<void> addSharedGradeColors(
|
||||
SharedGradeColors gradeColors) async {
|
||||
try {
|
||||
gradeColors.json.remove('json');
|
||||
gradeColors.json['is_public'] = gradeColors.isPublic.toString();
|
||||
gradeColors.json['five_color'] = gradeColors.fiveColor.value.toString();
|
||||
gradeColors.json['four_color'] = gradeColors.fourColor.value.toString();
|
||||
gradeColors.json['three_color'] = gradeColors.threeColor.value.toString();
|
||||
gradeColors.json['two_color'] = gradeColors.twoColor.value.toString();
|
||||
gradeColors.json['one_color'] = gradeColors.oneColor.value.toString();
|
||||
|
||||
http.Response res = await http.post(
|
||||
Uri.parse(gradeColorsShare),
|
||||
body: gradeColors.json,
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
);
|
||||
|
||||
if (res.statusCode != 201) {
|
||||
throw "HTTP ${res.statusCode}: ${res.body}";
|
||||
}
|
||||
|
||||
log('Shared grade colors successfully with ID: ${gradeColors.id}');
|
||||
} on Exception catch (error, stacktrace) {
|
||||
log("ERROR: FilcAPI.addSharedGradeColors: $error $stacktrace");
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Map?> getSharedGradeColors(String id) async {
|
||||
try {
|
||||
http.Response res = await http.get(Uri.parse(gradeColorsByID + id));
|
||||
|
||||
if (res.statusCode == 200) {
|
||||
return (jsonDecode(res.body) as Map);
|
||||
} else {
|
||||
throw "HTTP ${res.statusCode}: ${res.body}";
|
||||
}
|
||||
} on Exception catch (error, stacktrace) {
|
||||
log("ERROR: FilcAPI.getSharedGradeColors: $error $stacktrace");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorReport {
|
||||
String stack;
|
||||
String os;
|
||||
String version;
|
||||
String error;
|
||||
|
||||
ErrorReport({
|
||||
required this.stack,
|
||||
required this.os,
|
||||
required this.version,
|
||||
required this.error,
|
||||
});
|
||||
}
|
||||
191
refilc/lib/api/login.dart
Normal file
191
refilc/lib/api/login.dart
Normal file
@@ -0,0 +1,191 @@
|
||||
// ignore_for_file: avoid_print, use_build_context_synchronously
|
||||
|
||||
import 'package:refilc/utils/jwt.dart';
|
||||
import 'package:refilc_kreta_api/models/school.dart';
|
||||
import 'package:refilc_kreta_api/providers/absence_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/event_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/exam_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/grade_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/homework_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/message_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/note_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/timetable_provider.dart';
|
||||
import 'package:refilc/api/providers/user_provider.dart';
|
||||
import 'package:refilc/api/providers/database_provider.dart';
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/models/user.dart';
|
||||
import 'package:refilc_kreta_api/client/api.dart';
|
||||
import 'package:refilc_kreta_api/client/client.dart';
|
||||
import 'package:refilc_kreta_api/models/student.dart';
|
||||
import 'package:refilc_kreta_api/models/week.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:refilc/api/nonce.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
enum LoginState {
|
||||
missingFields,
|
||||
invalidGrant,
|
||||
failed,
|
||||
normal,
|
||||
inProgress,
|
||||
success,
|
||||
}
|
||||
|
||||
Nonce getNonce(String nonce, String username, String instituteCode) {
|
||||
Nonce nonceEncoder = Nonce(
|
||||
key: [98, 97, 83, 115, 120, 79, 119, 108, 85, 49, 106, 77], nonce: nonce);
|
||||
nonceEncoder
|
||||
.encode(instituteCode.toUpperCase() + nonce + username.toUpperCase());
|
||||
|
||||
return nonceEncoder;
|
||||
}
|
||||
|
||||
Future loginAPI({
|
||||
required String username,
|
||||
required String password,
|
||||
required String instituteCode,
|
||||
required BuildContext context,
|
||||
void Function(User)? onLogin,
|
||||
void Function()? onSuccess,
|
||||
}) async {
|
||||
Future testLogin(School school) async {
|
||||
var user = User(
|
||||
username: username,
|
||||
password: password,
|
||||
instituteCode: instituteCode,
|
||||
name: 'Teszt Lajos',
|
||||
student: Student(
|
||||
birth: DateTime.now(),
|
||||
id: const Uuid().v4(),
|
||||
name: 'Teszt Lajos',
|
||||
school: school,
|
||||
yearId: '1',
|
||||
parents: ['Teszt András', 'Teszt Linda'],
|
||||
json: {"a": "b"},
|
||||
address: '1117 Budapest, Gábor Dénes utca 4.',
|
||||
),
|
||||
role: Role.parent,
|
||||
);
|
||||
|
||||
if (onLogin != null) onLogin(user);
|
||||
|
||||
// store test user in db
|
||||
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);
|
||||
|
||||
if (onSuccess != null) onSuccess();
|
||||
|
||||
return LoginState.success;
|
||||
}
|
||||
|
||||
// if institute matches one of test things do test login
|
||||
if (instituteCode == 'refilc-test-sweden') {
|
||||
School school = School(
|
||||
city: "Stockholm",
|
||||
instituteCode: "refilc-test-sweden",
|
||||
name: "reFilc Test SE - Leo Ekström High School",
|
||||
);
|
||||
|
||||
await testLogin(school);
|
||||
} else if (instituteCode == 'refilc-test-spain') {
|
||||
School school = School(
|
||||
city: "Madrid",
|
||||
instituteCode: "refilc-test-spain",
|
||||
name: "reFilc Test ES - Emilio Obrero University",
|
||||
);
|
||||
|
||||
await testLogin(school);
|
||||
} else {
|
||||
// normal login from here
|
||||
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(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));
|
||||
Student student = Student.fromJson(studentJson!);
|
||||
var user = User(
|
||||
username: username,
|
||||
password: password,
|
||||
instituteCode: instituteCode,
|
||||
name: student.name,
|
||||
student: student,
|
||||
role: JwtUtils.getRoleFromJWT(res["access_token"])!,
|
||||
);
|
||||
|
||||
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 Future.wait([
|
||||
Provider.of<GradeProvider>(context, listen: false).fetch(),
|
||||
Provider.of<TimetableProvider>(context, listen: false)
|
||||
.fetch(week: Week.current()),
|
||||
Provider.of<ExamProvider>(context, listen: false).fetch(),
|
||||
Provider.of<HomeworkProvider>(context, listen: false).fetch(),
|
||||
Provider.of<MessageProvider>(context, listen: false).fetchAll(),
|
||||
Provider.of<MessageProvider>(context, listen: false)
|
||||
.fetchAllRecipients(),
|
||||
Provider.of<NoteProvider>(context, listen: false).fetch(),
|
||||
Provider.of<EventProvider>(context, listen: false).fetch(),
|
||||
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
refilc/lib/api/nonce.dart
Normal file
25
refilc/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": "v2",
|
||||
};
|
||||
}
|
||||
}
|
||||
29
refilc/lib/api/providers/ad_provider.dart
Normal file
29
refilc/lib/api/providers/ad_provider.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
import 'package:refilc/api/client.dart';
|
||||
import 'package:refilc/models/ad.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AdProvider extends ChangeNotifier {
|
||||
// private
|
||||
late List<Ad> _ads;
|
||||
bool get available => _ads.isNotEmpty;
|
||||
|
||||
// public
|
||||
List<Ad> get ads => _ads;
|
||||
|
||||
AdProvider({
|
||||
List<Ad> initialAds = const [],
|
||||
required BuildContext context,
|
||||
}) {
|
||||
_ads = List.castFrom(initialAds);
|
||||
}
|
||||
|
||||
Future<void> fetch() async {
|
||||
_ads = await FilcAPI.getAds() ?? [];
|
||||
_ads.sort((a, b) => -a.date.compareTo(b.date));
|
||||
|
||||
// check for new ads
|
||||
if (_ads.isNotEmpty) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
29
refilc/lib/api/providers/database_provider.dart
Normal file
29
refilc/lib/api/providers/database_provider.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:refilc/database/query.dart';
|
||||
import 'package:refilc/database/store.dart';
|
||||
// ignore: depend_on_referenced_packages
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
|
||||
class DatabaseProvider {
|
||||
// late Database _database;
|
||||
late DatabaseQuery query;
|
||||
late UserDatabaseQuery userQuery;
|
||||
late DatabaseStore store;
|
||||
late UserDatabaseStore userStore;
|
||||
|
||||
Future<void> init() async {
|
||||
Database db;
|
||||
|
||||
if (Platform.isLinux || Platform.isWindows) {
|
||||
db = await databaseFactoryFfi.openDatabase("app.db");
|
||||
} else {
|
||||
db = await openDatabase("app.db");
|
||||
}
|
||||
|
||||
query = DatabaseQuery(db: db);
|
||||
store = DatabaseStore(db: db);
|
||||
userQuery = UserDatabaseQuery(db: db);
|
||||
userStore = UserDatabaseStore(db: db);
|
||||
}
|
||||
}
|
||||
299
refilc/lib/api/providers/live_card_provider.dart
Normal file
299
refilc/lib/api/providers/live_card_provider.dart
Normal file
@@ -0,0 +1,299 @@
|
||||
// ignore_for_file: no_leading_underscores_for_local_identifiers
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:refilc/helpers/subject.dart';
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc_kreta_api/models/lesson.dart';
|
||||
import 'package:refilc_kreta_api/models/week.dart';
|
||||
import 'package:refilc/utils/format.dart';
|
||||
import 'package:refilc_kreta_api/providers/timetable_provider.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:live_activities/live_activities.dart';
|
||||
import 'package:refilc_mobile_ui/pages/home/live_card/live_card.i18n.dart';
|
||||
|
||||
enum LiveCardState {
|
||||
empty,
|
||||
duringLesson,
|
||||
duringBreak,
|
||||
morning,
|
||||
afternoon,
|
||||
night,
|
||||
summary
|
||||
}
|
||||
|
||||
class LiveCardProvider extends ChangeNotifier {
|
||||
Lesson? currentLesson;
|
||||
Lesson? nextLesson;
|
||||
Lesson? prevLesson;
|
||||
List<Lesson>? nextLessons;
|
||||
|
||||
LiveCardState currentState = LiveCardState.empty;
|
||||
late Timer _timer;
|
||||
late final TimetableProvider _timetable;
|
||||
late final SettingsProvider _settings;
|
||||
|
||||
late Duration _delay;
|
||||
|
||||
final _liveActivitiesPlugin = LiveActivities();
|
||||
String? _latestActivityId;
|
||||
Map<String, String> _lastActivity = {};
|
||||
|
||||
bool _hasCheckedTimetable = false;
|
||||
|
||||
LiveCardProvider({
|
||||
required TimetableProvider timetable,
|
||||
required SettingsProvider settings,
|
||||
}) : _timetable = timetable,
|
||||
_settings = settings {
|
||||
if (Platform.isIOS) {
|
||||
_liveActivitiesPlugin.areActivitiesEnabled().then((value) {
|
||||
// Console log
|
||||
if (kDebugMode) {
|
||||
print("iOS LiveActivity enabled: $value");
|
||||
}
|
||||
|
||||
if (value) {
|
||||
_liveActivitiesPlugin.init(appGroupId: "group.refilc2.livecard");
|
||||
|
||||
_liveActivitiesPlugin.getAllActivitiesIds().then((value) {
|
||||
_latestActivityId = value.isNotEmpty ? value.first : null;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) => update());
|
||||
_delay = settings.bellDelayEnabled
|
||||
? Duration(seconds: settings.bellDelay)
|
||||
: Duration.zero;
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer.cancel();
|
||||
if (Platform.isIOS) {
|
||||
_liveActivitiesPlugin.areActivitiesEnabled().then((value) {
|
||||
if (value) {
|
||||
if (_latestActivityId != null) {
|
||||
_liveActivitiesPlugin.endActivity(_latestActivityId!);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// Debugging
|
||||
static DateTime _now() {
|
||||
// return DateTime(2023, 9, 27, 9, 30);
|
||||
return DateTime.now();
|
||||
}
|
||||
|
||||
String getFloorDifference() {
|
||||
final prevFloor = prevLesson!.getFloor();
|
||||
final nextFloor = nextLesson!.getFloor();
|
||||
if (prevFloor == null || nextFloor == null || prevFloor == nextFloor) {
|
||||
return "to room";
|
||||
}
|
||||
if (nextFloor == 0) {
|
||||
return "ground floor";
|
||||
}
|
||||
if (nextFloor > prevFloor) {
|
||||
return "up floor";
|
||||
} else {
|
||||
return "down floor";
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, String> toMap() {
|
||||
switch (currentState) {
|
||||
case LiveCardState.duringLesson:
|
||||
return {
|
||||
"color":
|
||||
'#${_settings.liveActivityColor.toString().substring(10, 16)}',
|
||||
"icon": currentLesson != null
|
||||
? SubjectIcon.resolveName(subject: currentLesson?.subject)
|
||||
: "book",
|
||||
"index":
|
||||
currentLesson != null ? '${currentLesson!.lessonIndex}. ' : "",
|
||||
"title": currentLesson != null
|
||||
? currentLesson?.subject.renamedTo ??
|
||||
ShortSubject.resolve(subject: currentLesson?.subject)
|
||||
.capital()
|
||||
: "",
|
||||
"subtitle": currentLesson?.room.replaceAll("_", " ") ?? "",
|
||||
"description": currentLesson?.description ?? "",
|
||||
"startDate": ((currentLesson?.start.millisecondsSinceEpoch ?? 0) -
|
||||
_delay.inMilliseconds)
|
||||
.toString(),
|
||||
"endDate": ((currentLesson?.end.millisecondsSinceEpoch ?? 0) -
|
||||
_delay.inMilliseconds)
|
||||
.toString(),
|
||||
"nextSubject": nextLesson != null
|
||||
? nextLesson?.subject.renamedTo ??
|
||||
ShortSubject.resolve(subject: nextLesson?.subject).capital()
|
||||
: "",
|
||||
"nextRoom": nextLesson?.room.replaceAll("_", " ") ?? "",
|
||||
};
|
||||
case LiveCardState.duringBreak:
|
||||
final iconFloorMap = {
|
||||
"to room": "chevron.right.2",
|
||||
"up floor": "arrow.up.right",
|
||||
"down floor": "arrow.down.left",
|
||||
"ground floor": "arrow.down.left",
|
||||
};
|
||||
|
||||
final diff = getFloorDifference();
|
||||
|
||||
return {
|
||||
"color":
|
||||
'#${_settings.liveActivityColor.toString().substring(10, 16)}',
|
||||
"icon": iconFloorMap[diff] ?? "cup.and.saucer",
|
||||
"title": "Szünet",
|
||||
"description": "go $diff".i18n.fill([
|
||||
diff != "to room" ? (nextLesson!.getFloor() ?? 0) : nextLesson!.room
|
||||
]),
|
||||
"startDate": ((prevLesson?.end.millisecondsSinceEpoch ?? 0) -
|
||||
_delay.inMilliseconds)
|
||||
.toString(),
|
||||
"endDate": ((nextLesson?.start.millisecondsSinceEpoch ?? 0) -
|
||||
_delay.inMilliseconds)
|
||||
.toString(),
|
||||
"nextSubject": (nextLesson != null
|
||||
? nextLesson?.subject.renamedTo ??
|
||||
ShortSubject.resolve(subject: nextLesson?.subject)
|
||||
.capital()
|
||||
: "")
|
||||
.capital(),
|
||||
"nextRoom": nextLesson?.room.replaceAll("_", " ") ?? "",
|
||||
"index": "",
|
||||
"subtitle": "",
|
||||
};
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
void update() async {
|
||||
if (Platform.isIOS) {
|
||||
_liveActivitiesPlugin.areActivitiesEnabled().then((value) {
|
||||
if (value) {
|
||||
final cmap = toMap();
|
||||
if (!mapEquals(cmap, _lastActivity)) {
|
||||
_lastActivity = cmap;
|
||||
try {
|
||||
if (_lastActivity.isNotEmpty) {
|
||||
if (_latestActivityId == null) {
|
||||
_liveActivitiesPlugin
|
||||
.createActivity(_lastActivity)
|
||||
.then((value) => _latestActivityId = value);
|
||||
} else {
|
||||
_liveActivitiesPlugin.updateActivity(
|
||||
_latestActivityId!, _lastActivity);
|
||||
}
|
||||
} else {
|
||||
if (_latestActivityId != null) {
|
||||
_liveActivitiesPlugin.endActivity(_latestActivityId!);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('ERROR: Unable to create or update iOS LiveActivity!');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
List<Lesson> today = _today(_timetable);
|
||||
|
||||
if (today.isEmpty && !_hasCheckedTimetable) {
|
||||
_hasCheckedTimetable = true;
|
||||
await _timetable.fetch(week: Week.current());
|
||||
today = _today(_timetable);
|
||||
}
|
||||
|
||||
_delay = _settings.bellDelayEnabled
|
||||
? Duration(seconds: _settings.bellDelay)
|
||||
: Duration.zero;
|
||||
|
||||
final now = _now().add(_delay);
|
||||
|
||||
// Filter cancelled lessons #20
|
||||
// Filter label lessons #128
|
||||
today = today
|
||||
.where((lesson) =>
|
||||
lesson.status?.name != "Elmaradt" &&
|
||||
lesson.subject.id != '' &&
|
||||
!lesson.isEmpty)
|
||||
.toList();
|
||||
|
||||
if (today.isNotEmpty) {
|
||||
// sort
|
||||
today.sort((a, b) => a.start.compareTo(b.start));
|
||||
|
||||
final _lesson = today.firstWhere(
|
||||
(l) => l.start.isBefore(now) && l.end.isAfter(now),
|
||||
orElse: () => Lesson.fromJson({}));
|
||||
|
||||
if (_lesson.start.year != 0) {
|
||||
currentLesson = _lesson;
|
||||
} else {
|
||||
currentLesson = null;
|
||||
}
|
||||
|
||||
final _next = today.firstWhere((l) => l.start.isAfter(now),
|
||||
orElse: () => Lesson.fromJson({}));
|
||||
nextLessons = today.where((l) => l.start.isAfter(now)).toList();
|
||||
|
||||
if (_next.start.year != 0) {
|
||||
nextLesson = _next;
|
||||
} else {
|
||||
nextLesson = null;
|
||||
}
|
||||
|
||||
final _prev = today.lastWhere((l) => l.end.isBefore(now),
|
||||
orElse: () => Lesson.fromJson({}));
|
||||
|
||||
if (_prev.start.year != 0) {
|
||||
prevLesson = _prev;
|
||||
} else {
|
||||
prevLesson = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (now.isBefore(DateTime(now.year, DateTime.august, 31)) &&
|
||||
now.isAfter(DateTime(now.year, DateTime.june, 14))) {
|
||||
currentState = LiveCardState.summary;
|
||||
} else if (currentLesson != null) {
|
||||
currentState = LiveCardState.duringLesson;
|
||||
} else if (nextLesson != null && prevLesson != null) {
|
||||
currentState = LiveCardState.duringBreak;
|
||||
} else if (now.hour >= 12 && now.hour < 20) {
|
||||
currentState = LiveCardState.afternoon;
|
||||
} else if (now.hour >= 20) {
|
||||
currentState = LiveCardState.night;
|
||||
} else if (now.hour >= 5 && now.hour <= 10) {
|
||||
currentState = LiveCardState.morning;
|
||||
} else {
|
||||
currentState = LiveCardState.empty;
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get show => currentState != LiveCardState.empty;
|
||||
|
||||
Duration get delay => _delay;
|
||||
|
||||
bool _sameDate(DateTime a, DateTime b) =>
|
||||
(a.year == b.year && a.month == b.month && a.day == b.day);
|
||||
|
||||
List<Lesson> _today(TimetableProvider p) => (p.getWeek(Week.current()) ?? [])
|
||||
.where((l) => _sameDate(l.date, _now()))
|
||||
.toList();
|
||||
}
|
||||
109
refilc/lib/api/providers/news_provider.dart
Normal file
109
refilc/lib/api/providers/news_provider.dart
Normal file
@@ -0,0 +1,109 @@
|
||||
// ignore_for_file: use_build_context_synchronously
|
||||
import 'package:refilc/api/client.dart';
|
||||
import 'package:refilc/models/news.dart';
|
||||
import 'package:refilc/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 seen_ = Provider.of<SettingsProvider>(_context, listen: false).seenNews;
|
||||
|
||||
if (seen_.isEmpty) {
|
||||
var news_ = await FilcAPI.getNews();
|
||||
if (news_ != null) {
|
||||
_news = news_;
|
||||
show = true;
|
||||
}
|
||||
}
|
||||
|
||||
//_state = seen_;
|
||||
// Provider.of<SettingsProvider>(_context, listen: false)
|
||||
// .update(seenNewsId: news_.id);
|
||||
}
|
||||
|
||||
Future<void> fetch() async {
|
||||
var news_ = await FilcAPI.getNews();
|
||||
if (news_ == null) return;
|
||||
|
||||
show = false;
|
||||
|
||||
_news = news_;
|
||||
|
||||
for (var news in news_) {
|
||||
if (news.expireDate.isAfter(DateTime.now()) &&
|
||||
Provider.of<SettingsProvider>(_context, listen: false)
|
||||
.seenNews
|
||||
.contains(news.id) ==
|
||||
false) {
|
||||
show = true;
|
||||
Provider.of<SettingsProvider>(_context, listen: false)
|
||||
.update(seenNewsId: news.id);
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
// print(news_.length);
|
||||
// print(_state);
|
||||
|
||||
// _news = news_;
|
||||
// _fresh = news_.length - _state;
|
||||
|
||||
// if (_fresh < 0) {
|
||||
// _state = news_.length;
|
||||
// Provider.of<SettingsProvider>(_context, listen: false)
|
||||
// .update(newsState: _state);
|
||||
// }
|
||||
|
||||
// _fresh = max(_fresh, 0);
|
||||
|
||||
// if (_fresh > 0) {
|
||||
// show = true;
|
||||
// notifyListeners();
|
||||
// }
|
||||
|
||||
// print(_fresh);
|
||||
// print(_state);
|
||||
// print(show);
|
||||
}
|
||||
|
||||
void lock() => show = false;
|
||||
|
||||
void release() {
|
||||
// if (_fresh == 0) return;
|
||||
|
||||
// _fresh--;
|
||||
// //_state++;
|
||||
|
||||
// // Provider.of<SettingsProvider>(_context, listen: false)
|
||||
// // .update(seenNewsId: _state);
|
||||
|
||||
// if (_fresh > 0) {
|
||||
// show = true;
|
||||
// } else {
|
||||
// show = false;
|
||||
// }
|
||||
|
||||
// notifyListeners();
|
||||
}
|
||||
}
|
||||
53
refilc/lib/api/providers/self_note_provider.dart
Normal file
53
refilc/lib/api/providers/self_note_provider.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'package:refilc/api/providers/user_provider.dart';
|
||||
import 'package:refilc/api/providers/database_provider.dart';
|
||||
import 'package:refilc/models/self_note.dart';
|
||||
import 'package:refilc/models/user.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class SelfNoteProvider with ChangeNotifier {
|
||||
late List<SelfNote> _notes;
|
||||
late BuildContext _context;
|
||||
List<SelfNote> get notes => _notes;
|
||||
|
||||
SelfNoteProvider({
|
||||
List<SelfNote> initialNotes = const [],
|
||||
required BuildContext context,
|
||||
}) {
|
||||
_notes = List.castFrom(initialNotes);
|
||||
_context = context;
|
||||
|
||||
if (_notes.isEmpty) restore();
|
||||
}
|
||||
|
||||
// restore self notes from db
|
||||
Future<void> restore() async {
|
||||
String? userId = Provider.of<UserProvider>(_context, listen: false).id;
|
||||
|
||||
// load self notes from db
|
||||
if (userId != null) {
|
||||
var dbNotes = await Provider.of<DatabaseProvider>(_context, listen: false)
|
||||
.userQuery
|
||||
.getSelfNotes(userId: userId);
|
||||
_notes = dbNotes;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// fetches fresh data from api (not needed, cuz no api for that)
|
||||
// Future<void> fetch() async {
|
||||
// }
|
||||
|
||||
// store self notes in db
|
||||
Future<void> store(List<SelfNote> notes) async {
|
||||
User? user = Provider.of<UserProvider>(_context, listen: false).user;
|
||||
if (user == null) throw "Cannot store Self Notes for User null";
|
||||
String userId = user.id;
|
||||
|
||||
await Provider.of<DatabaseProvider>(_context, listen: false)
|
||||
.userStore
|
||||
.storeSelfNotes(notes, userId: userId);
|
||||
_notes = notes;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
126
refilc/lib/api/providers/status_provider.dart
Normal file
126
refilc/lib/api/providers/status_provider.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
enum Status { network, maintenance, syncing, apiError }
|
||||
|
||||
class StatusProvider extends ChangeNotifier {
|
||||
final List<Status> _stack = [];
|
||||
double _progress = 0.0;
|
||||
ConnectivityResult _networkType = ConnectivityResult.none;
|
||||
ConnectivityResult get networkType => _networkType;
|
||||
|
||||
StatusProvider() {
|
||||
_handleNetworkChanges();
|
||||
_handleDNSFailure();
|
||||
Connectivity().checkConnectivity().then((value) => _networkType = value);
|
||||
}
|
||||
|
||||
Status? getStatus() => _stack.isNotEmpty ? _stack[0] : null;
|
||||
// Status progress from 0.0 to 1.0
|
||||
double get progress => _progress;
|
||||
|
||||
void _handleNetworkChanges() {
|
||||
Connectivity().onConnectivityChanged.listen((event) {
|
||||
_networkType = event;
|
||||
if (event == ConnectivityResult.none) {
|
||||
if (!_stack.contains(Status.network)) {
|
||||
_stack.remove(Status.apiError);
|
||||
_stack.insert(0, Status.network);
|
||||
notifyListeners();
|
||||
}
|
||||
} else {
|
||||
if (_stack.contains(Status.network)) {
|
||||
_stack.remove(Status.network);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _handleDNSFailure() {
|
||||
try {
|
||||
InternetAddress.lookup('api.refilc.hu').then((status) {
|
||||
if (status.isEmpty) {
|
||||
if (!_stack.contains(Status.network)) {
|
||||
_stack.remove(Status.apiError);
|
||||
_stack.insert(0, Status.network);
|
||||
notifyListeners();
|
||||
}
|
||||
} else {
|
||||
if (_stack.contains(Status.network)) {
|
||||
_stack.remove(Status.network);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
});
|
||||
} on SocketException catch (_) {
|
||||
if (!_stack.contains(Status.network)) {
|
||||
_stack.remove(Status.apiError);
|
||||
_stack.insert(0, Status.network);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void triggerRequest(http.Response res) {
|
||||
if (res.headers.containsKey("x-maintenance-mode") ||
|
||||
res.statusCode == 503) {
|
||||
if (!_stack.contains(Status.maintenance)) {
|
||||
_stack.insert(0, Status.maintenance);
|
||||
notifyListeners();
|
||||
}
|
||||
} else {
|
||||
if (_stack.contains(Status.maintenance)) {
|
||||
_stack.remove(Status.maintenance);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
if (res.body == 'invalid_grant' ||
|
||||
res.body.replaceAll(' ', '') == '' ||
|
||||
res.statusCode == 400) {
|
||||
if (!_stack.contains(Status.apiError) &&
|
||||
!_stack.contains(Status.network)) {
|
||||
if (res.statusCode == 401) return;
|
||||
_stack.insert(0, Status.apiError);
|
||||
notifyListeners();
|
||||
}
|
||||
} else {
|
||||
if (_stack.contains(Status.apiError) &&
|
||||
res.request?.url.path != '/nonce') {
|
||||
_stack.remove(Status.apiError);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void triggerSync({required int current, required int max}) {
|
||||
double prev = _progress;
|
||||
|
||||
if (!_stack.contains(Status.syncing)) {
|
||||
_stack.add(Status.syncing);
|
||||
_progress = 0.0;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
if (max == 0) {
|
||||
_progress = 0.0;
|
||||
} else {
|
||||
_progress = current / max;
|
||||
}
|
||||
|
||||
if (_progress == 1.0) {
|
||||
notifyListeners();
|
||||
// Wait for animation
|
||||
Future.delayed(const Duration(milliseconds: 250), () {
|
||||
_stack.remove(Status.syncing);
|
||||
notifyListeners();
|
||||
});
|
||||
} else if (progress != prev) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
96
refilc/lib/api/providers/sync.dart
Normal file
96
refilc/lib/api/providers/sync.dart
Normal file
@@ -0,0 +1,96 @@
|
||||
// ignore_for_file: use_build_context_synchronously
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:refilc/api/providers/database_provider.dart';
|
||||
import 'package:refilc/api/providers/status_provider.dart';
|
||||
import 'package:refilc/api/providers/user_provider.dart';
|
||||
import 'package:refilc_kreta_api/client/api.dart';
|
||||
import 'package:refilc_kreta_api/client/client.dart';
|
||||
import 'package:refilc_kreta_api/models/student.dart';
|
||||
import 'package:refilc_kreta_api/models/week.dart';
|
||||
import 'package:refilc_kreta_api/providers/absence_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/event_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/exam_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/grade_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/homework_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/message_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/note_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/timetable_provider.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:home_widget/home_widget.dart';
|
||||
|
||||
// Mutex
|
||||
bool lock = false;
|
||||
|
||||
Future<void> syncAll(BuildContext context) {
|
||||
if (lock) return Future.value();
|
||||
// Lock
|
||||
lock = true;
|
||||
|
||||
// ignore: avoid_print
|
||||
print("INFO Syncing all");
|
||||
|
||||
UserProvider user = Provider.of<UserProvider>(context, listen: false);
|
||||
StatusProvider statusProvider =
|
||||
Provider.of<StatusProvider>(context, listen: false);
|
||||
|
||||
List<Future<void>> tasks = [];
|
||||
int taski = 0;
|
||||
|
||||
Future<void> syncStatus(Future<void> future) async {
|
||||
await future.onError((error, stackTrace) => null);
|
||||
taski++;
|
||||
statusProvider.triggerSync(current: taski, max: tasks.length);
|
||||
}
|
||||
|
||||
tasks = [
|
||||
syncStatus(Provider.of<GradeProvider>(context, listen: false).fetch()),
|
||||
syncStatus(Provider.of<TimetableProvider>(context, listen: false)
|
||||
.fetch(week: Week.current())),
|
||||
syncStatus(Provider.of<ExamProvider>(context, listen: false).fetch()),
|
||||
syncStatus(Provider.of<HomeworkProvider>(context, listen: false)
|
||||
.fetch(from: DateTime.now().subtract(const Duration(days: 30)))),
|
||||
syncStatus(Provider.of<MessageProvider>(context, listen: false).fetchAll()),
|
||||
syncStatus(Provider.of<MessageProvider>(context, listen: false)
|
||||
.fetchAllRecipients()),
|
||||
syncStatus(Provider.of<NoteProvider>(context, listen: false).fetch()),
|
||||
syncStatus(Provider.of<EventProvider>(context, listen: false).fetch()),
|
||||
syncStatus(Provider.of<AbsenceProvider>(context, listen: false).fetch()),
|
||||
|
||||
// Sync student
|
||||
syncStatus(() async {
|
||||
if (user.user == null) return;
|
||||
Map? studentJson = await Provider.of<KretaClient>(context, listen: false)
|
||||
.getAPI(KretaAPI.student(user.instituteCode!));
|
||||
if (studentJson == null) return;
|
||||
Student student = Student.fromJson(studentJson);
|
||||
|
||||
user.user?.name = student.name;
|
||||
|
||||
// Store user
|
||||
await Provider.of<DatabaseProvider>(context, listen: false)
|
||||
.store
|
||||
.storeUser(user.user!);
|
||||
}()),
|
||||
];
|
||||
|
||||
Future<bool?> updateWidget() async {
|
||||
try {
|
||||
return HomeWidget.updateWidget(name: 'widget_timetable.WidgetTimetable');
|
||||
} on PlatformException catch (exception) {
|
||||
debugPrint('Error Updating Widget. $exception');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return Future.wait(tasks).then((value) {
|
||||
// Unlock
|
||||
lock = false;
|
||||
|
||||
// Update Widget
|
||||
if (Platform.isAndroid) updateWidget();
|
||||
});
|
||||
}
|
||||
65
refilc/lib/api/providers/update_provider.dart
Normal file
65
refilc/lib/api/providers/update_provider.dart
Normal file
@@ -0,0 +1,65 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:refilc/api/client.dart';
|
||||
import 'package:refilc/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;
|
||||
bool _available = false;
|
||||
bool get available => _available && _releases.isNotEmpty;
|
||||
|
||||
// Public
|
||||
List<Release> get releases => _releases;
|
||||
|
||||
UpdateProvider({
|
||||
List<Release> initialReleases = const [],
|
||||
required BuildContext context,
|
||||
}) {
|
||||
_releases = List.castFrom(initialReleases);
|
||||
}
|
||||
|
||||
Future<void> fetch() async {
|
||||
late String currentVersion;
|
||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
currentVersion = packageInfo.version;
|
||||
|
||||
if (!Platform.isAndroid) return;
|
||||
|
||||
_releases = await FilcAPI.getReleases() ?? [];
|
||||
_releases.sort((a, b) => -a.version.compareTo(b.version));
|
||||
|
||||
// Check for new releases
|
||||
if (_releases.isNotEmpty) {
|
||||
if (!_releases.first.prerelease) {
|
||||
_available = _releases.first.version
|
||||
.compareTo(Version.fromString(currentVersion)) ==
|
||||
1;
|
||||
}
|
||||
|
||||
// ignore: avoid_print
|
||||
if (_available) print("INFO: New update: ${releases.first.version}");
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map> installedVersion() async {
|
||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
|
||||
String appName = packageInfo.appName;
|
||||
String packageName = packageInfo.packageName;
|
||||
String version = packageInfo.version;
|
||||
String buildNumber = packageInfo.buildNumber;
|
||||
|
||||
Map<String, String> release = {
|
||||
"app_name": appName,
|
||||
"package_name": packageName,
|
||||
"version": version,
|
||||
"build_number": buildNumber,
|
||||
};
|
||||
|
||||
return release;
|
||||
}
|
||||
}
|
||||
78
refilc/lib/api/providers/user_provider.dart
Normal file
78
refilc/lib/api/providers/user_provider.dart
Normal file
@@ -0,0 +1,78 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/models/user.dart';
|
||||
import 'package:refilc_kreta_api/models/student.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:home_widget/home_widget.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class UserProvider with ChangeNotifier {
|
||||
final 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;
|
||||
Role? get role => user?.role;
|
||||
Student? get student => user?.student;
|
||||
String? get nickname => user?.nickname;
|
||||
String get picture => user?.picture ?? "";
|
||||
String? get displayName => user?.displayName;
|
||||
|
||||
final SettingsProvider _settings;
|
||||
|
||||
UserProvider({required SettingsProvider settings}) : _settings = settings;
|
||||
|
||||
void setUser(String userId) async {
|
||||
_selectedUserId = userId;
|
||||
await _settings.update(lastAccountId: userId);
|
||||
if (Platform.isAndroid) updateWidget();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<bool?> updateWidget() async {
|
||||
try {
|
||||
return HomeWidget.updateWidget(name: 'widget_timetable.WidgetTimetable');
|
||||
} on PlatformException catch (exception) {
|
||||
if (kDebugMode) {
|
||||
print('Error Updating Widget After setUser. $exception');
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void addUser(User user) {
|
||||
_users[user.id] = user;
|
||||
if (kDebugMode) {
|
||||
print("DEBUG: Added User: ${user.id}");
|
||||
}
|
||||
}
|
||||
|
||||
void removeUser(String userId) async {
|
||||
_users.removeWhere((key, value) => key == userId);
|
||||
if (_users.isNotEmpty) {
|
||||
setUser(_users.keys.first);
|
||||
} else {
|
||||
await _settings.update(lastAccountId: "");
|
||||
}
|
||||
if (Platform.isAndroid) updateWidget();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
User getUser(String userId) {
|
||||
return _users[userId]!;
|
||||
}
|
||||
|
||||
List<User> getUsers() {
|
||||
return _users.values.toList();
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
293
refilc/lib/app.dart
Normal file
293
refilc/lib/app.dart
Normal file
@@ -0,0 +1,293 @@
|
||||
// ignore_for_file: deprecated_member_use
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:refilc/api/client.dart';
|
||||
import 'package:refilc/api/providers/ad_provider.dart';
|
||||
import 'package:refilc/api/providers/live_card_provider.dart';
|
||||
import 'package:refilc/api/providers/news_provider.dart';
|
||||
import 'package:refilc/api/providers/database_provider.dart';
|
||||
import 'package:refilc/api/providers/self_note_provider.dart';
|
||||
import 'package:refilc/api/providers/status_provider.dart';
|
||||
import 'package:refilc/models/config.dart';
|
||||
import 'package:refilc/providers/third_party_provider.dart';
|
||||
import 'package:refilc/theme/observer.dart';
|
||||
import 'package:refilc/theme/theme.dart';
|
||||
import 'package:refilc_kreta_api/client/client.dart';
|
||||
import 'package:refilc_kreta_api/providers/grade_provider.dart';
|
||||
import 'package:refilc_plus/providers/goal_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/share_provider.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:i18n_extension/i18n_widget.dart';
|
||||
import 'package:material_color_utilities/palettes/core_palette.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
// Mobile UI
|
||||
import 'package:refilc_mobile_ui/common/system_chrome.dart' as mobile;
|
||||
import 'package:refilc_mobile_ui/screens/login/login_route.dart' as mobile;
|
||||
import 'package:refilc_mobile_ui/screens/login/login_screen.dart' as mobile;
|
||||
import 'package:refilc_mobile_ui/screens/navigation/navigation_screen.dart'
|
||||
as mobile;
|
||||
import 'package:refilc_mobile_ui/screens/settings/settings_route.dart'
|
||||
as mobile;
|
||||
import 'package:refilc_mobile_ui/screens/settings/settings_screen.dart'
|
||||
as mobile;
|
||||
|
||||
// Desktop UI
|
||||
import 'package:refilc_desktop_ui/screens/navigation/navigation_screen.dart'
|
||||
as desktop;
|
||||
import 'package:refilc_desktop_ui/screens/login/login_screen.dart' as desktop;
|
||||
import 'package:refilc_desktop_ui/screens/login/login_route.dart' as desktop;
|
||||
|
||||
// Providers
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc_kreta_api/providers/absence_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/event_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/exam_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/homework_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/message_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/note_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/timetable_provider.dart';
|
||||
import 'package:refilc/api/providers/user_provider.dart';
|
||||
import 'package:refilc/api/providers/update_provider.dart';
|
||||
import 'package:refilc_mobile_ui/pages/grades/calculator/grade_calculator_provider.dart';
|
||||
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||||
import 'package:refilc_plus/providers/premium_provider.dart';
|
||||
|
||||
class App extends StatelessWidget {
|
||||
final SettingsProvider settings;
|
||||
final UserProvider user;
|
||||
final DatabaseProvider database;
|
||||
|
||||
const App(
|
||||
{super.key,
|
||||
required this.database,
|
||||
required this.settings,
|
||||
required this.user});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
mobile.setSystemChrome(context);
|
||||
|
||||
// Set high refresh mode #28
|
||||
if (Platform.isAndroid) FlutterDisplayMode.setHighRefreshRate();
|
||||
|
||||
CorePalette? corePalette;
|
||||
|
||||
final status = StatusProvider();
|
||||
final kreta = KretaClient(user: user, settings: settings, status: status);
|
||||
final timetable =
|
||||
TimetableProvider(user: user, database: database, kreta: kreta);
|
||||
final premium = PremiumProvider(settings: settings);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
FilcAPI.getConfig(settings).then((Config? config) {
|
||||
if (config != null) settings.update(config: config);
|
||||
});
|
||||
premium.activate();
|
||||
});
|
||||
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
// refilc providers
|
||||
ChangeNotifierProvider<PremiumProvider>(create: (_) => premium),
|
||||
ChangeNotifierProvider<SettingsProvider>(create: (_) => settings),
|
||||
ChangeNotifierProvider<UserProvider>(create: (_) => user),
|
||||
ChangeNotifierProvider<StatusProvider>(create: (_) => status),
|
||||
Provider<KretaClient>(create: (_) => kreta),
|
||||
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),
|
||||
),
|
||||
ChangeNotifierProvider<AdProvider>(
|
||||
create: (context) => AdProvider(context: context),
|
||||
),
|
||||
|
||||
// user data (kreten) providers
|
||||
ChangeNotifierProvider<GradeProvider>(
|
||||
create: (_) => GradeProvider(
|
||||
settings: settings,
|
||||
user: user,
|
||||
database: database,
|
||||
kreta: kreta,
|
||||
),
|
||||
),
|
||||
ChangeNotifierProvider<TimetableProvider>(create: (_) => timetable),
|
||||
ChangeNotifierProvider<ExamProvider>(
|
||||
create: (context) => ExamProvider(context: context),
|
||||
),
|
||||
ChangeNotifierProvider<HomeworkProvider>(
|
||||
create: (context) => HomeworkProvider(
|
||||
context: context,
|
||||
database: database,
|
||||
user: user,
|
||||
),
|
||||
),
|
||||
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),
|
||||
),
|
||||
|
||||
// other providers
|
||||
ChangeNotifierProvider<GradeCalculatorProvider>(
|
||||
create: (_) => GradeCalculatorProvider(
|
||||
settings: settings,
|
||||
user: user,
|
||||
database: database,
|
||||
kreta: kreta,
|
||||
),
|
||||
),
|
||||
ChangeNotifierProvider<LiveCardProvider>(
|
||||
create: (_) => LiveCardProvider(
|
||||
timetable: timetable,
|
||||
settings: settings,
|
||||
),
|
||||
),
|
||||
ChangeNotifierProvider<GoalProvider>(
|
||||
create: (_) => GoalProvider(
|
||||
database: database,
|
||||
user: user,
|
||||
),
|
||||
),
|
||||
ChangeNotifierProvider<ShareProvider>(
|
||||
create: (_) => ShareProvider(
|
||||
user: user,
|
||||
),
|
||||
),
|
||||
ChangeNotifierProvider<SelfNoteProvider>(
|
||||
create: (context) => SelfNoteProvider(context: context),
|
||||
),
|
||||
|
||||
// third party providers
|
||||
ChangeNotifierProvider<ThirdPartyProvider>(
|
||||
create: (context) => ThirdPartyProvider(context: context),
|
||||
),
|
||||
],
|
||||
child: Consumer<ThemeModeObserver>(
|
||||
builder: (context, themeMode, child) {
|
||||
return FutureBuilder<CorePalette?>(
|
||||
future: DynamicColorPlugin.getCorePalette(),
|
||||
builder: (context, snapshot) {
|
||||
corePalette = snapshot.data;
|
||||
return MaterialApp(
|
||||
builder: (context, child) {
|
||||
// Limit font size scaling to 1.0
|
||||
double textScaleFactor =
|
||||
min(MediaQuery.of(context).textScaleFactor, 1.0);
|
||||
|
||||
return I18n(
|
||||
initialLocale: Locale(
|
||||
settings.language, settings.language.toUpperCase()),
|
||||
child: MediaQuery(
|
||||
data: MediaQuery.of(context)
|
||||
.copyWith(textScaleFactor: textScaleFactor),
|
||||
child: child ?? Container(),
|
||||
),
|
||||
);
|
||||
},
|
||||
title: "reFilc",
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: AppTheme.lightTheme(context, palette: corePalette),
|
||||
darkTheme: AppTheme.darkTheme(context, palette: corePalette),
|
||||
themeMode: themeMode.themeMode,
|
||||
localizationsDelegates: const [
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: const [
|
||||
Locale('en', 'EN'),
|
||||
Locale('hu', 'HU'),
|
||||
Locale('de', 'DE'),
|
||||
],
|
||||
localeListResolutionCallback: (locales, supported) {
|
||||
Locale locale = const Locale('hu', 'HU');
|
||||
|
||||
for (var loc in locales ?? []) {
|
||||
if (supported.contains(loc)) {
|
||||
locale = loc;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return locale;
|
||||
},
|
||||
onGenerateRoute: (settings) => rootNavigator(settings),
|
||||
initialRoute:
|
||||
user.getUsers().isNotEmpty ? "navigation" : "login",
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Route? rootNavigator(RouteSettings route) {
|
||||
if (kIsWeb) {
|
||||
switch (route.name) {
|
||||
case "login_back":
|
||||
return CupertinoPageRoute(
|
||||
builder: (context) => const desktop.LoginScreen(back: true));
|
||||
case "login":
|
||||
return _rootRoute(const desktop.LoginScreen());
|
||||
case "navigation":
|
||||
return _rootRoute(const desktop.NavigationScreen());
|
||||
case "login_to_navigation":
|
||||
return desktop.loginRoute(const desktop.NavigationScreen());
|
||||
}
|
||||
} else if (Platform.isAndroid || Platform.isIOS) {
|
||||
switch (route.name) {
|
||||
case "login_back":
|
||||
return CupertinoPageRoute(
|
||||
builder: (context) => const mobile.LoginScreen(back: true));
|
||||
case "login":
|
||||
return _rootRoute(const mobile.LoginScreen());
|
||||
case "navigation":
|
||||
return _rootRoute(const mobile.NavigationScreen());
|
||||
case "login_to_navigation":
|
||||
return mobile.loginRoute(const mobile.NavigationScreen());
|
||||
case "settings":
|
||||
return mobile.settingsRoute(const mobile.SettingsScreen());
|
||||
}
|
||||
} else if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
|
||||
switch (route.name) {
|
||||
case "login_back":
|
||||
return CupertinoPageRoute(
|
||||
builder: (context) => const desktop.LoginScreen(back: true));
|
||||
case "login":
|
||||
return _rootRoute(const desktop.LoginScreen());
|
||||
case "navigation":
|
||||
return _rootRoute(const desktop.NavigationScreen());
|
||||
case "login_to_navigation":
|
||||
return desktop.loginRoute(const desktop.NavigationScreen());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Route _rootRoute(Widget widget) {
|
||||
return PageRouteBuilder(pageBuilder: (context, _, __) => widget);
|
||||
}
|
||||
}
|
||||
202
refilc/lib/database/init.dart
Normal file
202
refilc/lib/database/init.dart
Normal file
@@ -0,0 +1,202 @@
|
||||
// ignore_for_file: avoid_print
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:refilc/api/providers/database_provider.dart';
|
||||
import 'package:refilc/database/struct.dart';
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
// ignore: depend_on_referenced_packages
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart';
|
||||
|
||||
const settingsDB = DatabaseStruct("settings", {
|
||||
"language": String, "start_page": int, "rounding": int, "theme": int,
|
||||
"accent_color": int, "news": int, "seen_news": String,
|
||||
"developer_mode": int,
|
||||
"update_channel": int, "config": String, "custom_accent_color": int,
|
||||
"custom_background_color": int, "custom_highlight_color": int,
|
||||
"custom_icon_color": int, "shadow_effect": int, // general
|
||||
"grade_color1": int, "grade_color2": int, "grade_color3": int,
|
||||
"grade_color4": int, "grade_color5": int, // grade colors
|
||||
"vibration_strength": int, "ab_weeks": int, "swap_ab_weeks": int,
|
||||
"notifications": int, "notifications_bitfield": int,
|
||||
"notification_poll_interval": int,
|
||||
"notifications_grades": int,
|
||||
"notifications_absences": int,
|
||||
"notifications_messages": int,
|
||||
"notifications_lessons": int, // notifications
|
||||
"x_filc_id": String, "graph_class_avg": int, "presentation_mode": int,
|
||||
"bell_delay": int, "bell_delay_enabled": int,
|
||||
"grade_opening_fun": int, "icon_pack": String, "premium_scopes": String,
|
||||
"premium_token": String, "premium_login": String,
|
||||
"last_account_id": String, "renamed_subjects_enabled": int,
|
||||
"renamed_subjects_italics": int, "renamed_teachers_enabled": int,
|
||||
"renamed_teachers_italics": int,
|
||||
"live_activity_color": String,
|
||||
"welcome_message": String, "app_icon": String,
|
||||
// paints
|
||||
"current_theme_id": String, "current_theme_display_name": String,
|
||||
"current_theme_creator": String,
|
||||
// pinned settings
|
||||
"general_s_pin": String, "personalize_s_pin": String,
|
||||
"notify_s_pin": String, "extras_s_pin": String,
|
||||
// more
|
||||
"show_breaks": int,
|
||||
"font_family": String,
|
||||
});
|
||||
// DON'T FORGET TO UPDATE DEFAULT VALUES IN `initDB` MIGRATION OR ELSE PARENTS WILL COMPLAIN ABOUT THEIR CHILDREN MISSING
|
||||
// YOU'VE BEEN WARNED!!!
|
||||
const usersDB = DatabaseStruct("users", {
|
||||
"id": String, "name": String, "username": String, "password": String,
|
||||
"institute_code": String, "student": String, "role": int,
|
||||
"nickname": String, "picture": String // premium only
|
||||
});
|
||||
const userDataDB = DatabaseStruct("user_data", {
|
||||
"id": String, "grades": String, "timetable": String, "exams": String,
|
||||
"homework": String, "messages": String, "recipients": String, "notes": String,
|
||||
"events": String, "absences": String, "group_averages": String,
|
||||
// renamed subjects // non kreta data
|
||||
"renamed_subjects": String,
|
||||
// renamed teachers // non kreta data
|
||||
"renamed_teachers": String,
|
||||
// "subject_lesson_count": String, // non kreta data
|
||||
"last_seen_grade": int,
|
||||
// goal planning // non kreta data
|
||||
"goal_plans": String,
|
||||
"goal_averages": String,
|
||||
"goal_befores": String,
|
||||
"goal_pin_dates": String,
|
||||
// todo and notes
|
||||
"todo_items": String, "self_notes": String,
|
||||
// v5 shit
|
||||
"roundings": String,
|
||||
"grade_rarities": String,
|
||||
});
|
||||
|
||||
Future<void> createTable(Database db, DatabaseStruct struct) =>
|
||||
db.execute("CREATE TABLE IF NOT EXISTS ${struct.table} ($struct)");
|
||||
|
||||
Future<Database> initDB(DatabaseProvider database) async {
|
||||
Database db;
|
||||
|
||||
if (kIsWeb) {
|
||||
db = await databaseFactoryFfiWeb.openDatabase("app.db");
|
||||
} else if (Platform.isLinux || Platform.isWindows) {
|
||||
sqfliteFfiInit();
|
||||
db = await databaseFactoryFfi.openDatabase("app.db");
|
||||
} else {
|
||||
db = await openDatabase("app.db");
|
||||
}
|
||||
|
||||
await createTable(db, settingsDB);
|
||||
await createTable(db, usersDB);
|
||||
await createTable(db, userDataDB);
|
||||
|
||||
if ((await db.rawQuery("SELECT COUNT(*) FROM settings"))[0].values.first ==
|
||||
0) {
|
||||
// Set default values for table Settings
|
||||
await db.insert("settings",
|
||||
SettingsProvider.defaultSettings(database: database).toMap());
|
||||
}
|
||||
|
||||
// Migrate Databases
|
||||
try {
|
||||
await migrateDB(
|
||||
db,
|
||||
struct: settingsDB,
|
||||
defaultValues:
|
||||
SettingsProvider.defaultSettings(database: database).toMap(),
|
||||
);
|
||||
await migrateDB(
|
||||
db,
|
||||
struct: usersDB,
|
||||
defaultValues: {"role": 0, "nickname": "", "picture": ""},
|
||||
);
|
||||
await migrateDB(db, struct: userDataDB, defaultValues: {
|
||||
"grades": "[]", "timetable": "[]", "exams": "[]", "homework": "[]",
|
||||
"messages": "[]", "recipients": "[]", "notes": "[]", "events": "[]",
|
||||
"absences": "[]",
|
||||
"group_averages": "[]",
|
||||
// renamed subjects // non kreta data
|
||||
"renamed_subjects": "{}",
|
||||
// renamed teachers // non kreta data
|
||||
"renamed_teachers": "{}",
|
||||
// "subject_lesson_count": "{}", // non kreta data
|
||||
"last_seen_grade": 0,
|
||||
// goal planning // non kreta data
|
||||
"goal_plans": "{}",
|
||||
"goal_averages": "{}",
|
||||
"goal_befores": "{}",
|
||||
"goal_pin_dates": "{}",
|
||||
// todo and notes
|
||||
"todo_items": "{}", "self_notes": "[]",
|
||||
// v5 shit
|
||||
"roundings": "{}",
|
||||
"grade_rarities": "{}",
|
||||
});
|
||||
} catch (error) {
|
||||
print("ERROR: migrateDB: $error");
|
||||
}
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
Future<void> migrateDB(
|
||||
Database db, {
|
||||
required DatabaseStruct struct,
|
||||
required Map<String, Object?> defaultValues,
|
||||
}) async {
|
||||
var originalRows = await db.query(struct.table);
|
||||
|
||||
if (originalRows.isEmpty) {
|
||||
await db.execute("drop table ${struct.table}");
|
||||
await createTable(db, struct);
|
||||
return;
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> migrated = [];
|
||||
|
||||
// go through each row and add missing keys or delete non existing keys
|
||||
await Future.forEach<Map<String, Object?>>(originalRows, (original) async {
|
||||
bool migrationRequired = struct.struct.keys.any(
|
||||
(key) => !original.containsKey(key) || original[key] == null) ||
|
||||
original.keys.any((key) => !struct.struct.containsKey(key));
|
||||
|
||||
if (migrationRequired) {
|
||||
print("INFO: Migrating ${struct.table}");
|
||||
var copy = Map<String, Object?>.from(original);
|
||||
|
||||
// Fill missing columns
|
||||
for (var key in struct.struct.keys) {
|
||||
if (!original.containsKey(key) || original[key] == null) {
|
||||
print("DEBUG: migrating $key");
|
||||
copy[key] = defaultValues[key];
|
||||
}
|
||||
}
|
||||
|
||||
for (var key in original.keys) {
|
||||
if (!struct.struct.keys.contains(key)) {
|
||||
print("DEBUG: dropping $key");
|
||||
copy.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
migrated.add(copy);
|
||||
}
|
||||
});
|
||||
|
||||
// replace the old table with the migrated one
|
||||
if (migrated.isNotEmpty) {
|
||||
// Delete table
|
||||
await db.execute("drop table ${struct.table}");
|
||||
|
||||
// Recreate table
|
||||
await createTable(db, struct);
|
||||
await Future.forEach(migrated, (Map<String, Object?> copy) async {
|
||||
await db.insert(struct.table, copy);
|
||||
});
|
||||
|
||||
print("INFO: Database migrated");
|
||||
}
|
||||
}
|
||||
329
refilc/lib/database/query.dart
Normal file
329
refilc/lib/database/query.dart
Normal file
@@ -0,0 +1,329 @@
|
||||
import 'dart:convert';
|
||||
import 'package:refilc/api/providers/database_provider.dart';
|
||||
import 'package:refilc/models/self_note.dart';
|
||||
import 'package:refilc/models/subject_lesson_count.dart';
|
||||
import 'package:refilc/models/user.dart';
|
||||
import 'package:refilc_kreta_api/models/week.dart';
|
||||
// ignore: depend_on_referenced_packages
|
||||
import 'package:sqflite_common/sqlite_api.dart';
|
||||
|
||||
// Models
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/api/providers/user_provider.dart';
|
||||
import 'package:refilc_kreta_api/models/grade.dart';
|
||||
import 'package:refilc_kreta_api/models/lesson.dart';
|
||||
import 'package:refilc_kreta_api/models/exam.dart';
|
||||
import 'package:refilc_kreta_api/models/homework.dart';
|
||||
import 'package:refilc_kreta_api/models/message.dart';
|
||||
import 'package:refilc_kreta_api/models/note.dart';
|
||||
import 'package:refilc_kreta_api/models/event.dart';
|
||||
import 'package:refilc_kreta_api/models/absence.dart';
|
||||
import 'package:refilc_kreta_api/models/group_average.dart';
|
||||
|
||||
class DatabaseQuery {
|
||||
DatabaseQuery({required this.db});
|
||||
|
||||
final Database db;
|
||||
|
||||
Future<SettingsProvider> getSettings(DatabaseProvider database) async {
|
||||
Map settingsMap = (await db.query("settings")).elementAt(0);
|
||||
SettingsProvider settings =
|
||||
SettingsProvider.fromMap(settingsMap, database: database);
|
||||
return settings;
|
||||
}
|
||||
|
||||
Future<UserProvider> getUsers(SettingsProvider settings) async {
|
||||
var userProvider = UserProvider(settings: settings);
|
||||
List<Map> usersMap = await db.query("users");
|
||||
for (var user in usersMap) {
|
||||
userProvider.addUser(User.fromMap(user));
|
||||
}
|
||||
if (userProvider
|
||||
.getUsers()
|
||||
.map((e) => e.id)
|
||||
.contains(settings.lastAccountId)) {
|
||||
userProvider.setUser(settings.lastAccountId);
|
||||
} else {
|
||||
if (usersMap.isNotEmpty) {
|
||||
userProvider.setUser(userProvider.getUsers().first.id);
|
||||
settings.update(lastAccountId: userProvider.id);
|
||||
}
|
||||
}
|
||||
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.isEmpty) 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<Map<Week, List<Lesson>>> getLessons({required String userId}) async {
|
||||
List<Map> userData =
|
||||
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
|
||||
if (userData.isEmpty) return {};
|
||||
String? lessonsJson = userData.elementAt(0)["timetable"] as String?;
|
||||
if (lessonsJson == null) return {};
|
||||
if (jsonDecode(lessonsJson) is List) return {};
|
||||
Map<Week, List<Lesson>> lessons =
|
||||
(jsonDecode(lessonsJson) as Map).cast<String, List>().map((key, value) {
|
||||
return MapEntry(
|
||||
Week.fromId(int.parse(key)),
|
||||
value
|
||||
.cast<Map<String, Object?>>()
|
||||
.map((e) => Lesson.fromJson(e))
|
||||
.toList());
|
||||
}).cast();
|
||||
return lessons;
|
||||
}
|
||||
|
||||
Future<List<Exam>> getExams({required String userId}) async {
|
||||
List<Map> userData =
|
||||
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
|
||||
if (userData.isEmpty) 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.isEmpty) 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.isEmpty) 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<SendRecipient>> getRecipients({required String userId}) async {
|
||||
List<Map> userData =
|
||||
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
|
||||
if (userData.isEmpty) return [];
|
||||
String? recipientsJson = userData.elementAt(0)["recipients"] as String?;
|
||||
if (recipientsJson == null) return [];
|
||||
List<SendRecipient> recipients = (jsonDecode(recipientsJson) as List)
|
||||
.map((e) => SendRecipient.fromJson(
|
||||
e,
|
||||
SendRecipientType.fromJson(e != null
|
||||
? e['tipus']
|
||||
: {
|
||||
'azonosito': '',
|
||||
'kod': '',
|
||||
'leiras': '',
|
||||
'nev': '',
|
||||
'rovidNev': ''
|
||||
})))
|
||||
.toList();
|
||||
return recipients;
|
||||
}
|
||||
|
||||
Future<List<Note>> getNotes({required String userId}) async {
|
||||
List<Map> userData =
|
||||
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
|
||||
if (userData.isEmpty) 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.isEmpty) 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.isEmpty) return [];
|
||||
String? absencesJson = userData.elementAt(0)["absences"] as String?;
|
||||
if (absencesJson == null) return [];
|
||||
List<Absence> absences = (jsonDecode(absencesJson) as List)
|
||||
.map((e) => Absence.fromJson(e))
|
||||
.toList();
|
||||
return absences;
|
||||
}
|
||||
|
||||
Future<List<GroupAverage>> getGroupAverages({required String userId}) async {
|
||||
List<Map> userData =
|
||||
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
|
||||
if (userData.isEmpty) return [];
|
||||
String? groupAveragesJson =
|
||||
userData.elementAt(0)["group_averages"] as String?;
|
||||
if (groupAveragesJson == null) return [];
|
||||
List<GroupAverage> groupAverages = (jsonDecode(groupAveragesJson) as List)
|
||||
.map((e) => GroupAverage.fromJson(e))
|
||||
.toList();
|
||||
return groupAverages;
|
||||
}
|
||||
|
||||
Future<SubjectLessonCount> getSubjectLessonCount(
|
||||
{required String userId}) async {
|
||||
List<Map> userData =
|
||||
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
|
||||
if (userData.isEmpty) return SubjectLessonCount.fromMap({});
|
||||
String? lessonCountJson =
|
||||
userData.elementAt(0)["subject_lesson_count"] as String?;
|
||||
if (lessonCountJson == null) return SubjectLessonCount.fromMap({});
|
||||
SubjectLessonCount lessonCount =
|
||||
SubjectLessonCount.fromMap(jsonDecode(lessonCountJson) as Map);
|
||||
return lessonCount;
|
||||
}
|
||||
|
||||
Future<DateTime> lastSeenGrade({required String userId}) async {
|
||||
List<Map> userData =
|
||||
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
|
||||
if (userData.isEmpty) return DateTime(0);
|
||||
int? lastSeenDate = userData.elementAt(0)["last_seen_grade"] as int?;
|
||||
if (lastSeenDate == null) return DateTime(0);
|
||||
DateTime lastSeen = DateTime.fromMillisecondsSinceEpoch(lastSeenDate);
|
||||
return lastSeen;
|
||||
}
|
||||
|
||||
// renamed things
|
||||
Future<Map<String, String>> renamedSubjects({required String userId}) async {
|
||||
List<Map> userData =
|
||||
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
|
||||
if (userData.isEmpty) return {};
|
||||
String? renamedSubjectsJson =
|
||||
userData.elementAt(0)["renamed_subjects"] as String?;
|
||||
if (renamedSubjectsJson == null) return {};
|
||||
return (jsonDecode(renamedSubjectsJson) as Map)
|
||||
.map((key, value) => MapEntry(key.toString(), value.toString()));
|
||||
}
|
||||
|
||||
Future<Map<String, String>> renamedTeachers({required String userId}) async {
|
||||
List<Map> userData =
|
||||
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
|
||||
if (userData.isEmpty) return {};
|
||||
String? renamedTeachersJson =
|
||||
userData.elementAt(0)["renamed_teachers"] as String?;
|
||||
if (renamedTeachersJson == null) return {};
|
||||
return (jsonDecode(renamedTeachersJson) as Map)
|
||||
.map((key, value) => MapEntry(key.toString(), value.toString()));
|
||||
}
|
||||
|
||||
// goal planner
|
||||
Future<Map<String, String>> subjectGoalPlans({required String userId}) async {
|
||||
List<Map> userData =
|
||||
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
|
||||
if (userData.isEmpty) return {};
|
||||
String? goalPlansJson = userData.elementAt(0)["goal_plans"] as String?;
|
||||
if (goalPlansJson == null) return {};
|
||||
return (jsonDecode(goalPlansJson) as Map)
|
||||
.map((key, value) => MapEntry(key.toString(), value.toString()));
|
||||
}
|
||||
|
||||
Future<Map<String, String>> subjectGoalAverages(
|
||||
{required String userId}) async {
|
||||
List<Map> userData =
|
||||
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
|
||||
if (userData.isEmpty) return {};
|
||||
String? goalAvgsJson = userData.elementAt(0)["goal_averages"] as String?;
|
||||
if (goalAvgsJson == null) return {};
|
||||
return (jsonDecode(goalAvgsJson) as Map)
|
||||
.map((key, value) => MapEntry(key.toString(), value.toString()));
|
||||
}
|
||||
|
||||
Future<Map<String, String>> subjectGoalBefores(
|
||||
{required String userId}) async {
|
||||
List<Map> userData =
|
||||
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
|
||||
if (userData.isEmpty) return {};
|
||||
String? goalBeforesJson = userData.elementAt(0)["goal_befores"] as String?;
|
||||
if (goalBeforesJson == null) return {};
|
||||
return (jsonDecode(goalBeforesJson) as Map)
|
||||
.map((key, value) => MapEntry(key.toString(), value.toString()));
|
||||
}
|
||||
|
||||
Future<Map<String, String>> subjectGoalPinDates(
|
||||
{required String userId}) async {
|
||||
List<Map> userData =
|
||||
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
|
||||
if (userData.isEmpty) return {};
|
||||
String? goalPinDatesJson =
|
||||
userData.elementAt(0)["goal_pin_dates"] as String?;
|
||||
if (goalPinDatesJson == null) return {};
|
||||
return (jsonDecode(goalPinDatesJson) as Map)
|
||||
.map((key, value) => MapEntry(key.toString(), value.toString()));
|
||||
}
|
||||
|
||||
// get todo items and notes
|
||||
Future<Map<String, bool>> toDoItems({required String userId}) async {
|
||||
List<Map> userData =
|
||||
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
|
||||
if (userData.isEmpty) return {};
|
||||
String? toDoItemsJson = userData.elementAt(0)["todo_items"] as String?;
|
||||
if (toDoItemsJson == null) return {};
|
||||
return (jsonDecode(toDoItemsJson) as Map).map((key, value) =>
|
||||
MapEntry(key.toString(), value.toString().toLowerCase() == 'true'));
|
||||
}
|
||||
|
||||
Future<List<SelfNote>> getSelfNotes({required String userId}) async {
|
||||
List<Map> userData =
|
||||
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
|
||||
if (userData.isEmpty) return [];
|
||||
String? selfNotesJson = userData.elementAt(0)["self_notes"] as String?;
|
||||
if (selfNotesJson == null) return [];
|
||||
List<SelfNote> selfNotes = (jsonDecode(selfNotesJson) as List)
|
||||
.map((e) => SelfNote.fromJson(e))
|
||||
.toList();
|
||||
return selfNotes;
|
||||
}
|
||||
|
||||
// v5
|
||||
Future<Map<String, String>> getRoundings({required String userId}) async {
|
||||
List<Map> userData =
|
||||
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
|
||||
if (userData.isEmpty) return {};
|
||||
String? roundingsJson = userData.elementAt(0)["roundings"] as String?;
|
||||
if (roundingsJson == null) return {};
|
||||
return (jsonDecode(roundingsJson) as Map)
|
||||
.map((key, value) => MapEntry(key.toString(), value.toString()));
|
||||
}
|
||||
|
||||
Future<Map<String, String>> getGradeRarities({required String userId}) async {
|
||||
List<Map> userData =
|
||||
await db.query("user_data", where: "id = ?", whereArgs: [userId]);
|
||||
if (userData.isEmpty) return {};
|
||||
String? raritiesJson = userData.elementAt(0)["grade_rarities"] as String?;
|
||||
if (raritiesJson == null) return {};
|
||||
return (jsonDecode(raritiesJson) as Map)
|
||||
.map((key, value) => MapEntry(key.toString(), value.toString()));
|
||||
}
|
||||
}
|
||||
211
refilc/lib/database/store.dart
Normal file
211
refilc/lib/database/store.dart
Normal file
@@ -0,0 +1,211 @@
|
||||
import 'dart:convert';
|
||||
import 'package:refilc/models/self_note.dart';
|
||||
import 'package:refilc/models/subject_lesson_count.dart';
|
||||
import 'package:refilc_kreta_api/models/week.dart';
|
||||
// ignore: depend_on_referenced_packages
|
||||
import 'package:sqflite_common/sqlite_api.dart';
|
||||
|
||||
// Models
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/models/user.dart';
|
||||
import 'package:refilc_kreta_api/models/grade.dart';
|
||||
import 'package:refilc_kreta_api/models/lesson.dart';
|
||||
import 'package:refilc_kreta_api/models/exam.dart';
|
||||
import 'package:refilc_kreta_api/models/homework.dart';
|
||||
import 'package:refilc_kreta_api/models/message.dart';
|
||||
import 'package:refilc_kreta_api/models/note.dart';
|
||||
import 'package:refilc_kreta_api/models/event.dart';
|
||||
import 'package:refilc_kreta_api/models/absence.dart';
|
||||
import 'package:refilc_kreta_api/models/group_average.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.isNotEmpty) {
|
||||
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<void> 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<void> storeLessons(Map<Week, List<Lesson>?> lessons,
|
||||
{required String userId}) async {
|
||||
final map = lessons.map<String, List<Map<String, Object?>>>(
|
||||
(k, v) => MapEntry(k.id.toString(),
|
||||
v!.where((e) => e.json != null).map((e) => e.json!).toList().cast()),
|
||||
);
|
||||
String lessonsJson = jsonEncode(map);
|
||||
await db.update("user_data", {"timetable": lessonsJson},
|
||||
where: "id = ?", whereArgs: [userId]);
|
||||
}
|
||||
|
||||
Future<void> 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<void> 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<void> 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<void> storeRecipients(List<SendRecipient> recipients,
|
||||
{required String userId}) async {
|
||||
String recipientsJson = jsonEncode(recipients.map((e) => e.json).toList());
|
||||
await db.update("user_data", {"recipients": recipientsJson},
|
||||
where: "id = ?", whereArgs: [userId]);
|
||||
}
|
||||
|
||||
Future<void> 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<void> 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<void> 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]);
|
||||
}
|
||||
|
||||
Future<void> storeGroupAverages(List<GroupAverage> groupAverages,
|
||||
{required String userId}) async {
|
||||
String groupAveragesJson =
|
||||
jsonEncode(groupAverages.map((e) => e.json).toList());
|
||||
await db.update("user_data", {"group_averages": groupAveragesJson},
|
||||
where: "id = ?", whereArgs: [userId]);
|
||||
}
|
||||
|
||||
Future<void> storeSubjectLessonCount(SubjectLessonCount lessonCount,
|
||||
{required String userId}) async {
|
||||
String lessonCountJson = jsonEncode(lessonCount.toMap());
|
||||
await db.update("user_data", {"subject_lesson_count": lessonCountJson},
|
||||
where: "id = ?", whereArgs: [userId]);
|
||||
}
|
||||
|
||||
Future<void> storeLastSeenGrade(DateTime date,
|
||||
{required String userId}) async {
|
||||
int lastSeenDate = date.millisecondsSinceEpoch;
|
||||
await db.update("user_data", {"last_seen_grade": lastSeenDate},
|
||||
where: "id = ?", whereArgs: [userId]);
|
||||
}
|
||||
|
||||
// renamed things
|
||||
Future<void> storeRenamedSubjects(Map<String, String> subjects,
|
||||
{required String userId}) async {
|
||||
String renamedSubjectsJson = jsonEncode(subjects);
|
||||
await db.update("user_data", {"renamed_subjects": renamedSubjectsJson},
|
||||
where: "id = ?", whereArgs: [userId]);
|
||||
}
|
||||
|
||||
Future<void> storeRenamedTeachers(Map<String, String> teachers,
|
||||
{required String userId}) async {
|
||||
String renamedTeachersJson = jsonEncode(teachers);
|
||||
await db.update("user_data", {"renamed_teachers": renamedTeachersJson},
|
||||
where: "id = ?", whereArgs: [userId]);
|
||||
}
|
||||
|
||||
// goal planner
|
||||
Future<void> storeSubjectGoalPlans(Map<String, String> plans,
|
||||
{required String userId}) async {
|
||||
String goalPlansJson = jsonEncode(plans);
|
||||
await db.update("user_data", {"goal_plans": goalPlansJson},
|
||||
where: "id = ?", whereArgs: [userId]);
|
||||
}
|
||||
|
||||
Future<void> storeSubjectGoalAverages(Map<String, String> avgs,
|
||||
{required String userId}) async {
|
||||
String goalAvgsJson = jsonEncode(avgs);
|
||||
await db.update("user_data", {"goal_averages": goalAvgsJson},
|
||||
where: "id = ?", whereArgs: [userId]);
|
||||
}
|
||||
|
||||
Future<void> storeSubjectGoalBefores(Map<String, String> befores,
|
||||
{required String userId}) async {
|
||||
String goalBeforesJson = jsonEncode(befores);
|
||||
await db.update("user_data", {"goal_befores": goalBeforesJson},
|
||||
where: "id = ?", whereArgs: [userId]);
|
||||
}
|
||||
|
||||
Future<void> storeSubjectGoalPinDates(Map<String, String> dates,
|
||||
{required String userId}) async {
|
||||
String goalPinDatesJson = jsonEncode(dates);
|
||||
await db.update("user_data", {"goal_pin_dates": goalPinDatesJson},
|
||||
where: "id = ?", whereArgs: [userId]);
|
||||
}
|
||||
|
||||
// todo and notes
|
||||
Future<void> storeToDoItem(Map<String, bool> items,
|
||||
{required String userId}) async {
|
||||
String toDoItemsJson = jsonEncode(items);
|
||||
await db.update("user_data", {"todo_items": toDoItemsJson},
|
||||
where: "id = ?", whereArgs: [userId]);
|
||||
}
|
||||
|
||||
Future<void> storeSelfNotes(List<SelfNote> selfNotes,
|
||||
{required String userId}) async {
|
||||
String selfNotesJson = jsonEncode(selfNotes.map((e) => e.json).toList());
|
||||
await db.update("user_data", {"self_notes": selfNotesJson},
|
||||
where: "id = ?", whereArgs: [userId]);
|
||||
}
|
||||
|
||||
// v5
|
||||
Future<void> storeRoundings(Map<String, String> roundings,
|
||||
{required String userId}) async {
|
||||
String roundingsJson = jsonEncode(roundings);
|
||||
await db.update("user_data", {"roundings": roundingsJson},
|
||||
where: "id = ?", whereArgs: [userId]);
|
||||
}
|
||||
|
||||
Future<void> storeGradeRarities(Map<String, String> rarities,
|
||||
{required String userId}) async {
|
||||
String raritiesJson = jsonEncode(rarities);
|
||||
await db.update("user_data", {"grade_rarities": raritiesJson},
|
||||
where: "id = ?", whereArgs: [userId]);
|
||||
}
|
||||
}
|
||||
30
refilc/lib/database/struct.dart
Normal file
30
refilc/lib/database/struct.dart
Normal file
@@ -0,0 +1,30 @@
|
||||
class DatabaseStruct {
|
||||
final String table;
|
||||
final Map<String, dynamic> struct;
|
||||
|
||||
const DatabaseStruct(this.table, 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()} ${name == 'id' ? 'NOT NULL' : ''}";
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
List<String> columns = [];
|
||||
struct.forEach((key, value) {
|
||||
columns.add(_toDBfield(key, value));
|
||||
});
|
||||
return columns.join(",");
|
||||
}
|
||||
}
|
||||
65
refilc/lib/helpers/attachment_helper.dart
Normal file
65
refilc/lib/helpers/attachment_helper.dart
Normal file
@@ -0,0 +1,65 @@
|
||||
// ignore_for_file: use_build_context_synchronously
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:refilc/api/providers/user_provider.dart';
|
||||
import 'package:refilc/helpers/storage_helper.dart';
|
||||
import 'package:refilc_kreta_api/client/client.dart';
|
||||
import 'package:refilc_kreta_api/models/attachment.dart';
|
||||
import 'package:refilc_kreta_api/models/homework.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:open_filex/open_filex.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 OpenFilex.open("$downloads/$name");
|
||||
return result.type == ResultType.done;
|
||||
}
|
||||
}
|
||||
|
||||
extension HomeworkAttachmentHelper on HomeworkAttachment {
|
||||
Future<String> download(BuildContext context,
|
||||
{bool overwrite = false}) async {
|
||||
String downloads = await StorageHelper.downloadsPath();
|
||||
|
||||
if (!overwrite && await File("$downloads/$name").exists()) {
|
||||
return "$downloads/$name";
|
||||
}
|
||||
|
||||
String url = downloadUrl(
|
||||
Provider.of<UserProvider>(context, listen: false).instituteCode ?? "");
|
||||
Uint8List data = await Provider.of<KretaClient>(context, listen: false)
|
||||
.getAPI(url, 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 OpenFilex.open("$downloads/$name");
|
||||
return result.type == ResultType.done;
|
||||
}
|
||||
}
|
||||
25
refilc/lib/helpers/average_helper.dart
Normal file
25
refilc/lib/helpers/average_helper.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'package:refilc_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)));
|
||||
}
|
||||
|
||||
for (var e in grades) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
608
refilc/lib/helpers/notification_helper.dart
Normal file
608
refilc/lib/helpers/notification_helper.dart
Normal file
@@ -0,0 +1,608 @@
|
||||
import 'package:refilc/api/providers/database_provider.dart';
|
||||
import 'package:refilc/api/providers/status_provider.dart';
|
||||
import 'package:refilc/api/providers/user_provider.dart';
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/helpers/notification_helper.i18n.dart';
|
||||
import 'package:refilc_kreta_api/client/api.dart';
|
||||
import 'package:refilc_kreta_api/client/client.dart';
|
||||
import 'package:refilc_kreta_api/models/absence.dart';
|
||||
import 'package:refilc_kreta_api/models/grade.dart';
|
||||
import 'package:refilc_kreta_api/models/lesson.dart';
|
||||
import 'package:refilc_kreta_api/models/week.dart';
|
||||
import 'package:refilc_kreta_api/providers/grade_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/timetable_provider.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart'
|
||||
hide Message;
|
||||
import 'package:i18n_extension/i18n_widget.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:refilc_kreta_api/models/message.dart';
|
||||
|
||||
class NotificationsHelper {
|
||||
late DatabaseProvider database;
|
||||
late SettingsProvider settingsProvider;
|
||||
late UserProvider userProvider;
|
||||
late KretaClient kretaClient;
|
||||
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
||||
List<T> combineLists<T, K>(
|
||||
List<T> list1,
|
||||
List<T> list2,
|
||||
K Function(T) keyExtractor,
|
||||
) {
|
||||
Set<K> uniqueKeys = <K>{};
|
||||
List<T> combinedList = [];
|
||||
|
||||
for (T item in list1) {
|
||||
K key = keyExtractor(item);
|
||||
if (!uniqueKeys.contains(key)) {
|
||||
uniqueKeys.add(key);
|
||||
combinedList.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
for (T item in list2) {
|
||||
K key = keyExtractor(item);
|
||||
if (!uniqueKeys.contains(key)) {
|
||||
uniqueKeys.add(key);
|
||||
combinedList.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
return combinedList;
|
||||
}
|
||||
|
||||
String dayTitle(DateTime date) {
|
||||
try {
|
||||
return DateFormat("EEEE", I18n.locale.languageCode).format(date);
|
||||
} catch (e) {
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
void backgroundJob() async {
|
||||
// initialize providers
|
||||
database = DatabaseProvider();
|
||||
await database.init();
|
||||
settingsProvider = await database.query.getSettings(database);
|
||||
userProvider = await database.query.getUsers(settingsProvider);
|
||||
|
||||
if (userProvider.id != null && settingsProvider.notificationsEnabled) {
|
||||
// refresh kreta login
|
||||
final status = StatusProvider();
|
||||
kretaClient = KretaClient(
|
||||
user: userProvider, settings: settingsProvider, status: status);
|
||||
kretaClient.refreshLogin();
|
||||
if (settingsProvider.notificationsGradesEnabled) gradeNotification();
|
||||
if (settingsProvider.notificationsAbsencesEnabled) absenceNotification();
|
||||
if (settingsProvider.notificationsMessagesEnabled) messageNotification();
|
||||
if (settingsProvider.notificationsLessonsEnabled) lessonNotification();
|
||||
}
|
||||
}
|
||||
|
||||
void gradeNotification() async {
|
||||
// fetch grades
|
||||
GradeProvider gradeProvider = GradeProvider(
|
||||
settings: settingsProvider,
|
||||
user: userProvider,
|
||||
database: database,
|
||||
kreta: kretaClient);
|
||||
gradeProvider.fetch();
|
||||
List<Grade> grades =
|
||||
await database.userQuery.getGrades(userId: userProvider.id ?? "");
|
||||
DateTime lastSeenGrade =
|
||||
await database.userQuery.lastSeenGrade(userId: userProvider.id ?? "");
|
||||
|
||||
// loop through grades and see which hasn't been seen yet
|
||||
for (Grade grade in grades) {
|
||||
// if grade is not a normal grade (1-5), don't show it
|
||||
if ([1, 2, 3, 4, 5].contains(grade.value.value)) {
|
||||
// if the grade was added over a week ago, don't show it to avoid notification spam
|
||||
// it worked in reverse, cuz someone added a * -1 to it, but it has been fixed now :D
|
||||
// old code below
|
||||
// if (grade.seenDate.isAfter(lastSeenGrade) &&
|
||||
// grade.date.difference(DateTime.now()).inDays * -1 < 7) {
|
||||
// new code from here :P
|
||||
if (grade.seenDate.isAfter(lastSeenGrade) &&
|
||||
grade.date.difference(DateTime.now()).inDays < 7) {
|
||||
// send notificiation about new grade
|
||||
AndroidNotificationDetails androidNotificationDetails =
|
||||
AndroidNotificationDetails(
|
||||
'GRADES',
|
||||
'Jegyek',
|
||||
channelDescription: 'Értesítés jegyek beírásakor',
|
||||
importance: Importance.max,
|
||||
priority: Priority.max,
|
||||
color: settingsProvider.customAccentColor,
|
||||
ticker: 'Jegyek',
|
||||
);
|
||||
NotificationDetails notificationDetails =
|
||||
NotificationDetails(android: androidNotificationDetails);
|
||||
if (userProvider.getUsers().length == 1) {
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
grade.id.hashCode,
|
||||
"title_grade".i18n,
|
||||
"body_grade".i18n.fill(
|
||||
[
|
||||
grade.value.value.toString(),
|
||||
grade.subject.isRenamed &&
|
||||
settingsProvider.renamedSubjectsEnabled
|
||||
? grade.subject.renamedTo!
|
||||
: grade.subject.name
|
||||
],
|
||||
),
|
||||
notificationDetails,
|
||||
);
|
||||
} else {
|
||||
// multiple users are added, also display student name
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
grade.id.hashCode,
|
||||
"title_grade".i18n,
|
||||
"body_grade_multiuser".i18n.fill(
|
||||
[
|
||||
userProvider.displayName!,
|
||||
grade.value.value.toString(),
|
||||
grade.subject.isRenamed &&
|
||||
settingsProvider.renamedSubjectsEnabled
|
||||
? grade.subject.renamedTo!
|
||||
: grade.subject.name
|
||||
],
|
||||
),
|
||||
notificationDetails,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// set grade seen status
|
||||
gradeProvider.seenAll();
|
||||
}
|
||||
|
||||
void absenceNotification() async {
|
||||
// get absences from api
|
||||
List? absenceJson = await kretaClient
|
||||
.getAPI(KretaAPI.absences(userProvider.instituteCode ?? ""));
|
||||
List<Absence> storedAbsences =
|
||||
await database.userQuery.getAbsences(userId: userProvider.id!);
|
||||
if (absenceJson == null) {
|
||||
return;
|
||||
}
|
||||
// format api absences to correct format while preserving isSeen value
|
||||
List<Absence> absences = absenceJson.map((e) {
|
||||
Absence apiAbsence = Absence.fromJson(e);
|
||||
Absence storedAbsence = storedAbsences.firstWhere(
|
||||
(stored) => stored.id == apiAbsence.id,
|
||||
orElse: () => apiAbsence);
|
||||
apiAbsence.isSeen = storedAbsence.isSeen;
|
||||
return apiAbsence;
|
||||
}).toList();
|
||||
List<Absence> modifiedAbsences = [];
|
||||
if (absences != storedAbsences) {
|
||||
// remove absences that are not new
|
||||
absences.removeWhere((element) => storedAbsences.contains(element));
|
||||
for (Absence absence in absences) {
|
||||
if (!absence.isSeen) {
|
||||
absence.isSeen = true;
|
||||
modifiedAbsences.add(absence);
|
||||
AndroidNotificationDetails androidNotificationDetails =
|
||||
AndroidNotificationDetails(
|
||||
'ABSENCES',
|
||||
'Hiányzások',
|
||||
channelDescription: 'Értesítés hiányzások beírásakor',
|
||||
importance: Importance.max,
|
||||
priority: Priority.max,
|
||||
color: settingsProvider.customAccentColor,
|
||||
ticker: 'Hiányzások',
|
||||
);
|
||||
NotificationDetails notificationDetails =
|
||||
NotificationDetails(android: androidNotificationDetails);
|
||||
if (userProvider.getUsers().length == 1) {
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
absence.id.hashCode,
|
||||
"title_absence"
|
||||
.i18n, // https://discord.com/channels/1111649116020285532/1153273625206591528
|
||||
"body_absence".i18n.fill(
|
||||
[
|
||||
DateFormat("yyyy-MM-dd").format(absence.date),
|
||||
absence.subject.isRenamed &&
|
||||
settingsProvider.renamedSubjectsEnabled
|
||||
? absence.subject.renamedTo!
|
||||
: absence.subject.name
|
||||
],
|
||||
),
|
||||
notificationDetails,
|
||||
);
|
||||
} else {
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
absence.id.hashCode,
|
||||
"title_absence"
|
||||
.i18n, // https://discord.com/channels/1111649116020285532/1153273625206591528
|
||||
"body_absence_multiuser".i18n.fill(
|
||||
[
|
||||
userProvider.displayName!,
|
||||
DateFormat("yyyy-MM-dd").format(absence.date),
|
||||
absence.subject.isRenamed &&
|
||||
settingsProvider.renamedSubjectsEnabled
|
||||
? absence.subject.renamedTo!
|
||||
: absence.subject.name
|
||||
],
|
||||
),
|
||||
notificationDetails,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// combine modified absences and storedabsences list and save them to the database
|
||||
List<Absence> combinedAbsences = combineLists(
|
||||
modifiedAbsences,
|
||||
storedAbsences,
|
||||
(Absence absence) => absence.id,
|
||||
);
|
||||
await database.userStore
|
||||
.storeAbsences(combinedAbsences, userId: userProvider.id!);
|
||||
}
|
||||
|
||||
void messageNotification() async {
|
||||
// get messages from api
|
||||
List? messageJson =
|
||||
await kretaClient.getAPI(KretaAPI.messages("beerkezett"));
|
||||
List<Message> storedmessages =
|
||||
await database.userQuery.getMessages(userId: userProvider.id!);
|
||||
if (messageJson == null) {
|
||||
return;
|
||||
}
|
||||
// format api messages to correct format while preserving isSeen value
|
||||
// Parse messages
|
||||
List<Message> messages = [];
|
||||
await Future.wait(List.generate(messageJson.length, (index) {
|
||||
return () async {
|
||||
Map message = messageJson.cast<Map>()[index];
|
||||
Map? innerMessageJson = await kretaClient
|
||||
.getAPI(KretaAPI.message(message["azonosito"].toString()));
|
||||
if (innerMessageJson != null) {
|
||||
messages.add(
|
||||
Message.fromJson(innerMessageJson, forceType: MessageType.inbox));
|
||||
}
|
||||
}();
|
||||
}));
|
||||
|
||||
for (Message message in messages) {
|
||||
for (Message storedMessage in storedmessages) {
|
||||
if (message.id == storedMessage.id) {
|
||||
message.isSeen = storedMessage.isSeen;
|
||||
}
|
||||
}
|
||||
}
|
||||
List<Message> modifiedmessages = [];
|
||||
if (messages != storedmessages) {
|
||||
// remove messages that are not new
|
||||
messages.removeWhere((element) => storedmessages.contains(element));
|
||||
for (Message message in messages) {
|
||||
if (!message.isSeen) {
|
||||
message.isSeen = true;
|
||||
modifiedmessages.add(message);
|
||||
AndroidNotificationDetails androidNotificationDetails =
|
||||
AndroidNotificationDetails(
|
||||
'MESSAGES',
|
||||
'Üzenetek',
|
||||
channelDescription: 'Értesítés kapott üzenetekkor',
|
||||
importance: Importance.max,
|
||||
priority: Priority.max,
|
||||
color: settingsProvider.customAccentColor,
|
||||
ticker: 'Üzenetek',
|
||||
);
|
||||
NotificationDetails notificationDetails =
|
||||
NotificationDetails(android: androidNotificationDetails);
|
||||
if (userProvider.getUsers().length == 1) {
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
message.id.hashCode,
|
||||
message.author,
|
||||
message.content.replaceAll(RegExp(r'<[^>]*>'), ''),
|
||||
notificationDetails,
|
||||
);
|
||||
} else {
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
message.id.hashCode,
|
||||
"(${userProvider.displayName!}) ${message.author}",
|
||||
message.content.replaceAll(RegExp(r'<[^>]*>'), ''),
|
||||
notificationDetails,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// combine modified messages and storedmessages list and save them to the database
|
||||
List<Message> combinedmessages = combineLists(
|
||||
modifiedmessages,
|
||||
storedmessages,
|
||||
(Message message) => message.id,
|
||||
);
|
||||
await database.userStore
|
||||
.storeMessages(combinedmessages, userId: userProvider.id!);
|
||||
}
|
||||
|
||||
void lessonNotification() async {
|
||||
// get lesson from api
|
||||
TimetableProvider timetableProvider = TimetableProvider(
|
||||
user: userProvider, database: database, kreta: kretaClient);
|
||||
List<Lesson> storedlessons =
|
||||
timetableProvider.lessons[Week.current()] ?? [];
|
||||
List? apilessons = timetableProvider.getWeek(Week.current()) ?? [];
|
||||
for (Lesson lesson in apilessons) {
|
||||
for (Lesson storedLesson in storedlessons) {
|
||||
if (lesson.id == storedLesson.id) {
|
||||
lesson.isSeen = storedLesson.isSeen;
|
||||
}
|
||||
}
|
||||
}
|
||||
List<Lesson> modifiedlessons = [];
|
||||
if (apilessons != storedlessons) {
|
||||
// remove lessons that are not new
|
||||
apilessons.removeWhere((element) => storedlessons.contains(element));
|
||||
for (Lesson lesson in apilessons) {
|
||||
if (!lesson.isSeen && lesson.isChanged) {
|
||||
lesson.isSeen = true;
|
||||
modifiedlessons.add(lesson);
|
||||
AndroidNotificationDetails androidNotificationDetails =
|
||||
AndroidNotificationDetails(
|
||||
'LESSONS',
|
||||
'Órák',
|
||||
channelDescription:
|
||||
'Értesítés órák elmaradásáról, helyettesítésről',
|
||||
importance: Importance.max,
|
||||
priority: Priority.max,
|
||||
color: settingsProvider.customAccentColor,
|
||||
ticker: 'Órák',
|
||||
);
|
||||
NotificationDetails notificationDetails =
|
||||
NotificationDetails(android: androidNotificationDetails);
|
||||
if (userProvider.getUsers().length == 1) {
|
||||
if (lesson.status?.name == "Elmaradt") {
|
||||
switch (I18n.localeStr) {
|
||||
case "en_en":
|
||||
{
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
lesson.id.hashCode,
|
||||
"title_lesson".i18n,
|
||||
"body_lesson_canceled".i18n.fill(
|
||||
[
|
||||
lesson.lessonIndex,
|
||||
lesson.name,
|
||||
dayTitle(lesson.date)
|
||||
],
|
||||
),
|
||||
notificationDetails,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "hu_hu":
|
||||
{
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
lesson.id.hashCode,
|
||||
"title_lesson".i18n,
|
||||
"body_lesson_canceled".i18n.fill(
|
||||
[
|
||||
dayTitle(lesson.date),
|
||||
lesson.lessonIndex,
|
||||
lesson.name
|
||||
],
|
||||
),
|
||||
notificationDetails,
|
||||
);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
lesson.id.hashCode,
|
||||
"title_lesson".i18n,
|
||||
"body_lesson_canceled".i18n.fill(
|
||||
[
|
||||
lesson.lessonIndex,
|
||||
lesson.name,
|
||||
dayTitle(lesson.date)
|
||||
],
|
||||
),
|
||||
notificationDetails,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (lesson.substituteTeacher?.name != "") {
|
||||
switch (I18n.localeStr) {
|
||||
case "en_en":
|
||||
{
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
lesson.id.hashCode,
|
||||
"title_lesson".i18n,
|
||||
"body_lesson_substituted".i18n.fill(
|
||||
[
|
||||
lesson.lessonIndex,
|
||||
lesson.name,
|
||||
dayTitle(lesson.date),
|
||||
lesson.substituteTeacher!.isRenamed
|
||||
? lesson.substituteTeacher!.renamedTo!
|
||||
: lesson.substituteTeacher!.name
|
||||
],
|
||||
),
|
||||
notificationDetails,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "hu_hu":
|
||||
{
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
lesson.id.hashCode,
|
||||
"title_lesson".i18n,
|
||||
"body_lesson_substituted".i18n.fill(
|
||||
[
|
||||
dayTitle(lesson.date),
|
||||
lesson.lessonIndex,
|
||||
lesson.name,
|
||||
lesson.substituteTeacher!.isRenamed
|
||||
? lesson.substituteTeacher!.renamedTo!
|
||||
: lesson.substituteTeacher!.name
|
||||
],
|
||||
),
|
||||
notificationDetails,
|
||||
);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
lesson.id.hashCode,
|
||||
"title_lesson".i18n,
|
||||
"body_lesson_substituted".i18n.fill(
|
||||
[
|
||||
lesson.lessonIndex,
|
||||
lesson.name,
|
||||
dayTitle(lesson.date),
|
||||
lesson.substituteTeacher!.isRenamed
|
||||
? lesson.substituteTeacher!.renamedTo!
|
||||
: lesson.substituteTeacher!.name
|
||||
],
|
||||
),
|
||||
notificationDetails,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (lesson.status?.name == "Elmaradt") {
|
||||
switch (I18n.localeStr) {
|
||||
case "en_en":
|
||||
{
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
lesson.id.hashCode,
|
||||
"title_lesson".i18n,
|
||||
"body_lesson_canceled".i18n.fill(
|
||||
[
|
||||
userProvider.displayName!,
|
||||
lesson.lessonIndex,
|
||||
lesson.name,
|
||||
dayTitle(lesson.date)
|
||||
],
|
||||
),
|
||||
notificationDetails,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "hu_hu":
|
||||
{
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
lesson.id.hashCode,
|
||||
"title_lesson".i18n,
|
||||
"body_lesson_canceled".i18n.fill(
|
||||
[
|
||||
userProvider.displayName!,
|
||||
dayTitle(lesson.date),
|
||||
lesson.lessonIndex,
|
||||
lesson.name
|
||||
],
|
||||
),
|
||||
notificationDetails,
|
||||
);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
lesson.id.hashCode,
|
||||
"title_lesson".i18n,
|
||||
"body_lesson_canceled".i18n.fill(
|
||||
[
|
||||
userProvider.displayName!,
|
||||
lesson.lessonIndex,
|
||||
lesson.name,
|
||||
dayTitle(lesson.date)
|
||||
],
|
||||
),
|
||||
notificationDetails,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (lesson.substituteTeacher?.name != "") {
|
||||
switch (I18n.localeStr) {
|
||||
case "en_en":
|
||||
{
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
lesson.id.hashCode,
|
||||
"title_lesson".i18n,
|
||||
"body_lesson_substituted".i18n.fill(
|
||||
[
|
||||
userProvider.displayName!,
|
||||
lesson.lessonIndex,
|
||||
lesson.name,
|
||||
dayTitle(lesson.date),
|
||||
lesson.substituteTeacher!.isRenamed
|
||||
? lesson.substituteTeacher!.renamedTo!
|
||||
: lesson.substituteTeacher!.name
|
||||
],
|
||||
),
|
||||
notificationDetails,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "hu_hu":
|
||||
{
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
lesson.id.hashCode,
|
||||
"title_lesson".i18n,
|
||||
"body_lesson_substituted".i18n.fill(
|
||||
[
|
||||
userProvider.displayName!,
|
||||
dayTitle(lesson.date),
|
||||
lesson.lessonIndex,
|
||||
lesson.name,
|
||||
lesson.substituteTeacher!.isRenamed
|
||||
? lesson.substituteTeacher!.renamedTo!
|
||||
: lesson.substituteTeacher!.name
|
||||
],
|
||||
),
|
||||
notificationDetails,
|
||||
);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
lesson.id.hashCode,
|
||||
"title_lesson".i18n,
|
||||
"body_lesson_substituted".i18n.fill(
|
||||
[
|
||||
userProvider.displayName!,
|
||||
lesson.lessonIndex,
|
||||
lesson.name,
|
||||
dayTitle(lesson.date),
|
||||
lesson.substituteTeacher!.isRenamed
|
||||
? lesson.substituteTeacher!.renamedTo!
|
||||
: lesson.substituteTeacher!.name
|
||||
],
|
||||
),
|
||||
notificationDetails,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// combine modified lesson and storedlesson list and save them to the database
|
||||
List<Lesson> combinedlessons = combineLists(
|
||||
modifiedlessons,
|
||||
storedlessons,
|
||||
(Lesson message) => message.id,
|
||||
);
|
||||
Map<Week, List<Lesson>> timetableLessons = timetableProvider.lessons;
|
||||
timetableLessons[Week.current()] = combinedlessons;
|
||||
await database.userStore
|
||||
.storeLessons(timetableLessons, userId: userProvider.id!);
|
||||
}
|
||||
}
|
||||
}
|
||||
51
refilc/lib/helpers/notification_helper.i18n.dart
Normal file
51
refilc/lib/helpers/notification_helper.i18n.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
import 'package:i18n_extension/i18n_extension.dart';
|
||||
|
||||
extension Localization on String {
|
||||
static final _t = Translations.byLocale("hu_hu") +
|
||||
{
|
||||
"en_en": {
|
||||
"title_grade": "New grade",
|
||||
"body_grade": "You got a %s in %s",
|
||||
"body_grade_multiuser": "%s got a %s in %s",
|
||||
"title_absence": "Absence recorded",
|
||||
"body_absence": "An absence was recorded on %s for %s",
|
||||
"body_absence_multiuser": "An absence was recorded for %s on %s for the subject %s",
|
||||
"title_lesson": "Timetable modified",
|
||||
"body_lesson_canceled": "Lesson #%s (%s) has been canceled on %s",
|
||||
"body_lesson_canceled_multiuser": "(%s) Lesson #%s (%s) has been canceled on %s",
|
||||
"body_lesson_substituted": "Lesson #%s (%s) on %s will be substituted by %s",
|
||||
"body_lesson_substituted_multiuser": "(%s) Lesson #%s (%s) on %s will be substituted by %s"
|
||||
},
|
||||
"hu_hu": {
|
||||
"title_grade": "Új jegy",
|
||||
"body_grade": "%s-st kaptál %s tantárgyból",
|
||||
"body_grade_multiuser": "%s tanuló %s-st kapott %s tantárgyból",
|
||||
"title_absence": "Új hiányzás",
|
||||
"body_absence": "Új hiányzást kaptál %s napon %s tantárgyból",
|
||||
"body_absence_multiuser": "%s tanuló új hiányzást kapott %s napon %s tantárgyból",
|
||||
"title_lesson": "Órarend szerkesztve",
|
||||
"body_lesson_canceled": "%s-i %s. óra (%s) elmarad",
|
||||
"body_lesson_canceled_multiuser": "(%s) %s-i %s. óra (%s) elmarad",
|
||||
"body_lesson_substituted": "%s-i %s. (%s) órát %s helyettesíti",
|
||||
"body_lesson_substituted_multiuser": "(%s) %s-i %s. (%s) órát %s helyettesíti"
|
||||
},
|
||||
"de_de": {
|
||||
"title_grade": "Neue Note",
|
||||
"body_grade": "Du hast eine %s in %s",
|
||||
"body_grade_multiuser": "%s hast eine %s in %s",
|
||||
"title_absence": "Abwesenheit aufgezeichnet",
|
||||
"body_absence": "Auf %s für %s wurde eine Abwesenheit aufgezeichnet",
|
||||
"body_absence_multiuser": "Für %s wurde am %s für das Thema Mathematik eine Abwesenheit aufgezeichnet",
|
||||
"title_lesson": "Fahrplan geändert",
|
||||
"body_lesson_canceled": "Lektion Nr. %s (%s) wurde am %s abgesagt",
|
||||
"body_lesson_canceled_multiuser": "(%s) Lektion Nr. %s (%s) wurde am %s abgesagt",
|
||||
"body_lesson_substituted": "Lektion Nr. %s (%s) wird am %s durch %s ersetzt",
|
||||
"body_lesson_substituted_multiuser": "(%s) Lektion Nr. %s (%s) wird am %s durch %s ersetzt"
|
||||
},
|
||||
};
|
||||
|
||||
String get i18n => localize(this, _t);
|
||||
String fill(List<Object> params) => localizeFill(this, params);
|
||||
String plural(int value) => localizePlural(value, this, _t);
|
||||
String version(Object modifier) => localizeVersion(modifier, this, _t);
|
||||
}
|
||||
48
refilc/lib/helpers/quick_actions.dart
Normal file
48
refilc/lib/helpers/quick_actions.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:quick_actions/quick_actions.dart';
|
||||
import 'package:refilc_mobile_ui/common/screens.i18n.dart';
|
||||
|
||||
const QuickActions quickActions = QuickActions();
|
||||
|
||||
void setupQuickActions() {
|
||||
quickActions.setShortcutItems(<ShortcutItem>[
|
||||
ShortcutItem(
|
||||
type: 'action_grades',
|
||||
localizedTitle: 'grades'.i18n,
|
||||
icon: 'ic_grades'),
|
||||
ShortcutItem(
|
||||
type: 'action_timetable',
|
||||
localizedTitle: 'timetable'.i18n,
|
||||
icon: 'ic_timetable'),
|
||||
ShortcutItem(
|
||||
type: 'action_messages',
|
||||
localizedTitle: 'messages'.i18n,
|
||||
icon: 'ic_messages'),
|
||||
ShortcutItem(
|
||||
type: 'action_absences',
|
||||
localizedTitle: 'absences'.i18n,
|
||||
icon: 'ic_absences')
|
||||
]);
|
||||
}
|
||||
|
||||
void handleQuickActions(BuildContext context, void Function(String) callback) {
|
||||
quickActions.initialize((shortcutType) {
|
||||
switch (shortcutType) {
|
||||
case 'action_home':
|
||||
callback("home");
|
||||
break;
|
||||
case 'action_grades':
|
||||
callback("grades");
|
||||
break;
|
||||
case 'action_timetable':
|
||||
callback("timetable");
|
||||
break;
|
||||
case 'action_messages':
|
||||
callback("messages");
|
||||
break;
|
||||
case 'action_absences':
|
||||
callback("absences");
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
18
refilc/lib/helpers/share_helper.dart
Normal file
18
refilc/lib/helpers/share_helper.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
import 'package:refilc/helpers/attachment_helper.dart';
|
||||
import 'package:refilc_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);
|
||||
// ignore: deprecated_member_use
|
||||
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);
|
||||
}
|
||||
}
|
||||
39
refilc/lib/helpers/storage_helper.dart
Normal file
39
refilc/lib/helpers/storage_helper.dart
Normal file
@@ -0,0 +1,39 @@
|
||||
// ignore_for_file: avoid_print
|
||||
|
||||
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.manageExternalStorage.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;
|
||||
// return (await getTemporaryDirectory()).path;
|
||||
}
|
||||
}
|
||||
292
refilc/lib/helpers/subject.dart
Normal file
292
refilc/lib/helpers/subject.dart
Normal file
@@ -0,0 +1,292 @@
|
||||
import 'package:refilc/icons/filc_icons.dart';
|
||||
import 'package:refilc/models/icon_pack.dart';
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/utils/format.dart';
|
||||
import 'package:refilc_kreta_api/models/subject.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
typedef SubjectIconVariants = Map<IconPack, IconData>;
|
||||
|
||||
class SubjectIconData {
|
||||
final SubjectIconVariants data;
|
||||
final String name; // for iOS live activities compatibilty
|
||||
|
||||
SubjectIconData({
|
||||
this.data = const {
|
||||
IconPack.material: Icons.widgets_outlined,
|
||||
IconPack.cupertino: CupertinoIcons.rectangle_grid_2x2,
|
||||
},
|
||||
this.name = "square.grid.2x2",
|
||||
});
|
||||
}
|
||||
|
||||
SubjectIconVariants createIcon(
|
||||
{required IconData material, required IconData cupertino}) {
|
||||
return {
|
||||
IconPack.material: material,
|
||||
IconPack.cupertino: cupertino,
|
||||
};
|
||||
}
|
||||
|
||||
class SubjectIcon {
|
||||
static String resolveName({GradeSubject? subject, String? subjectName}) =>
|
||||
_resolve(subject: subject, subjectName: subjectName).name;
|
||||
static IconData resolveVariant(
|
||||
{GradeSubject? subject,
|
||||
String? subjectName,
|
||||
required BuildContext context}) =>
|
||||
_resolve(subject: subject, subjectName: subjectName).data[
|
||||
Provider.of<SettingsProvider>(context, listen: false).iconPack]!;
|
||||
|
||||
static SubjectIconData _resolve(
|
||||
{GradeSubject? subject, String? subjectName}) {
|
||||
assert(!(subject == null && subjectName == null));
|
||||
|
||||
String name = (subject?.name ?? subjectName ?? "")
|
||||
.toLowerCase()
|
||||
.specialChars()
|
||||
.trim();
|
||||
String category =
|
||||
subject?.category.description.toLowerCase().specialChars() ?? "";
|
||||
|
||||
// todo: check for categories
|
||||
if (RegExp("mate(k|matika)").hasMatch(name) || category == "matematika") {
|
||||
return SubjectIconData(
|
||||
data: createIcon(
|
||||
cupertino: CupertinoIcons.function,
|
||||
material: Icons.calculate_outlined),
|
||||
name: "function");
|
||||
} else if (RegExp("magyar nyelv|nyelvtan").hasMatch(name)) {
|
||||
return SubjectIconData(
|
||||
data: createIcon(
|
||||
cupertino: CupertinoIcons.textformat_alt,
|
||||
material: Icons.spellcheck_outlined),
|
||||
name: "textformat.alt");
|
||||
} else if (RegExp("irodalom").hasMatch(name)) {
|
||||
return SubjectIconData(
|
||||
data: createIcon(
|
||||
cupertino: CupertinoIcons.book,
|
||||
material: Icons.menu_book_outlined),
|
||||
name: "book");
|
||||
} else if (RegExp("tor(i|tenelem)").hasMatch(name)) {
|
||||
return SubjectIconData(
|
||||
data: createIcon(
|
||||
cupertino: CupertinoIcons.compass,
|
||||
material: Icons.hourglass_empty_outlined),
|
||||
name: "safari");
|
||||
} else if (RegExp("foldrajz").hasMatch(name)) {
|
||||
return SubjectIconData(
|
||||
data: createIcon(
|
||||
cupertino: CupertinoIcons.map, material: Icons.public_outlined),
|
||||
name: "map");
|
||||
} else if (RegExp("rajz|muvtori|muveszet|vizualis").hasMatch(name)) {
|
||||
return SubjectIconData(
|
||||
data: createIcon(
|
||||
cupertino: CupertinoIcons.paintbrush,
|
||||
material: Icons.palette_outlined),
|
||||
name: "paintbrush");
|
||||
} else if (RegExp("fizika").hasMatch(name)) {
|
||||
return SubjectIconData(
|
||||
data: createIcon(
|
||||
cupertino: CupertinoIcons.lightbulb,
|
||||
material: Icons.emoji_objects_outlined),
|
||||
name: "lightbulb");
|
||||
} else if (RegExp("^enek|zene|szolfezs|zongora|korus").hasMatch(name)) {
|
||||
return SubjectIconData(
|
||||
data: createIcon(
|
||||
cupertino: CupertinoIcons.music_note,
|
||||
material: Icons.music_note_outlined),
|
||||
name: "music.note");
|
||||
} else if (RegExp("^tes(i|tneveles)|sport").hasMatch(name)) {
|
||||
return SubjectIconData(
|
||||
data: createIcon(
|
||||
cupertino: CupertinoIcons.sportscourt,
|
||||
material: Icons.sports_soccer_outlined),
|
||||
name: "sportscourt");
|
||||
} else if (RegExp("kemia").hasMatch(name)) {
|
||||
return SubjectIconData(
|
||||
data: createIcon(
|
||||
cupertino: CupertinoIcons.lab_flask,
|
||||
material: Icons.science_outlined),
|
||||
name: "testtube.2");
|
||||
} else if (RegExp("biologia").hasMatch(name)) {
|
||||
return SubjectIconData(
|
||||
data: createIcon(
|
||||
cupertino: CupertinoIcons.paw, material: Icons.pets_outlined),
|
||||
name: "pawprint");
|
||||
} else if (RegExp(
|
||||
"kornyezet|termeszet ?(tudomany|ismeret)|hon( es nep)?ismeret")
|
||||
.hasMatch(name)) {
|
||||
return SubjectIconData(
|
||||
data: createIcon(
|
||||
cupertino: CupertinoIcons.arrow_3_trianglepath,
|
||||
material: Icons.eco_outlined),
|
||||
name: "arrow.3.trianglepath");
|
||||
} else if (RegExp("(hit|erkolcs)tan|vallas|etika").hasMatch(name)) {
|
||||
return SubjectIconData(
|
||||
data: createIcon(
|
||||
cupertino: CupertinoIcons.heart,
|
||||
material: Icons.favorite_border_outlined),
|
||||
name: "heart");
|
||||
} else if (RegExp("penzugy").hasMatch(name)) {
|
||||
return SubjectIconData(
|
||||
data: createIcon(
|
||||
cupertino: CupertinoIcons.money_dollar,
|
||||
material: Icons.savings_outlined),
|
||||
name: "dollarsign");
|
||||
} else if (RegExp("informatika|szoftver|iroda|digitalis").hasMatch(name)) {
|
||||
return SubjectIconData(
|
||||
data: createIcon(
|
||||
cupertino: CupertinoIcons.device_laptop,
|
||||
material: Icons.computer_outlined),
|
||||
name: "laptopcomputer");
|
||||
} else if (RegExp("prog").hasMatch(name)) {
|
||||
return SubjectIconData(
|
||||
data: createIcon(
|
||||
cupertino: CupertinoIcons.chevron_left_slash_chevron_right,
|
||||
material: Icons.code_outlined),
|
||||
name: "chevron.left.forwardslash.chevron.right");
|
||||
} else if (RegExp("halozat").hasMatch(name)) {
|
||||
return SubjectIconData(
|
||||
data: createIcon(
|
||||
cupertino: CupertinoIcons.antenna_radiowaves_left_right,
|
||||
material: Icons.wifi_tethering_outlined),
|
||||
name: "antenna.radiowaves.left.and.right");
|
||||
} else if (RegExp("szinhaz").hasMatch(name)) {
|
||||
return SubjectIconData(
|
||||
data: createIcon(
|
||||
cupertino: CupertinoIcons.hifispeaker,
|
||||
material: Icons.theater_comedy_outlined),
|
||||
name: "hifispeaker");
|
||||
} else if (RegExp("film|media").hasMatch(name)) {
|
||||
return SubjectIconData(
|
||||
data: createIcon(
|
||||
cupertino: CupertinoIcons.film,
|
||||
material: Icons.theaters_outlined),
|
||||
name: "film");
|
||||
} else if (RegExp("elektro(tech)?nika").hasMatch(name)) {
|
||||
return SubjectIconData(
|
||||
data: createIcon(
|
||||
cupertino: CupertinoIcons.bolt,
|
||||
material: Icons.electrical_services_outlined),
|
||||
name: "bolt");
|
||||
} else if (RegExp("gepesz|mernok|ipar").hasMatch(name)) {
|
||||
return SubjectIconData(
|
||||
data: createIcon(
|
||||
cupertino: CupertinoIcons.wrench,
|
||||
material: Icons.precision_manufacturing_outlined),
|
||||
name: "wrench");
|
||||
} else if (RegExp("technika").hasMatch(name)) {
|
||||
return SubjectIconData(
|
||||
data: createIcon(
|
||||
cupertino: CupertinoIcons.hammer, material: Icons.build_outlined),
|
||||
name: "hammer");
|
||||
} else if (RegExp("tanc").hasMatch(name)) {
|
||||
return SubjectIconData(
|
||||
data: createIcon(
|
||||
cupertino: CupertinoIcons.music_mic,
|
||||
material: Icons.speaker_outlined),
|
||||
name: "music.mic");
|
||||
} else if (RegExp("filozofia").hasMatch(name)) {
|
||||
return SubjectIconData(
|
||||
data: createIcon(
|
||||
cupertino: CupertinoIcons.bubble_left,
|
||||
material: Icons.psychology_outlined),
|
||||
name: "bubble.left");
|
||||
} else if (RegExp("osztaly(fonoki|kozosseg)").hasMatch(name) ||
|
||||
name == "ofo") {
|
||||
return SubjectIconData(
|
||||
data: createIcon(
|
||||
cupertino: CupertinoIcons.group, material: Icons.groups_outlined),
|
||||
name: "person.3");
|
||||
} else if (RegExp("gazdasag").hasMatch(name)) {
|
||||
return SubjectIconData(
|
||||
data: createIcon(
|
||||
cupertino: CupertinoIcons.chart_pie,
|
||||
material: Icons.account_balance_outlined),
|
||||
name: "chart.pie");
|
||||
} else if (RegExp("szorgalom").hasMatch(name)) {
|
||||
return SubjectIconData(
|
||||
data: createIcon(
|
||||
cupertino: CupertinoIcons.checkmark_seal,
|
||||
material: Icons.verified_outlined),
|
||||
name: "checkmark.seal");
|
||||
} else if (RegExp("magatartas").hasMatch(name)) {
|
||||
return SubjectIconData(
|
||||
data: createIcon(
|
||||
cupertino: CupertinoIcons.smiley,
|
||||
material: Icons.emoji_people_outlined),
|
||||
name: "face.smiling");
|
||||
} else if (RegExp(
|
||||
"angol|nemet|francia|olasz|orosz|spanyol|latin|kinai|nyelv")
|
||||
.hasMatch(name)) {
|
||||
return SubjectIconData(
|
||||
data: createIcon(
|
||||
cupertino: CupertinoIcons.globe,
|
||||
material: Icons.translate_outlined),
|
||||
name: "globe");
|
||||
} else if (RegExp("linux").hasMatch(name)) {
|
||||
return SubjectIconData(
|
||||
data: createIcon(
|
||||
material: FilcIcons.linux, cupertino: FilcIcons.linux));
|
||||
} else if (RegExp("adatbazis").hasMatch(name)) {
|
||||
return SubjectIconData(
|
||||
data: createIcon(
|
||||
cupertino: CupertinoIcons.table_badge_more,
|
||||
material: Icons.table_chart),
|
||||
name: "table.badge.more");
|
||||
} else if (RegExp("asztali alkalmazasok").hasMatch(name)) {
|
||||
return SubjectIconData(
|
||||
data: createIcon(
|
||||
cupertino: CupertinoIcons.macwindow,
|
||||
material: Icons.desktop_windows_outlined),
|
||||
name: "macwindow");
|
||||
} else if (RegExp("projekt").hasMatch(name)) {
|
||||
return SubjectIconData(
|
||||
data: createIcon(
|
||||
cupertino: CupertinoIcons.person_3_fill,
|
||||
material: Icons.groups_3),
|
||||
name: "person.3.fill");
|
||||
}
|
||||
|
||||
return SubjectIconData();
|
||||
}
|
||||
}
|
||||
|
||||
class ShortSubject {
|
||||
static String resolve({GradeSubject? subject, String? subjectName}) {
|
||||
assert(!(subject == null && subjectName == null));
|
||||
|
||||
String name = (subject?.name ?? subjectName ?? "")
|
||||
.toLowerCase()
|
||||
.specialChars()
|
||||
.trim();
|
||||
// String category = subject?.category.description.toLowerCase().specialChars() ?? "";
|
||||
|
||||
if (RegExp("magyar irodalom").hasMatch(name)) {
|
||||
return "Irodalom";
|
||||
} else if (RegExp("magyar nyelv").hasMatch(name)) {
|
||||
return "Nyelvtan";
|
||||
} else if (RegExp("matematika").hasMatch(name)) {
|
||||
return "Matek";
|
||||
} else if (RegExp("digitalis kultura").hasMatch(name)) {
|
||||
return "Dig. kult.";
|
||||
} else if (RegExp("testneveles").hasMatch(name)) {
|
||||
return "Tesi";
|
||||
} else if (RegExp("tortenelem").hasMatch(name)) {
|
||||
return "Töri";
|
||||
} else if (RegExp(
|
||||
"(angol|nemet|francia|olasz|orosz|spanyol|latin|kinai) nyelv")
|
||||
.hasMatch(name)) {
|
||||
return (subject?.name ?? subjectName ?? "?").replaceAll(" nyelv", "");
|
||||
} else if (RegExp("informatika").hasMatch(name)) {
|
||||
return "Infó";
|
||||
} else if (RegExp("osztalyfonoki").hasMatch(name)) {
|
||||
return "Ofő";
|
||||
}
|
||||
|
||||
return subject?.name.capital() ?? subjectName?.capital() ?? "?";
|
||||
}
|
||||
}
|
||||
80
refilc/lib/helpers/update_helper.dart
Normal file
80
refilc/lib/helpers/update_helper.dart
Normal file
@@ -0,0 +1,80 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:refilc/api/client.dart';
|
||||
import 'package:refilc/helpers/storage_helper.dart';
|
||||
import 'package:refilc/models/release.dart';
|
||||
import 'package:open_filex/open_filex.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
enum UpdateState { none, preparing, downloading, installing }
|
||||
|
||||
typedef UpdateCallback = Function(double progress, UpdateState state);
|
||||
|
||||
// ignore: todo
|
||||
// TODO: cleanup old apk files
|
||||
|
||||
extension UpdateHelper on Release {
|
||||
Future<void> install({UpdateCallback? updateCallback}) async {
|
||||
updateCallback!(-1, UpdateState.preparing);
|
||||
|
||||
String downloads = await StorageHelper.downloadsPath();
|
||||
File apk = File("$downloads/refilc-v$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 installPerms =
|
||||
(await Permission.manageExternalStorage.request().isGranted &&
|
||||
await Permission.requestInstallPackages.request().isGranted);
|
||||
if (installPerms) {
|
||||
var result = await OpenFilex.open(apk.path);
|
||||
|
||||
if (result.type != ResultType.done) {
|
||||
// ignore: avoid_print
|
||||
print("ERROR: installUpdate.openFile: ${result.message}");
|
||||
throw result.message;
|
||||
}
|
||||
}
|
||||
|
||||
updateCallback(-1, UpdateState.none);
|
||||
}
|
||||
|
||||
Future<Uint8List> download({UpdateCallback? updateCallback}) async {
|
||||
var response = await FilcAPI.downloadRelease(downloads.first);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
46
refilc/lib/icons/filc_icons.dart
Normal file
46
refilc/lib/icons/filc_icons.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class FilcIcons {
|
||||
FilcIcons._();
|
||||
|
||||
static const iconFontFamily = 'FilcIcons';
|
||||
|
||||
/// home
|
||||
static const IconData home = IconData(0x21, fontFamily: iconFontFamily);
|
||||
|
||||
/// linux
|
||||
static const IconData linux = IconData(0x22, fontFamily: iconFontFamily);
|
||||
|
||||
/// upstairs
|
||||
static const IconData upstairs = IconData(0x23, fontFamily: iconFontFamily);
|
||||
|
||||
/// downstairs
|
||||
static const IconData downstairs = IconData(0x24, fontFamily: iconFontFamily);
|
||||
|
||||
/// premium
|
||||
static const IconData premium = IconData(0x25, fontFamily: iconFontFamily);
|
||||
|
||||
/// tinta
|
||||
static const IconData tinta = IconData(0x26, fontFamily: iconFontFamily);
|
||||
|
||||
/// kupak
|
||||
static const IconData kupak = IconData(0x27, fontFamily: iconFontFamily);
|
||||
|
||||
/// homefill
|
||||
static const IconData homefill = IconData(0x28, fontFamily: iconFontFamily);
|
||||
|
||||
/// gradesfill
|
||||
static const IconData gradesfill = IconData(0x29, fontFamily: iconFontFamily);
|
||||
|
||||
/// timetablefill
|
||||
static const IconData timetablefill =
|
||||
IconData(0x2a, fontFamily: iconFontFamily);
|
||||
|
||||
/// messagesfill
|
||||
static const IconData messagesfill =
|
||||
IconData(0x2b, fontFamily: iconFontFamily);
|
||||
|
||||
/// absencesfill
|
||||
static const IconData absencesfill =
|
||||
IconData(0x2c, fontFamily: iconFontFamily);
|
||||
}
|
||||
207
refilc/lib/main.dart
Normal file
207
refilc/lib/main.dart
Normal file
@@ -0,0 +1,207 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:background_fetch/background_fetch.dart';
|
||||
import 'package:refilc/api/providers/user_provider.dart';
|
||||
import 'package:refilc/api/providers/database_provider.dart';
|
||||
import 'package:refilc/database/init.dart';
|
||||
import 'package:refilc/helpers/notification_helper.dart';
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:refilc/app.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:refilc_mobile_ui/screens/error_screen.dart';
|
||||
import 'package:refilc_mobile_ui/screens/error_report_screen.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
// import 'package:firebase_core/firebase_core.dart';
|
||||
|
||||
// import 'firebase_options.dart';
|
||||
|
||||
void main() async {
|
||||
// Initalize
|
||||
WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized();
|
||||
// ignore: deprecated_member_use
|
||||
binding.renderView.automaticSystemUiAdjustment = false;
|
||||
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
||||
// Startup
|
||||
Startup startup = Startup();
|
||||
await startup.start();
|
||||
|
||||
// Custom error page
|
||||
ErrorWidget.builder = errorBuilder;
|
||||
|
||||
BackgroundFetch.registerHeadlessTask(backgroundHeadlessTask);
|
||||
|
||||
// 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 {
|
||||
database = DatabaseProvider();
|
||||
var db = await initDB(database);
|
||||
await db.close();
|
||||
await database.init();
|
||||
settings = await database.query.getSettings(database);
|
||||
user = await database.query.getUsers(settings);
|
||||
|
||||
late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;
|
||||
// Notifications setup
|
||||
if (!kIsWeb) {
|
||||
initPlatformState();
|
||||
flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
||||
}
|
||||
|
||||
// Get permission to show notifications
|
||||
if (kIsWeb) {
|
||||
// do nothing
|
||||
} else if (Platform.isAndroid) {
|
||||
await flutterLocalNotificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin>()!
|
||||
.requestNotificationsPermission();
|
||||
} else if (Platform.isIOS) {
|
||||
await flutterLocalNotificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
IOSFlutterLocalNotificationsPlugin>()
|
||||
?.requestPermissions(
|
||||
alert: false,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
} else if (Platform.isMacOS) {
|
||||
await flutterLocalNotificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
MacOSFlutterLocalNotificationsPlugin>()
|
||||
?.requestPermissions(
|
||||
alert: false,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
} else if (Platform.isLinux) {
|
||||
// no permissions are needed on linux
|
||||
}
|
||||
|
||||
// Platform specific settings
|
||||
if (!kIsWeb) {
|
||||
const DarwinInitializationSettings initializationSettingsDarwin =
|
||||
DarwinInitializationSettings(
|
||||
requestSoundPermission: true,
|
||||
requestBadgePermission: true,
|
||||
requestAlertPermission: false,
|
||||
);
|
||||
const AndroidInitializationSettings initializationSettingsAndroid =
|
||||
AndroidInitializationSettings('ic_notification');
|
||||
const LinuxInitializationSettings initializationSettingsLinux =
|
||||
LinuxInitializationSettings(defaultActionName: 'Open notification');
|
||||
const InitializationSettings initializationSettings =
|
||||
InitializationSettings(
|
||||
android: initializationSettingsAndroid,
|
||||
iOS: initializationSettingsDarwin,
|
||||
macOS: initializationSettingsDarwin,
|
||||
linux: initializationSettingsLinux,
|
||||
);
|
||||
|
||||
// Initialize notifications
|
||||
await flutterLocalNotificationsPlugin.initialize(
|
||||
initializationSettings,
|
||||
);
|
||||
}
|
||||
|
||||
// if (Platform.isAndroid || Platform.isIOS) {
|
||||
// await Firebase.initializeApp(
|
||||
// options: DefaultFirebaseOptions.currentPlatform,
|
||||
// );
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
bool errorShown = false;
|
||||
String lastException = '';
|
||||
|
||||
Widget errorBuilder(FlutterErrorDetails details) {
|
||||
return Builder(builder: (context) {
|
||||
if (Navigator.of(context).canPop()) Navigator.pop(context);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!errorShown && details.exceptionAsString() != lastException) {
|
||||
errorShown = true;
|
||||
lastException = details.exceptionAsString();
|
||||
Navigator.of(context, rootNavigator: true)
|
||||
.push(MaterialPageRoute(builder: (context) {
|
||||
if (kReleaseMode) {
|
||||
return ErrorReportScreen(details);
|
||||
} else {
|
||||
return ErrorScreen(details);
|
||||
}
|
||||
})).then((_) => errorShown = false);
|
||||
}
|
||||
});
|
||||
|
||||
return Container();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> initPlatformState() async {
|
||||
// Configure BackgroundFetch.
|
||||
int status = await BackgroundFetch.configure(
|
||||
BackgroundFetchConfig(
|
||||
minimumFetchInterval: 15,
|
||||
stopOnTerminate: false,
|
||||
enableHeadless: true,
|
||||
requiresBatteryNotLow: false,
|
||||
requiresCharging: false,
|
||||
requiresStorageNotLow: false,
|
||||
requiresDeviceIdle: false,
|
||||
requiredNetworkType: NetworkType.ANY,
|
||||
startOnBoot: true), (String taskId) async {
|
||||
// <-- Event handler
|
||||
if (kDebugMode) {
|
||||
print("[BackgroundFetch] Event received $taskId");
|
||||
}
|
||||
NotificationsHelper().backgroundJob();
|
||||
BackgroundFetch.finish(taskId);
|
||||
}, (String taskId) async {
|
||||
// <-- Task timeout handler.
|
||||
if (kDebugMode) {
|
||||
print("[BackgroundFetch] TASK TIMEOUT taskId: $taskId");
|
||||
}
|
||||
BackgroundFetch.finish(taskId);
|
||||
});
|
||||
if (kDebugMode) {
|
||||
print('[BackgroundFetch] configure success: $status');
|
||||
}
|
||||
BackgroundFetch.scheduleTask(TaskConfig(
|
||||
taskId: "com.transistorsoft.refilcnotification",
|
||||
delay: 900000, // 15 minutes
|
||||
periodic: true,
|
||||
forceAlarmManager: true,
|
||||
stopOnTerminate: false,
|
||||
enableHeadless: true));
|
||||
}
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
void backgroundHeadlessTask(HeadlessTask task) {
|
||||
String taskId = task.taskId;
|
||||
bool isTimeout = task.timeout;
|
||||
if (isTimeout) {
|
||||
if (kDebugMode) {
|
||||
print("[BackgroundFetch] Headless task timed-out: $taskId");
|
||||
}
|
||||
BackgroundFetch.finish(taskId);
|
||||
return;
|
||||
}
|
||||
if (kDebugMode) {
|
||||
print('[BackgroundFetch] Headless event received.');
|
||||
}
|
||||
NotificationsHelper().backgroundJob();
|
||||
BackgroundFetch.finish(task.taskId);
|
||||
}
|
||||
37
refilc/lib/models/ad.dart
Normal file
37
refilc/lib/models/ad.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
class Ad {
|
||||
String title;
|
||||
String description;
|
||||
String author;
|
||||
Uri? logoUrl;
|
||||
bool overridePremium;
|
||||
DateTime date;
|
||||
DateTime expireDate;
|
||||
Uri launchUrl;
|
||||
|
||||
Ad({
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.author,
|
||||
this.logoUrl,
|
||||
this.overridePremium = false,
|
||||
required this.date,
|
||||
required this.expireDate,
|
||||
required this.launchUrl,
|
||||
});
|
||||
|
||||
factory Ad.fromJson(Map json) {
|
||||
return Ad(
|
||||
title: json['title'] ?? 'Ad',
|
||||
description: json['description'] ?? '',
|
||||
author: json['author'] ?? 'reFilc',
|
||||
logoUrl: json['logo_url'] != null ? Uri.parse(json['logo_url']) : null,
|
||||
overridePremium: json['override_premium'] ?? false,
|
||||
date:
|
||||
json['date'] != null ? DateTime.parse(json['date']) : DateTime.now(),
|
||||
expireDate: json['expire_date'] != null
|
||||
? DateTime.parse(json['expire_date'])
|
||||
: DateTime.now(),
|
||||
launchUrl: Uri.parse(json['launch_url'] ?? 'https://refilc.hu'),
|
||||
);
|
||||
}
|
||||
}
|
||||
41
refilc/lib/models/config.dart
Normal file
41
refilc/lib/models/config.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
import 'dart:io';
|
||||
|
||||
class Config {
|
||||
String _userAgent;
|
||||
Map? json;
|
||||
static const String _version =
|
||||
String.fromEnvironment("APPVER", defaultValue: "3.0.4");
|
||||
|
||||
Config({required String userAgent, this.json}) : _userAgent = userAgent;
|
||||
|
||||
factory Config.fromJson(Map json) {
|
||||
return Config(
|
||||
userAgent: json["user_agent"] ?? "hu.ekreta.student/\$0/\$1/\$2",
|
||||
json: json,
|
||||
);
|
||||
}
|
||||
|
||||
String get userAgent => _userAgent
|
||||
.replaceAll("\$0", _version)
|
||||
.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();
|
||||
}
|
||||
1
refilc/lib/models/icon_pack.dart
Normal file
1
refilc/lib/models/icon_pack.dart
Normal file
@@ -0,0 +1 @@
|
||||
enum IconPack { material, cupertino }
|
||||
37
refilc/lib/models/news.dart
Normal file
37
refilc/lib/models/news.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
class News {
|
||||
String id;
|
||||
String title;
|
||||
String content;
|
||||
String link;
|
||||
String openLabel;
|
||||
String platform;
|
||||
bool emergency;
|
||||
DateTime expireDate;
|
||||
Map? json;
|
||||
|
||||
News({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.content,
|
||||
required this.link,
|
||||
required this.openLabel,
|
||||
required this.platform,
|
||||
required this.emergency,
|
||||
required this.expireDate,
|
||||
this.json,
|
||||
});
|
||||
|
||||
factory News.fromJson(Map json) {
|
||||
return News(
|
||||
id: json["id"] ?? "",
|
||||
title: json["title"] ?? "",
|
||||
content: json["content"] ?? "",
|
||||
link: json["link"] ?? "",
|
||||
openLabel: json["open_label"] ?? "",
|
||||
platform: json["platform"] ?? "",
|
||||
emergency: json["emergency"] ?? false,
|
||||
expireDate: DateTime.parse(json["expire_date"] ?? ''),
|
||||
json: json,
|
||||
);
|
||||
}
|
||||
}
|
||||
21
refilc/lib/models/personality.dart
Normal file
21
refilc/lib/models/personality.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
class Personality {
|
||||
PersonalityType type;
|
||||
|
||||
Personality({
|
||||
this.type = PersonalityType.npc,
|
||||
});
|
||||
}
|
||||
|
||||
enum PersonalityType {
|
||||
geek,
|
||||
sick,
|
||||
late,
|
||||
quitter,
|
||||
healthy,
|
||||
acceptable,
|
||||
fallible,
|
||||
average,
|
||||
diligent,
|
||||
cheater,
|
||||
npc
|
||||
}
|
||||
151
refilc/lib/models/release.dart
Normal file
151
refilc/lib/models/release.dart
Normal file
@@ -0,0 +1,151 @@
|
||||
class ReleaseDownload {
|
||||
String url;
|
||||
int size;
|
||||
|
||||
ReleaseDownload({
|
||||
required this.url,
|
||||
required this.size,
|
||||
});
|
||||
|
||||
factory ReleaseDownload.fromJson(Map json) {
|
||||
return ReleaseDownload(
|
||||
url: json["browser_download_url"] ?? "",
|
||||
size: json["size"] ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Release {
|
||||
String tag;
|
||||
Version version;
|
||||
String author;
|
||||
String body;
|
||||
List<ReleaseDownload> 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) => ReleaseDownload.fromJson(a)).toList().cast<ReleaseDownload>() : [],
|
||||
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) {
|
||||
// ignore: avoid_print
|
||||
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", "nightly", "test"];
|
||||
|
||||
@override
|
||||
int get hashCode => toString().hashCode;
|
||||
}
|
||||
28
refilc/lib/models/self_note.dart
Normal file
28
refilc/lib/models/self_note.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
class SelfNote {
|
||||
String id;
|
||||
String? title;
|
||||
String content;
|
||||
Map? json;
|
||||
|
||||
SelfNote({
|
||||
required this.id,
|
||||
this.title,
|
||||
required this.content,
|
||||
this.json,
|
||||
});
|
||||
|
||||
factory SelfNote.fromJson(Map json) {
|
||||
return SelfNote(
|
||||
id: json['id'],
|
||||
title: json['title'],
|
||||
content: json['content'],
|
||||
json: json,
|
||||
);
|
||||
}
|
||||
|
||||
get toJson => {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'content': content,
|
||||
};
|
||||
}
|
||||
669
refilc/lib/models/settings.dart
Normal file
669
refilc/lib/models/settings.dart
Normal file
@@ -0,0 +1,669 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:refilc/api/providers/database_provider.dart';
|
||||
import 'package:refilc/models/config.dart';
|
||||
import 'package:refilc/models/icon_pack.dart';
|
||||
import 'package:refilc/theme/colors/accent.dart';
|
||||
import 'package:refilc/theme/colors/dark_mobile.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
enum Pages { home, grades, timetable, messages, absences }
|
||||
|
||||
enum UpdateChannel { stable, beta, dev }
|
||||
|
||||
enum VibrationStrength { off, light, medium, strong }
|
||||
|
||||
class SettingsProvider extends ChangeNotifier {
|
||||
final DatabaseProvider? _database;
|
||||
|
||||
// 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;
|
||||
String _seenNews;
|
||||
bool _notificationsEnabled;
|
||||
bool _notificationsGradesEnabled;
|
||||
bool _notificationsAbsencesEnabled;
|
||||
bool _notificationsMessagesEnabled;
|
||||
bool _notificationsLessonsEnabled;
|
||||
/*
|
||||
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;
|
||||
VibrationStrength _vibrate;
|
||||
bool _abWeeks;
|
||||
bool _swapABweeks;
|
||||
UpdateChannel _updateChannel;
|
||||
Config _config;
|
||||
String _xFilcId;
|
||||
bool _graphClassAvg;
|
||||
bool _goodStudent;
|
||||
bool _presentationMode;
|
||||
bool _bellDelayEnabled;
|
||||
int _bellDelay;
|
||||
bool _gradeOpeningFun;
|
||||
IconPack _iconPack;
|
||||
Color _customAccentColor;
|
||||
Color _customBackgroundColor;
|
||||
Color _customHighlightColor;
|
||||
Color _customIconColor;
|
||||
bool _shadowEffect;
|
||||
List<String> _premiumScopes;
|
||||
String _premiumAccessToken;
|
||||
String _premiumLogin;
|
||||
String _lastAccountId;
|
||||
bool _renamedSubjectsEnabled;
|
||||
bool _renamedSubjectsItalics;
|
||||
bool _renamedTeachersEnabled;
|
||||
bool _renamedTeachersItalics;
|
||||
Color _liveActivityColor;
|
||||
String _welcomeMessage;
|
||||
String _appIcon;
|
||||
// current theme
|
||||
String _currentThemeId;
|
||||
String _currentThemeDisplayName;
|
||||
String _currentThemeCreator;
|
||||
// pinned settings
|
||||
String _pinSetGeneral;
|
||||
String _pinSetPersonalize;
|
||||
String _pinSetNotify;
|
||||
String _pinSetExtras;
|
||||
// more
|
||||
bool _showBreaks;
|
||||
String _fontFamily;
|
||||
|
||||
SettingsProvider({
|
||||
DatabaseProvider? database,
|
||||
required String language,
|
||||
required Pages startPage,
|
||||
required int rounding,
|
||||
required ThemeMode theme,
|
||||
required AccentColor accentColor,
|
||||
required List<Color> gradeColors,
|
||||
required bool newsEnabled,
|
||||
required String seenNews,
|
||||
required bool notificationsEnabled,
|
||||
required bool notificationsGradesEnabled,
|
||||
required bool notificationsAbsencesEnabled,
|
||||
required bool notificationsMessagesEnabled,
|
||||
required bool notificationsLessonsEnabled,
|
||||
required int notificationsBitfield,
|
||||
required bool developerMode,
|
||||
required int notificationPollInterval,
|
||||
required VibrationStrength vibrate,
|
||||
required bool abWeeks,
|
||||
required bool swapABweeks,
|
||||
required UpdateChannel updateChannel,
|
||||
required Config config,
|
||||
required String xFilcId,
|
||||
required bool graphClassAvg,
|
||||
required bool goodStudent,
|
||||
required bool presentationMode,
|
||||
required bool bellDelayEnabled,
|
||||
required int bellDelay,
|
||||
required bool gradeOpeningFun,
|
||||
required IconPack iconPack,
|
||||
required Color customAccentColor,
|
||||
required Color customBackgroundColor,
|
||||
required Color customHighlightColor,
|
||||
required Color customIconColor,
|
||||
required bool shadowEffect,
|
||||
required List<String> premiumScopes,
|
||||
required String premiumAccessToken,
|
||||
required String premiumLogin,
|
||||
required String lastAccountId,
|
||||
required bool renameSubjectsEnabled,
|
||||
required bool renameSubjectsItalics,
|
||||
required bool renameTeachersEnabled,
|
||||
required bool renameTeachersItalics,
|
||||
required Color liveActivityColor,
|
||||
required String welcomeMessage,
|
||||
required String appIcon,
|
||||
required String currentThemeId,
|
||||
required String currentThemeDisplayName,
|
||||
required String currentThemeCreator,
|
||||
required bool showBreaks,
|
||||
required String pinSetGeneral,
|
||||
required String pinSetPersonalize,
|
||||
required String pinSetNotify,
|
||||
required String pinSetExtras,
|
||||
required String fontFamily,
|
||||
}) : _database = database,
|
||||
_language = language,
|
||||
_startPage = startPage,
|
||||
_rounding = rounding,
|
||||
_theme = theme,
|
||||
_accentColor = accentColor,
|
||||
_gradeColors = gradeColors,
|
||||
_newsEnabled = newsEnabled,
|
||||
_seenNews = seenNews,
|
||||
_notificationsEnabled = notificationsEnabled,
|
||||
_notificationsGradesEnabled = notificationsGradesEnabled,
|
||||
_notificationsAbsencesEnabled = notificationsAbsencesEnabled,
|
||||
_notificationsMessagesEnabled = notificationsMessagesEnabled,
|
||||
_notificationsLessonsEnabled = notificationsLessonsEnabled,
|
||||
_notificationsBitfield = notificationsBitfield,
|
||||
_developerMode = developerMode,
|
||||
_notificationPollInterval = notificationPollInterval,
|
||||
_vibrate = vibrate,
|
||||
_abWeeks = abWeeks,
|
||||
_swapABweeks = swapABweeks,
|
||||
_updateChannel = updateChannel,
|
||||
_config = config,
|
||||
_xFilcId = xFilcId,
|
||||
_graphClassAvg = graphClassAvg,
|
||||
_goodStudent = goodStudent,
|
||||
_presentationMode = presentationMode,
|
||||
_bellDelayEnabled = bellDelayEnabled,
|
||||
_bellDelay = bellDelay,
|
||||
_gradeOpeningFun = gradeOpeningFun,
|
||||
_iconPack = iconPack,
|
||||
_customAccentColor = customAccentColor,
|
||||
_customBackgroundColor = customBackgroundColor,
|
||||
_customHighlightColor = customHighlightColor,
|
||||
_customIconColor = customIconColor,
|
||||
_shadowEffect = shadowEffect,
|
||||
_premiumScopes = premiumScopes,
|
||||
_premiumAccessToken = premiumAccessToken,
|
||||
_premiumLogin = premiumLogin,
|
||||
_lastAccountId = lastAccountId,
|
||||
_renamedSubjectsEnabled = renameSubjectsEnabled,
|
||||
_renamedSubjectsItalics = renameSubjectsItalics,
|
||||
_renamedTeachersEnabled = renameTeachersEnabled,
|
||||
_renamedTeachersItalics = renameTeachersItalics,
|
||||
_liveActivityColor = liveActivityColor,
|
||||
_welcomeMessage = welcomeMessage,
|
||||
_appIcon = appIcon,
|
||||
_currentThemeId = currentThemeId,
|
||||
_currentThemeDisplayName = currentThemeDisplayName,
|
||||
_currentThemeCreator = currentThemeCreator,
|
||||
_showBreaks = showBreaks,
|
||||
_pinSetGeneral = pinSetGeneral,
|
||||
_pinSetPersonalize = pinSetPersonalize,
|
||||
_pinSetNotify = pinSetNotify,
|
||||
_pinSetExtras = pinSetExtras,
|
||||
_fontFamily = fontFamily;
|
||||
|
||||
factory SettingsProvider.fromMap(Map map,
|
||||
{required DatabaseProvider database}) {
|
||||
Map<String, Object?>? configMap;
|
||||
|
||||
try {
|
||||
configMap = jsonDecode(map["config"] ?? "{}");
|
||||
} catch (e) {
|
||||
log("[ERROR] SettingsProvider.fromMap: $e");
|
||||
}
|
||||
|
||||
return SettingsProvider(
|
||||
database: database,
|
||||
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,
|
||||
seenNews: map["seen_news"],
|
||||
notificationsEnabled: map["notifications"] == 1,
|
||||
notificationsGradesEnabled: map["notifications_grades"] == 1,
|
||||
notificationsAbsencesEnabled: map["notifications_absences"] == 1,
|
||||
notificationsMessagesEnabled: map["notifications_messages"] == 1,
|
||||
notificationsLessonsEnabled: map["notifications_lessons"] == 1,
|
||||
notificationsBitfield: map["notifications_bitfield"],
|
||||
notificationPollInterval: map["notification_poll_interval"],
|
||||
developerMode: map["developer_mode"] == 1,
|
||||
vibrate: VibrationStrength.values[map["vibration_strength"]],
|
||||
abWeeks: map["ab_weeks"] == 1,
|
||||
swapABweeks: map["swap_ab_weeks"] == 1,
|
||||
updateChannel: UpdateChannel.values[map["update_channel"]],
|
||||
config: Config.fromJson(configMap ?? {}),
|
||||
xFilcId: map["x_filc_id"],
|
||||
graphClassAvg: map["graph_class_avg"] == 1,
|
||||
goodStudent: false,
|
||||
presentationMode: map["presentation_mode"] == 1,
|
||||
bellDelayEnabled: map["bell_delay_enabled"] == 1,
|
||||
bellDelay: map["bell_delay"],
|
||||
gradeOpeningFun: map["grade_opening_fun"] == 1,
|
||||
iconPack: Map.fromEntries(
|
||||
IconPack.values.map((e) => MapEntry(e.name, e)))[map["icon_pack"]]!,
|
||||
customAccentColor: Color(map["custom_accent_color"]),
|
||||
customBackgroundColor: Color(map["custom_background_color"]),
|
||||
customHighlightColor: Color(map["custom_highlight_color"]),
|
||||
customIconColor: Color(map["custom_icon_color"]),
|
||||
shadowEffect: map["shadow_effect"] == 1,
|
||||
premiumScopes: jsonDecode(map["premium_scopes"]).cast<String>(),
|
||||
premiumAccessToken: map["premium_token"],
|
||||
premiumLogin: map["premium_login"],
|
||||
lastAccountId: map["last_account_id"],
|
||||
renameSubjectsEnabled: map["renamed_subjects_enabled"] == 1,
|
||||
renameSubjectsItalics: map["renamed_subjects_italics"] == 1,
|
||||
renameTeachersEnabled: map["renamed_teachers_enabled"] == 1,
|
||||
renameTeachersItalics: map["renamed_teachers_italics"] == 1,
|
||||
liveActivityColor: Color(map["live_activity_color"]),
|
||||
welcomeMessage: map["welcome_message"],
|
||||
appIcon: map["app_icon"],
|
||||
currentThemeId: map['current_theme_id'],
|
||||
currentThemeDisplayName: map['current_theme_display_name'],
|
||||
currentThemeCreator: map['current_theme_creator'],
|
||||
showBreaks: map['show_breaks'] == 1,
|
||||
pinSetGeneral: map['general_s_pin'],
|
||||
pinSetPersonalize: map['personalize_s_pin'],
|
||||
pinSetNotify: map['notify_s_pin'],
|
||||
pinSetExtras: map['extras_s_pin'],
|
||||
fontFamily: map['font_family'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, Object?> toMap() {
|
||||
return {
|
||||
"language": _language,
|
||||
"start_page": _startPage.index,
|
||||
"rounding": _rounding,
|
||||
"theme": _theme.index,
|
||||
"accent_color": _accentColor.index,
|
||||
"news": _newsEnabled ? 1 : 0,
|
||||
"seen_news": _seenNews,
|
||||
"notifications": _notificationsEnabled ? 1 : 0,
|
||||
"notifications_grades": _notificationsGradesEnabled ? 1 : 0,
|
||||
"notifications_absences": _notificationsAbsencesEnabled ? 1 : 0,
|
||||
"notifications_messages": _notificationsMessagesEnabled ? 1 : 0,
|
||||
"notifications_lessons": _notificationsLessonsEnabled ? 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,
|
||||
"vibration_strength": _vibrate.index,
|
||||
"ab_weeks": _abWeeks ? 1 : 0,
|
||||
"swap_ab_weeks": _swapABweeks ? 1 : 0,
|
||||
"notification_poll_interval": _notificationPollInterval,
|
||||
"config": jsonEncode(config.json),
|
||||
"x_filc_id": _xFilcId,
|
||||
"graph_class_avg": _graphClassAvg ? 1 : 0,
|
||||
"presentation_mode": _presentationMode ? 1 : 0,
|
||||
"bell_delay_enabled": _bellDelayEnabled ? 1 : 0,
|
||||
"bell_delay": _bellDelay,
|
||||
"grade_opening_fun": _gradeOpeningFun ? 1 : 0,
|
||||
"icon_pack": _iconPack.name,
|
||||
"custom_accent_color": _customAccentColor.value,
|
||||
"custom_background_color": _customBackgroundColor.value,
|
||||
"custom_highlight_color": _customHighlightColor.value,
|
||||
"custom_icon_color": _customIconColor.value,
|
||||
"shadow_effect": _shadowEffect ? 1 : 0,
|
||||
"premium_scopes": jsonEncode(_premiumScopes),
|
||||
"premium_token": _premiumAccessToken,
|
||||
"premium_login": _premiumLogin,
|
||||
"last_account_id": _lastAccountId,
|
||||
"renamed_subjects_enabled": _renamedSubjectsEnabled ? 1 : 0,
|
||||
"renamed_subjects_italics": _renamedSubjectsItalics ? 1 : 0,
|
||||
"renamed_teachers_enabled": _renamedTeachersEnabled ? 1 : 0,
|
||||
"renamed_teachers_italics": _renamedTeachersItalics ? 1 : 0,
|
||||
"live_activity_color": _liveActivityColor.value,
|
||||
"welcome_message": _welcomeMessage,
|
||||
"app_icon": _appIcon,
|
||||
"current_theme_id": _currentThemeId,
|
||||
"current_theme_display_name": _currentThemeDisplayName,
|
||||
"current_theme_creator": _currentThemeCreator,
|
||||
"show_breaks": _showBreaks ? 1 : 0,
|
||||
"general_s_pin": _pinSetGeneral,
|
||||
"personalize_s_pin": _pinSetPersonalize,
|
||||
"notify_s_pin": _pinSetNotify,
|
||||
"extras_s_pin": _pinSetExtras,
|
||||
"font_family": _fontFamily,
|
||||
};
|
||||
}
|
||||
|
||||
factory SettingsProvider.defaultSettings({DatabaseProvider? database}) {
|
||||
return SettingsProvider(
|
||||
database: database,
|
||||
language: "hu",
|
||||
startPage: Pages.home,
|
||||
rounding: 5,
|
||||
theme: ThemeMode.system,
|
||||
accentColor: AccentColor.filc,
|
||||
gradeColors: [
|
||||
DarkMobileAppColors().gradeOne,
|
||||
DarkMobileAppColors().gradeTwo,
|
||||
DarkMobileAppColors().gradeThree,
|
||||
DarkMobileAppColors().gradeFour,
|
||||
DarkMobileAppColors().gradeFive,
|
||||
],
|
||||
newsEnabled: true,
|
||||
seenNews: '',
|
||||
notificationsEnabled: true,
|
||||
notificationsGradesEnabled: true,
|
||||
notificationsAbsencesEnabled: true,
|
||||
notificationsMessagesEnabled: true,
|
||||
notificationsLessonsEnabled: true,
|
||||
notificationsBitfield: 255,
|
||||
developerMode: false,
|
||||
notificationPollInterval: 1,
|
||||
vibrate: VibrationStrength.medium,
|
||||
abWeeks: false,
|
||||
swapABweeks: false,
|
||||
updateChannel: UpdateChannel.stable,
|
||||
config: Config.fromJson({}),
|
||||
xFilcId: const Uuid().v4(),
|
||||
graphClassAvg: false,
|
||||
goodStudent: false,
|
||||
presentationMode: false,
|
||||
bellDelayEnabled: false,
|
||||
bellDelay: 0,
|
||||
gradeOpeningFun: false,
|
||||
iconPack: IconPack.cupertino,
|
||||
customAccentColor: const Color(0xff3D7BF4),
|
||||
customBackgroundColor: const Color(0xff000000),
|
||||
customHighlightColor: const Color(0xff222222),
|
||||
customIconColor: const Color(0x00000000),
|
||||
shadowEffect: true,
|
||||
premiumScopes: [],
|
||||
premiumAccessToken: "",
|
||||
premiumLogin: "",
|
||||
lastAccountId: "",
|
||||
renameSubjectsEnabled: false,
|
||||
renameSubjectsItalics: false,
|
||||
renameTeachersEnabled: false,
|
||||
renameTeachersItalics: false,
|
||||
liveActivityColor: const Color(0xFF676767),
|
||||
welcomeMessage: '',
|
||||
appIcon: 'refilc_default',
|
||||
currentThemeId: '',
|
||||
currentThemeDisplayName: '',
|
||||
currentThemeCreator: 'reFilc',
|
||||
showBreaks: true,
|
||||
pinSetGeneral: '',
|
||||
pinSetPersonalize: '',
|
||||
pinSetNotify: '',
|
||||
pinSetExtras: '',
|
||||
fontFamily: '',
|
||||
);
|
||||
}
|
||||
|
||||
// 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;
|
||||
List<String> get seenNews => _seenNews.split(',');
|
||||
bool get notificationsEnabled => _notificationsEnabled;
|
||||
bool get notificationsGradesEnabled => _notificationsGradesEnabled;
|
||||
bool get notificationsAbsencesEnabled => _notificationsAbsencesEnabled;
|
||||
bool get notificationsMessagesEnabled => _notificationsMessagesEnabled;
|
||||
bool get notificationsLessonsEnabled => _notificationsLessonsEnabled;
|
||||
int get notificationsBitfield => _notificationsBitfield;
|
||||
bool get developerMode => _developerMode;
|
||||
int get notificationPollInterval => _notificationPollInterval;
|
||||
VibrationStrength get vibrate => _vibrate;
|
||||
bool get abWeeks => _abWeeks;
|
||||
bool get swapABweeks => _swapABweeks;
|
||||
UpdateChannel get updateChannel => _updateChannel;
|
||||
Config get config => _config;
|
||||
String get xFilcId => _xFilcId;
|
||||
bool get graphClassAvg => _graphClassAvg;
|
||||
bool get goodStudent => _goodStudent;
|
||||
bool get presentationMode => _presentationMode;
|
||||
bool get bellDelayEnabled => _bellDelayEnabled;
|
||||
int get bellDelay => _bellDelay;
|
||||
bool get gradeOpeningFun => _gradeOpeningFun;
|
||||
IconPack get iconPack => _iconPack;
|
||||
Color? get customAccentColor =>
|
||||
_customAccentColor == accentColorMap[AccentColor.custom]
|
||||
? null
|
||||
: _customAccentColor;
|
||||
Color? get customBackgroundColor => _customBackgroundColor;
|
||||
Color? get customHighlightColor => _customHighlightColor;
|
||||
Color? get customIconColor => _customIconColor;
|
||||
bool get shadowEffect => _shadowEffect;
|
||||
List<String> get premiumScopes => _premiumScopes;
|
||||
String get premiumAccessToken => _premiumAccessToken;
|
||||
String get premiumLogin => _premiumLogin;
|
||||
String get lastAccountId => _lastAccountId;
|
||||
bool get renamedSubjectsEnabled => _renamedSubjectsEnabled;
|
||||
bool get renamedSubjectsItalics => _renamedSubjectsItalics;
|
||||
bool get renamedTeachersEnabled => _renamedTeachersEnabled;
|
||||
bool get renamedTeachersItalics => _renamedTeachersItalics;
|
||||
Color get liveActivityColor => _liveActivityColor;
|
||||
String get welcomeMessage => _welcomeMessage;
|
||||
String get appIcon => _appIcon;
|
||||
String get currentThemeId => _currentThemeId;
|
||||
String get currentThemeDisplayName => _currentThemeDisplayName;
|
||||
String get currentThemeCreator => _currentThemeCreator;
|
||||
bool get showBreaks => _showBreaks;
|
||||
String get fontFamily => _fontFamily;
|
||||
|
||||
Future<void> update({
|
||||
bool store = true,
|
||||
String? language,
|
||||
Pages? startPage,
|
||||
int? rounding,
|
||||
ThemeMode? theme,
|
||||
AccentColor? accentColor,
|
||||
List<Color>? gradeColors,
|
||||
bool? newsEnabled,
|
||||
String? seenNewsId,
|
||||
bool? notificationsEnabled,
|
||||
bool? notificationsGradesEnabled,
|
||||
bool? notificationsAbsencesEnabled,
|
||||
bool? notificationsMessagesEnabled,
|
||||
bool? notificationsLessonsEnabled,
|
||||
int? notificationsBitfield,
|
||||
bool? developerMode,
|
||||
int? notificationPollInterval,
|
||||
VibrationStrength? vibrate,
|
||||
bool? abWeeks,
|
||||
bool? swapABweeks,
|
||||
UpdateChannel? updateChannel,
|
||||
Config? config,
|
||||
String? xFilcId,
|
||||
bool? graphClassAvg,
|
||||
bool? goodStudent,
|
||||
bool? presentationMode,
|
||||
bool? bellDelayEnabled,
|
||||
int? bellDelay,
|
||||
bool? gradeOpeningFun,
|
||||
IconPack? iconPack,
|
||||
Color? customAccentColor,
|
||||
Color? customBackgroundColor,
|
||||
Color? customHighlightColor,
|
||||
Color? customIconColor,
|
||||
bool? shadowEffect,
|
||||
List<String>? premiumScopes,
|
||||
String? premiumAccessToken,
|
||||
String? premiumLogin,
|
||||
String? lastAccountId,
|
||||
bool? renamedSubjectsEnabled,
|
||||
bool? renamedSubjectsItalics,
|
||||
bool? renamedTeachersEnabled,
|
||||
bool? renamedTeachersItalics,
|
||||
Color? liveActivityColor,
|
||||
String? welcomeMessage,
|
||||
String? appIcon,
|
||||
String? currentThemeId,
|
||||
String? currentThemeDisplayName,
|
||||
String? currentThemeCreator,
|
||||
bool? showBreaks,
|
||||
String? fontFamily,
|
||||
}) 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 (seenNewsId != null && !_seenNews.split(',').contains(seenNewsId)) {
|
||||
var tempList = _seenNews.split(',');
|
||||
tempList.add(seenNewsId);
|
||||
_seenNews = tempList.join(',');
|
||||
}
|
||||
if (notificationsEnabled != null &&
|
||||
notificationsEnabled != _notificationsEnabled) {
|
||||
_notificationsEnabled = notificationsEnabled;
|
||||
}
|
||||
if (notificationsGradesEnabled != null &&
|
||||
notificationsGradesEnabled != _notificationsGradesEnabled) {
|
||||
_notificationsGradesEnabled = notificationsGradesEnabled;
|
||||
}
|
||||
if (notificationsAbsencesEnabled != null &&
|
||||
notificationsAbsencesEnabled != _notificationsAbsencesEnabled) {
|
||||
_notificationsAbsencesEnabled = notificationsAbsencesEnabled;
|
||||
}
|
||||
if (notificationsMessagesEnabled != null &&
|
||||
notificationsMessagesEnabled != _notificationsMessagesEnabled) {
|
||||
_notificationsMessagesEnabled = notificationsMessagesEnabled;
|
||||
}
|
||||
if (notificationsLessonsEnabled != null &&
|
||||
notificationsLessonsEnabled != _notificationsLessonsEnabled) {
|
||||
_notificationsLessonsEnabled = notificationsLessonsEnabled;
|
||||
}
|
||||
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 (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 (xFilcId != null && xFilcId != _xFilcId) _xFilcId = xFilcId;
|
||||
if (graphClassAvg != null && graphClassAvg != _graphClassAvg) {
|
||||
_graphClassAvg = graphClassAvg;
|
||||
}
|
||||
if (goodStudent != null) _goodStudent = goodStudent;
|
||||
if (presentationMode != null && presentationMode != _presentationMode) {
|
||||
_presentationMode = presentationMode;
|
||||
}
|
||||
if (bellDelay != null && bellDelay != _bellDelay) _bellDelay = bellDelay;
|
||||
if (bellDelayEnabled != null && bellDelayEnabled != _bellDelayEnabled) {
|
||||
_bellDelayEnabled = bellDelayEnabled;
|
||||
}
|
||||
if (gradeOpeningFun != null && gradeOpeningFun != _gradeOpeningFun) {
|
||||
_gradeOpeningFun = gradeOpeningFun;
|
||||
}
|
||||
if (iconPack != null && iconPack != _iconPack) _iconPack = iconPack;
|
||||
if (customAccentColor != null && customAccentColor != _customAccentColor) {
|
||||
_customAccentColor = customAccentColor;
|
||||
}
|
||||
if (customBackgroundColor != null &&
|
||||
customBackgroundColor != _customBackgroundColor) {
|
||||
_customBackgroundColor = customBackgroundColor;
|
||||
}
|
||||
if (customHighlightColor != null &&
|
||||
customHighlightColor != _customHighlightColor) {
|
||||
_customHighlightColor = customHighlightColor;
|
||||
}
|
||||
if (customIconColor != null && customIconColor != _customIconColor) {
|
||||
_customIconColor = customIconColor;
|
||||
}
|
||||
if (shadowEffect != null && shadowEffect != _shadowEffect) {
|
||||
_shadowEffect = shadowEffect;
|
||||
}
|
||||
if (premiumScopes != null && premiumScopes != _premiumScopes) {
|
||||
_premiumScopes = premiumScopes;
|
||||
}
|
||||
if (premiumAccessToken != null &&
|
||||
premiumAccessToken != _premiumAccessToken) {
|
||||
_premiumAccessToken = premiumAccessToken;
|
||||
}
|
||||
if (premiumLogin != null && premiumLogin != _premiumLogin) {
|
||||
_premiumLogin = premiumLogin;
|
||||
}
|
||||
if (lastAccountId != null && lastAccountId != _lastAccountId) {
|
||||
_lastAccountId = lastAccountId;
|
||||
}
|
||||
if (renamedSubjectsEnabled != null &&
|
||||
renamedSubjectsEnabled != _renamedSubjectsEnabled) {
|
||||
_renamedSubjectsEnabled = renamedSubjectsEnabled;
|
||||
}
|
||||
if (renamedSubjectsItalics != null &&
|
||||
renamedSubjectsItalics != _renamedSubjectsItalics) {
|
||||
_renamedSubjectsItalics = renamedSubjectsItalics;
|
||||
}
|
||||
if (renamedTeachersEnabled != null &&
|
||||
renamedTeachersEnabled != _renamedTeachersEnabled) {
|
||||
_renamedTeachersEnabled = renamedTeachersEnabled;
|
||||
}
|
||||
if (renamedTeachersItalics != null &&
|
||||
renamedTeachersItalics != _renamedTeachersItalics) {
|
||||
_renamedTeachersItalics = renamedTeachersItalics;
|
||||
}
|
||||
if (liveActivityColor != null && liveActivityColor != _liveActivityColor) {
|
||||
_liveActivityColor = liveActivityColor;
|
||||
}
|
||||
if (welcomeMessage != null && welcomeMessage != _welcomeMessage) {
|
||||
_welcomeMessage = welcomeMessage;
|
||||
}
|
||||
if (appIcon != null && appIcon != _appIcon) {
|
||||
_appIcon = appIcon;
|
||||
}
|
||||
if (currentThemeId != null && currentThemeId != _currentThemeId) {
|
||||
_currentThemeId = currentThemeId;
|
||||
}
|
||||
if (currentThemeDisplayName != null &&
|
||||
currentThemeDisplayName != _currentThemeDisplayName) {
|
||||
_currentThemeDisplayName = currentThemeDisplayName;
|
||||
}
|
||||
if (currentThemeCreator != null &&
|
||||
currentThemeCreator != _currentThemeCreator) {
|
||||
_currentThemeCreator = currentThemeCreator;
|
||||
}
|
||||
if (showBreaks != null && showBreaks != _showBreaks) {
|
||||
_showBreaks = showBreaks;
|
||||
}
|
||||
if (fontFamily != null && fontFamily != _fontFamily) {
|
||||
_fontFamily = fontFamily;
|
||||
}
|
||||
// store or not
|
||||
if (store) await _database?.store.storeSettings(this);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
91
refilc/lib/models/shared_theme.dart
Normal file
91
refilc/lib/models/shared_theme.dart
Normal file
@@ -0,0 +1,91 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SharedTheme {
|
||||
Map json;
|
||||
String id;
|
||||
bool isPublic;
|
||||
String nickname;
|
||||
Color backgroundColor;
|
||||
Color panelsColor;
|
||||
Color accentColor;
|
||||
Color iconColor;
|
||||
bool shadowEffect;
|
||||
SharedGradeColors gradeColors;
|
||||
String displayName;
|
||||
ThemeMode? themeMode;
|
||||
String fontFamily;
|
||||
|
||||
SharedTheme({
|
||||
required this.json,
|
||||
required this.id,
|
||||
this.isPublic = false,
|
||||
this.nickname = 'Anonymous',
|
||||
required this.backgroundColor,
|
||||
required this.panelsColor,
|
||||
required this.accentColor,
|
||||
required this.iconColor,
|
||||
required this.shadowEffect,
|
||||
required this.gradeColors,
|
||||
this.displayName = 'displayName',
|
||||
this.themeMode,
|
||||
required this.fontFamily,
|
||||
});
|
||||
|
||||
factory SharedTheme.fromJson(Map json, SharedGradeColors gradeColors) {
|
||||
return SharedTheme(
|
||||
json: json,
|
||||
id: json['public_id'],
|
||||
isPublic: json['is_public'] ?? false,
|
||||
nickname: json['nickname'] ?? 'Anonymous',
|
||||
backgroundColor: Color(json['background_color']),
|
||||
panelsColor: Color(json['panels_color']),
|
||||
accentColor: Color(json['accent_color']),
|
||||
iconColor: Color(json['icon_color']),
|
||||
shadowEffect: json['shadow_effect'] ?? true,
|
||||
gradeColors: gradeColors,
|
||||
displayName: json['display_name'] ?? 'no_name',
|
||||
themeMode: json['theme_mode'] == 'dark'
|
||||
? ThemeMode.dark
|
||||
: (json['theme_mode'] == 'light' ? ThemeMode.light : null),
|
||||
fontFamily: json['font_family'] ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SharedGradeColors {
|
||||
Map json;
|
||||
String id;
|
||||
bool isPublic;
|
||||
String nickname;
|
||||
Color fiveColor;
|
||||
Color fourColor;
|
||||
Color threeColor;
|
||||
Color twoColor;
|
||||
Color oneColor;
|
||||
|
||||
SharedGradeColors({
|
||||
required this.json,
|
||||
required this.id,
|
||||
this.isPublic = false,
|
||||
this.nickname = 'Anonymous',
|
||||
required this.fiveColor,
|
||||
required this.fourColor,
|
||||
required this.threeColor,
|
||||
required this.twoColor,
|
||||
required this.oneColor,
|
||||
});
|
||||
|
||||
factory SharedGradeColors.fromJson(Map json) {
|
||||
return SharedGradeColors(
|
||||
json: json,
|
||||
id: json['public_id'],
|
||||
isPublic: json['is_public'] ?? false,
|
||||
nickname: json['nickname'] ?? 'Anonymous',
|
||||
fiveColor: Color(json['five_color']),
|
||||
fourColor: Color(json['four_color']),
|
||||
threeColor: Color(json['three_color']),
|
||||
twoColor: Color(json['two_color']),
|
||||
oneColor: Color(json['one_color']),
|
||||
);
|
||||
}
|
||||
}
|
||||
35
refilc/lib/models/subject_lesson_count.dart
Normal file
35
refilc/lib/models/subject_lesson_count.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
import 'package:refilc_kreta_api/models/category.dart';
|
||||
import 'package:refilc_kreta_api/models/subject.dart';
|
||||
|
||||
enum SubjectLessonCountUpdateState { ready, updating }
|
||||
|
||||
class SubjectLessonCount {
|
||||
DateTime lastUpdated;
|
||||
Map<GradeSubject, int> subjects;
|
||||
SubjectLessonCountUpdateState state;
|
||||
|
||||
SubjectLessonCount(
|
||||
{required this.lastUpdated,
|
||||
required this.subjects,
|
||||
this.state = SubjectLessonCountUpdateState.ready});
|
||||
|
||||
factory SubjectLessonCount.fromMap(Map json) {
|
||||
return SubjectLessonCount(
|
||||
lastUpdated:
|
||||
DateTime.fromMillisecondsSinceEpoch(json["last_updated"] ?? 0),
|
||||
subjects: ((json["subjects"] as Map?) ?? {}).map(
|
||||
(key, value) => MapEntry(
|
||||
GradeSubject(id: key, name: "", category: Category.fromJson({})),
|
||||
value,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Map toMap() {
|
||||
return {
|
||||
"last_updated": lastUpdated.millisecondsSinceEpoch,
|
||||
"subjects": subjects.map((key, value) => MapEntry(key.id, value)),
|
||||
};
|
||||
}
|
||||
}
|
||||
50
refilc/lib/models/supporter.dart
Normal file
50
refilc/lib/models/supporter.dart
Normal file
@@ -0,0 +1,50 @@
|
||||
enum DonationType { once, monthly }
|
||||
|
||||
class Supporter {
|
||||
final String avatar;
|
||||
final String name;
|
||||
final String comment;
|
||||
final int price;
|
||||
final DonationType type;
|
||||
|
||||
const Supporter({required this.avatar, required this.name, this.comment = "", this.price = 0, this.type = DonationType.once});
|
||||
|
||||
factory Supporter.fromJson(Map json, {String? avatarPattern}) {
|
||||
return Supporter(
|
||||
avatar: json["avatar"] ?? avatarPattern != null ? avatarPattern!.replaceFirst("\$", json["name"]) : "",
|
||||
name: json["name"] ?? "Unknown",
|
||||
comment: json["comment"] ?? "",
|
||||
price: json["price"].toInt() ?? 0,
|
||||
type: DonationType.values.asNameMap()[json["type"] ?? "once"] ?? DonationType.once,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Supporters {
|
||||
final double progress;
|
||||
final double max;
|
||||
final String description;
|
||||
final List<Supporter> github;
|
||||
final List<Supporter> patreon;
|
||||
|
||||
Supporters({
|
||||
required this.progress,
|
||||
required this.max,
|
||||
required this.description,
|
||||
required this.github,
|
||||
required this.patreon,
|
||||
});
|
||||
|
||||
factory Supporters.fromJson(Map json) {
|
||||
return Supporters(
|
||||
progress: json["percentage"].toDouble() ?? 100.0,
|
||||
max: json["target"].toDouble() ?? 1.0,
|
||||
description: json["description"] ?? "",
|
||||
github: json["sponsors"]["github"]
|
||||
.map((e) => Supporter.fromJson(e, avatarPattern: "https://github.com/\$.png?size=200"))
|
||||
.cast<Supporter>()
|
||||
.toList(),
|
||||
patreon: json["sponsors"]["patreon"].map((e) => Supporter.fromJson(e)).cast<Supporter>().toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
115
refilc/lib/models/user.dart
Normal file
115
refilc/lib/models/user.dart
Normal file
@@ -0,0 +1,115 @@
|
||||
import 'dart:convert';
|
||||
import 'package:refilc_kreta_api/client/api.dart';
|
||||
import 'package:refilc_kreta_api/models/school.dart';
|
||||
import 'package:refilc_kreta_api/models/student.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
enum Role { student, parent }
|
||||
|
||||
class User {
|
||||
late String id;
|
||||
String username;
|
||||
String password;
|
||||
String instituteCode;
|
||||
String name;
|
||||
Student student;
|
||||
Role role;
|
||||
String nickname;
|
||||
String picture;
|
||||
|
||||
String get displayName => nickname != '' ? nickname : name;
|
||||
|
||||
User({
|
||||
String? id,
|
||||
required this.name,
|
||||
required this.username,
|
||||
required this.password,
|
||||
required this.instituteCode,
|
||||
required this.student,
|
||||
required this.role,
|
||||
this.nickname = "",
|
||||
this.picture = "",
|
||||
}) {
|
||||
if (id != null) {
|
||||
this.id = id;
|
||||
} else {
|
||||
this.id = const 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"].trim(),
|
||||
student: map["student"] != 'null'
|
||||
? Student.fromJson(jsonDecode(map["student"]))
|
||||
: Student(
|
||||
id: const Uuid().v4(),
|
||||
name: 'Ismeretlen Diák',
|
||||
school: School(instituteCode: '', name: '', city: ''),
|
||||
birth: DateTime.now(),
|
||||
yearId: '1',
|
||||
parents: [],
|
||||
),
|
||||
role: Role.values[map["role"] ?? 0],
|
||||
nickname: map["nickname"] ?? "",
|
||||
picture: map["picture"] ?? "",
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, Object?> toMap() {
|
||||
return {
|
||||
"id": id,
|
||||
"username": username,
|
||||
"password": password,
|
||||
"institute_code": instituteCode,
|
||||
"name": name,
|
||||
"student": jsonEncode(student.json),
|
||||
"role": role.index,
|
||||
"nickname": nickname,
|
||||
"picture": picture,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => jsonEncode(toMap());
|
||||
|
||||
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.clientId,
|
||||
};
|
||||
}
|
||||
|
||||
static Map<String, Object?> refreshBody({
|
||||
required String refreshToken,
|
||||
required String instituteCode,
|
||||
}) {
|
||||
return {
|
||||
"refresh_token": refreshToken,
|
||||
"institute_code": instituteCode,
|
||||
"client_id": KretaAPI.clientId,
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_user_data": "false",
|
||||
};
|
||||
}
|
||||
|
||||
static Map<String, Object?> logoutBody({
|
||||
required String refreshToken,
|
||||
}) {
|
||||
return {
|
||||
"refresh_token": refreshToken,
|
||||
"client_id": KretaAPI.clientId,
|
||||
};
|
||||
}
|
||||
}
|
||||
137
refilc/lib/providers/third_party_provider.dart
Normal file
137
refilc/lib/providers/third_party_provider.dart
Normal file
@@ -0,0 +1,137 @@
|
||||
import 'package:extension_google_sign_in_as_googleapis_auth/extension_google_sign_in_as_googleapis_auth.dart';
|
||||
import 'package:refilc_kreta_api/controllers/timetable_controller.dart';
|
||||
import 'package:refilc_kreta_api/models/lesson.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:googleapis/calendar/v3.dart';
|
||||
import 'package:google_sign_in/google_sign_in.dart';
|
||||
|
||||
class ThirdPartyProvider with ChangeNotifier {
|
||||
late List<Event>? _googleEvents;
|
||||
// late BuildContext _context;
|
||||
|
||||
static final _googleSignIn = GoogleSignIn(scopes: [
|
||||
CalendarApi.calendarScope,
|
||||
CalendarApi.calendarEventsScope,
|
||||
]);
|
||||
|
||||
List<Event> get googleEvents => _googleEvents ?? [];
|
||||
|
||||
ThirdPartyProvider({
|
||||
required BuildContext context,
|
||||
}) {
|
||||
// _context = context;
|
||||
}
|
||||
|
||||
Future<GoogleSignInAccount?> googleSignIn() async {
|
||||
if (!await _googleSignIn.isSignedIn()) {
|
||||
return _googleSignIn.signIn();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Future<void> fetchGoogle() async {
|
||||
// try {
|
||||
// var httpClient = (await _googleSignIn.authenticatedClient())!;
|
||||
// var calendarApi = CalendarApi(httpClient);
|
||||
|
||||
// var calendarList = await calendarApi.calendarList.list();
|
||||
|
||||
// if (calendarList.items == null) return;
|
||||
// if (calendarList.items!.isEmpty) return;
|
||||
|
||||
// _googleEvents = (await calendarApi.events.list(
|
||||
// '13342d17fe1e68680c43c0c44dcb7e30cb0171cc4e4ee9ee13c9ff3082d3279c@group.calendar.google.com'))
|
||||
// .items;
|
||||
|
||||
// print(calendarList.items!
|
||||
// .map((e) => (e.id ?? 'noid') + '-' + (e.description ?? 'nodesc')));
|
||||
// print(_googleEvents!.map((e) => e.toJson()));
|
||||
// } catch (e) {
|
||||
// print(e);
|
||||
// await _googleSignIn.signOut();
|
||||
// }
|
||||
// }
|
||||
|
||||
Future<Event?> pushEvent({
|
||||
required String title,
|
||||
required String calendarId,
|
||||
required DateTime start,
|
||||
required DateTime end,
|
||||
}) async {
|
||||
try {
|
||||
var httpClient = (await _googleSignIn.authenticatedClient())!;
|
||||
var calendarApi = CalendarApi(httpClient);
|
||||
|
||||
Event event = Event(
|
||||
created: DateTime.now(),
|
||||
creator: EventCreator(self: true),
|
||||
start: EventDateTime(dateTime: start),
|
||||
end: EventDateTime(dateTime: end),
|
||||
summary: title,
|
||||
);
|
||||
|
||||
return await calendarApi.events.insert(event, calendarId);
|
||||
} catch (e) {
|
||||
if (kDebugMode) print(e);
|
||||
await _googleSignIn.signOut();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<Calendar?> createCalendar({
|
||||
required String name,
|
||||
required String description,
|
||||
}) async {
|
||||
try {
|
||||
var httpClient = (await _googleSignIn.authenticatedClient())!;
|
||||
var calendarApi = CalendarApi(httpClient);
|
||||
|
||||
Calendar calendar = Calendar(
|
||||
summary: name,
|
||||
description: description,
|
||||
timeZone: 'Europe/Budapest',
|
||||
);
|
||||
|
||||
return await calendarApi.calendars.insert(calendar);
|
||||
} catch (e) {
|
||||
if (kDebugMode) print(e);
|
||||
await _googleSignIn.signOut();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> pushTimetable(
|
||||
BuildContext context, TimetableController controller) async {
|
||||
Calendar? calendar = await createCalendar(
|
||||
name: 'reFilc - Órarend',
|
||||
description:
|
||||
'Ez egy automatikusan generált naptár, melyet a reFilc hozott létre az órarend számára.',
|
||||
);
|
||||
|
||||
if (calendar == null) return;
|
||||
|
||||
final days = controller.days!;
|
||||
final everyLesson = days.expand((x) => x).toList();
|
||||
everyLesson.sort((a, b) => a.start.compareTo(b.start));
|
||||
|
||||
for (Lesson l in everyLesson) {
|
||||
Event? event = await pushEvent(
|
||||
title: l.name,
|
||||
calendarId: calendar.id!,
|
||||
start: l.start,
|
||||
end: l.end,
|
||||
);
|
||||
|
||||
// temp shit (DONT BULLY ME, ILL CUM)
|
||||
if (kDebugMode) {
|
||||
if (false != true) print(event);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
// print('finished');
|
||||
}
|
||||
}
|
||||
98
refilc/lib/providers/todo_notes_provider.dart
Normal file
98
refilc/lib/providers/todo_notes_provider.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
// // ignore_for_file: no_leading_underscores_for_local_identifiers
|
||||
|
||||
// import 'package:refilc/api/providers/user_provider.dart';
|
||||
// import 'package:refilc/api/providers/database_provider.dart';
|
||||
// import 'package:refilc/models/user.dart';
|
||||
// import 'package:refilc_kreta_api/client/api.dart';
|
||||
// import 'package:refilc_kreta_api/client/client.dart';
|
||||
// import 'package:refilc_kreta_api/models/absence.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:provider/provider.dart';
|
||||
|
||||
// class TodoNotesProvider with ChangeNotifier {
|
||||
// late Map<> _absences;
|
||||
// late BuildContext _context;
|
||||
// List<Absence> get absences => _absences;
|
||||
|
||||
// TodoNotesProvider({
|
||||
// List<Absence> initialAbsences = const [],
|
||||
// required BuildContext context,
|
||||
// }) {
|
||||
// _absences = List.castFrom(initialAbsences);
|
||||
// _context = context;
|
||||
|
||||
// if (_absences.isEmpty) restore();
|
||||
// }
|
||||
|
||||
// Future<void> restore() async {
|
||||
// String? userId = Provider.of<UserProvider>(_context, listen: false).id;
|
||||
|
||||
// // Load absences from the database
|
||||
// if (userId != null) {
|
||||
// var dbAbsences =
|
||||
// await Provider.of<DatabaseProvider>(_context, listen: false)
|
||||
// .userQuery
|
||||
// .getAbsences(userId: userId);
|
||||
// _absences = dbAbsences;
|
||||
// await convertBySettings();
|
||||
// }
|
||||
// }
|
||||
|
||||
// // for renamed subjects
|
||||
// Future<void> convertBySettings() async {
|
||||
// final _database = Provider.of<DatabaseProvider>(_context, listen: false);
|
||||
// Map<String, String> renamedSubjects =
|
||||
// (await _database.query.getSettings(_database)).renamedSubjectsEnabled
|
||||
// ? await _database.userQuery.renamedSubjects(
|
||||
// userId:
|
||||
// // ignore: use_build_context_synchronously
|
||||
// Provider.of<UserProvider>(_context, listen: false).user!.id)
|
||||
// : {};
|
||||
// Map<String, String> renamedTeachers =
|
||||
// (await _database.query.getSettings(_database)).renamedTeachersEnabled
|
||||
// ? await _database.userQuery.renamedTeachers(
|
||||
// userId:
|
||||
// // ignore: use_build_context_synchronously
|
||||
// Provider.of<UserProvider>(_context, listen: false).user!.id)
|
||||
// : {};
|
||||
|
||||
// for (Absence absence in _absences) {
|
||||
// absence.subject.renamedTo = renamedSubjects.isNotEmpty
|
||||
// ? renamedSubjects[absence.subject.id]
|
||||
// : null;
|
||||
// absence.teacher.renamedTo = renamedTeachers.isNotEmpty
|
||||
// ? renamedTeachers[absence.teacher.id]
|
||||
// : null;
|
||||
// }
|
||||
|
||||
// notifyListeners();
|
||||
// }
|
||||
|
||||
// // Fetches Absences from the Kreta API then stores them in the database
|
||||
// Future<void> fetch() async {
|
||||
// User? user = Provider.of<UserProvider>(_context, listen: false).user;
|
||||
// if (user == null) throw "Cannot fetch Absences for User null";
|
||||
// String iss = user.instituteCode;
|
||||
|
||||
// List? absencesJson = await Provider.of<KretaClient>(_context, listen: false)
|
||||
// .getAPI(KretaAPI.absences(iss));
|
||||
// if (absencesJson == null) throw "Cannot fetch Absences for User ${user.id}";
|
||||
// List<Absence> absences =
|
||||
// absencesJson.map((e) => Absence.fromJson(e)).toList();
|
||||
|
||||
// if (absences.isNotEmpty || _absences.isNotEmpty) await store(absences);
|
||||
// }
|
||||
|
||||
// // Stores Absences in the database
|
||||
// Future<void> store(List<Absence> absences) async {
|
||||
// User? user = Provider.of<UserProvider>(_context, listen: false).user;
|
||||
// if (user == null) throw "Cannot store Absences for User null";
|
||||
// String userId = user.id;
|
||||
|
||||
// await Provider.of<DatabaseProvider>(_context, listen: false)
|
||||
// .userStore
|
||||
// .storeAbsences(absences, userId: userId);
|
||||
// _absences = absences;
|
||||
// await convertBySettings();
|
||||
// }
|
||||
// }
|
||||
33
refilc/lib/theme/colors/accent.dart
Normal file
33
refilc/lib/theme/colors/accent.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum AccentColor {
|
||||
filc,
|
||||
blue,
|
||||
green,
|
||||
lime,
|
||||
yellow,
|
||||
orange,
|
||||
red,
|
||||
pink,
|
||||
purple,
|
||||
none,
|
||||
ogfilc,
|
||||
adaptive,
|
||||
custom
|
||||
}
|
||||
|
||||
Map<AccentColor, Color> accentColorMap = {
|
||||
AccentColor.filc: const Color(0xFF3D7BF4),
|
||||
AccentColor.blue: Colors.blue.shade300,
|
||||
AccentColor.green: Colors.green.shade400,
|
||||
AccentColor.lime: Colors.lightGreen.shade400,
|
||||
AccentColor.yellow: Colors.orange.shade300,
|
||||
AccentColor.orange: Colors.deepOrange.shade300,
|
||||
AccentColor.red: Colors.red.shade300,
|
||||
AccentColor.pink: Colors.pink.shade300,
|
||||
AccentColor.purple: Colors.purple.shade300,
|
||||
//AccentColor.none: Colors.black,
|
||||
AccentColor.ogfilc: const Color(0xff20AC9B),
|
||||
AccentColor.adaptive: const Color(0xFF3D7BF4),
|
||||
AccentColor.custom: const Color(0xFF3D7BF4),
|
||||
};
|
||||
59
refilc/lib/theme/colors/colors.dart
Normal file
59
refilc/lib/theme/colors/colors.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:refilc/theme/colors/dark_desktop.dart';
|
||||
import 'package:refilc/theme/colors/dark_mobile.dart';
|
||||
import 'package:refilc/theme/colors/light_desktop.dart';
|
||||
import 'package:refilc/theme/colors/light_mobile.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppColors {
|
||||
static ThemeAppColors of(BuildContext context) =>
|
||||
fromBrightness(Theme.of(context).brightness);
|
||||
|
||||
static ThemeAppColors fromBrightness(Brightness brightness) {
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
switch (brightness) {
|
||||
case Brightness.light:
|
||||
return LightMobileAppColors();
|
||||
case Brightness.dark:
|
||||
return DarkMobileAppColors();
|
||||
}
|
||||
} else {
|
||||
switch (brightness) {
|
||||
case Brightness.light:
|
||||
return LightDesktopAppColors();
|
||||
case Brightness.dark:
|
||||
return DarkDesktopAppColors();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class ThemeAppColors {
|
||||
final Color shadow = const Color(0x00000000);
|
||||
final Color text = const Color(0x00000000);
|
||||
final Color background = const Color(0x00000000);
|
||||
final Color highlight = const Color(0x00000000);
|
||||
final Color red = const Color(0x00000000);
|
||||
final Color orange = const Color(0x00000000);
|
||||
final Color yellow = const Color(0x00000000);
|
||||
final Color green = const Color(0x00000000);
|
||||
final Color filc = const Color(0x00000000);
|
||||
final Color teal = const Color(0x00000000);
|
||||
final Color blue = const Color(0x00000000);
|
||||
final Color indigo = const Color(0x00000000);
|
||||
final Color purple = const Color(0x00000000);
|
||||
final Color pink = const Color(0x00000000);
|
||||
// new default grade colors
|
||||
final Color gradeFive = const Color(0x00000000);
|
||||
final Color gradeFour = const Color(0x00000000);
|
||||
final Color gradeThree = const Color(0x00000000);
|
||||
final Color gradeTwo = const Color(0x00000000);
|
||||
final Color gradeOne = const Color(0x00000000);
|
||||
// v5 ui login
|
||||
final loginPrimary = const Color(0x00000000);
|
||||
final loginSecondary = const Color(0x00000000);
|
||||
final inputBorder = const Color(0x00000000);
|
||||
final loginBackground = const Color(0x00000000);
|
||||
final buttonBackground = const Color(0x00000000);
|
||||
}
|
||||
55
refilc/lib/theme/colors/dark_desktop.dart
Normal file
55
refilc/lib/theme/colors/dark_desktop.dart
Normal file
@@ -0,0 +1,55 @@
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DarkDesktopAppColors implements ThemeAppColors {
|
||||
@override
|
||||
final shadow = const Color(0x00000000);
|
||||
@override
|
||||
final text = Colors.white;
|
||||
@override
|
||||
final background = const Color.fromARGB(255, 42, 42, 42);
|
||||
@override
|
||||
final highlight = const Color.fromARGB(255, 46, 48, 50);
|
||||
@override
|
||||
final red = const Color(0xffFF453A);
|
||||
@override
|
||||
final orange = const Color(0xffFF9F0A);
|
||||
@override
|
||||
final yellow = const Color(0xffFFD60A);
|
||||
@override
|
||||
final green = const Color(0xff32D74B);
|
||||
@override
|
||||
final filc = const Color(0xff3d7bf4);
|
||||
@override
|
||||
final teal = const Color(0xff64D2FF);
|
||||
@override
|
||||
final blue = const Color(0xff0A84FF);
|
||||
@override
|
||||
final indigo = const Color(0xff5E5CE6);
|
||||
@override
|
||||
final purple = const Color(0xffBF5AF2);
|
||||
@override
|
||||
final pink = const Color(0xffFF375F);
|
||||
// new default grade colors
|
||||
@override
|
||||
final gradeFive = const Color(0xff3d7bf4);
|
||||
@override
|
||||
final gradeFour = const Color(0xFF4C3DF4);
|
||||
@override
|
||||
final gradeThree = const Color(0xFF833DF4);
|
||||
@override
|
||||
final gradeTwo = const Color(0xFFAE3DF4);
|
||||
@override
|
||||
final gradeOne = const Color(0xFFF43DAB);
|
||||
// v5 ui login
|
||||
@override
|
||||
final loginPrimary = const Color(0xFF0A1C41);
|
||||
@override
|
||||
final loginSecondary = const Color(0xFF0A1C41);
|
||||
@override
|
||||
final inputBorder = const Color(0xFF586A8E);
|
||||
@override
|
||||
final loginBackground = const Color(0xFFEFF4FE);
|
||||
@override
|
||||
final buttonBackground = const Color(0xFF0A1C41);
|
||||
}
|
||||
56
refilc/lib/theme/colors/dark_mobile.dart
Normal file
56
refilc/lib/theme/colors/dark_mobile.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DarkMobileAppColors implements ThemeAppColors {
|
||||
@override
|
||||
final shadow = const Color(0x00000000);
|
||||
@override
|
||||
final text = Colors.white;
|
||||
@override
|
||||
final background = const Color(0xff000000);
|
||||
@override
|
||||
final highlight = const Color(0xff141516);
|
||||
@override
|
||||
final red = const Color(0xffFF453A);
|
||||
@override
|
||||
final orange = const Color(0xffFF9F0A);
|
||||
@override
|
||||
final yellow = const Color(0xffFFD60A);
|
||||
@override
|
||||
final green = const Color(0xff32D74B);
|
||||
@override
|
||||
final filc = const Color(0xff3d7bf4);
|
||||
@override
|
||||
final teal = const Color(0xff64D2FF);
|
||||
@override
|
||||
final blue = const Color(0xff0A84FF);
|
||||
@override
|
||||
final indigo = const Color(0xff5E5CE6);
|
||||
@override
|
||||
// new default grade colors
|
||||
@override
|
||||
final gradeFive = const Color(0xff3d7bf4);
|
||||
@override
|
||||
final gradeFour = const Color(0xFF4C3DF4);
|
||||
@override
|
||||
final gradeThree = const Color(0xFF833DF4);
|
||||
@override
|
||||
final gradeTwo = const Color(0xFFAE3DF4);
|
||||
@override
|
||||
final gradeOne = const Color(0xFFF43DAB);
|
||||
@override
|
||||
final purple = const Color(0xffBF5AF2);
|
||||
@override
|
||||
final pink = const Color(0xffFF375F);
|
||||
// v5 ui login
|
||||
@override
|
||||
final loginPrimary = const Color(0xFFD4DAE7);
|
||||
@override
|
||||
final loginSecondary = const Color(0xFFA4B1CC);
|
||||
@override
|
||||
final inputBorder = const Color(0xFF586A8E);
|
||||
@override
|
||||
final loginBackground = const Color(0xFF0F131D);
|
||||
@override
|
||||
final buttonBackground = const Color(0xFF3D7BF4);
|
||||
}
|
||||
55
refilc/lib/theme/colors/light_desktop.dart
Normal file
55
refilc/lib/theme/colors/light_desktop.dart
Normal file
@@ -0,0 +1,55 @@
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class LightDesktopAppColors implements ThemeAppColors {
|
||||
@override
|
||||
final shadow = const Color(0xffE8E8E8);
|
||||
@override
|
||||
final text = Colors.black;
|
||||
@override
|
||||
final background = const Color(0xffF4F9FF);
|
||||
@override
|
||||
final highlight = const Color(0xffFFFFFF);
|
||||
@override
|
||||
final red = const Color(0xffFF3B30);
|
||||
@override
|
||||
final orange = const Color(0xffFF9500);
|
||||
@override
|
||||
final yellow = const Color(0xffFFCC00);
|
||||
@override
|
||||
final green = const Color(0xff34C759);
|
||||
@override
|
||||
final filc = const Color(0xff3d7bf4);
|
||||
@override
|
||||
final teal = const Color(0xff5AC8FA);
|
||||
@override
|
||||
final blue = const Color(0xff007AFF);
|
||||
@override
|
||||
final indigo = const Color(0xff5856D6);
|
||||
@override
|
||||
final purple = const Color(0xffAF52DE);
|
||||
@override
|
||||
final pink = const Color(0xffFF2D55);
|
||||
// new default grade colors
|
||||
@override
|
||||
final gradeFive = const Color(0xff3d7bf4);
|
||||
@override
|
||||
final gradeFour = const Color(0xFF4C3DF4);
|
||||
@override
|
||||
final gradeThree = const Color(0xFF833DF4);
|
||||
@override
|
||||
final gradeTwo = const Color(0xFFAE3DF4);
|
||||
@override
|
||||
final gradeOne = const Color(0xFFF43DAB);
|
||||
// v5 ui login
|
||||
@override
|
||||
final loginPrimary = const Color(0xFF0A1C41);
|
||||
@override
|
||||
final loginSecondary = const Color(0xFF0A1C41);
|
||||
@override
|
||||
final inputBorder = const Color(0xFF586A8E);
|
||||
@override
|
||||
final loginBackground = const Color(0xFFEFF4FE);
|
||||
@override
|
||||
final buttonBackground = const Color(0xFF0A1C41);
|
||||
}
|
||||
55
refilc/lib/theme/colors/light_mobile.dart
Normal file
55
refilc/lib/theme/colors/light_mobile.dart
Normal file
@@ -0,0 +1,55 @@
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class LightMobileAppColors implements ThemeAppColors {
|
||||
@override
|
||||
final shadow = const Color(0xffE8E8E8);
|
||||
@override
|
||||
final text = const Color(0xFF000000);
|
||||
@override
|
||||
final background = const Color(0xffF4F9FF);
|
||||
@override
|
||||
final highlight = const Color(0xffFFFFFF);
|
||||
@override
|
||||
final red = const Color(0xffFF3B30);
|
||||
@override
|
||||
final orange = const Color(0xffFF9500);
|
||||
@override
|
||||
final yellow = const Color(0xffFFCC00);
|
||||
@override
|
||||
final green = const Color(0xff34C759);
|
||||
@override
|
||||
final filc = const Color(0xff3d7bf4);
|
||||
@override
|
||||
final teal = const Color(0xff5AC8FA);
|
||||
@override
|
||||
final blue = const Color(0xff007AFF);
|
||||
@override
|
||||
final indigo = const Color(0xff5856D6);
|
||||
@override
|
||||
final purple = const Color(0xffAF52DE);
|
||||
@override
|
||||
final pink = const Color(0xffFF2D55);
|
||||
// new default grade colors
|
||||
@override
|
||||
final gradeFive = const Color(0xff3d7bf4);
|
||||
@override
|
||||
final gradeFour = const Color(0xFF4C3DF4);
|
||||
@override
|
||||
final gradeThree = const Color(0xFF833DF4);
|
||||
@override
|
||||
final gradeTwo = const Color(0xFFAE3DF4);
|
||||
@override
|
||||
final gradeOne = const Color(0xFFF43DAB);
|
||||
// ui v5 login
|
||||
@override
|
||||
final loginPrimary = const Color(0xFF0A1C41);
|
||||
@override
|
||||
final loginSecondary = const Color(0xFF0A1C41);
|
||||
@override
|
||||
final inputBorder = const Color(0xFF586A8E);
|
||||
@override
|
||||
final loginBackground = const Color(0xFFEFF4FE);
|
||||
@override
|
||||
final buttonBackground = const Color(0xFF0A1C41);
|
||||
}
|
||||
18
refilc/lib/theme/observer.dart
Normal file
18
refilc/lib/theme/observer.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ThemeModeObserver extends ChangeNotifier {
|
||||
ThemeMode _themeMode;
|
||||
bool _updateNavbarColor;
|
||||
ThemeMode get themeMode => _themeMode;
|
||||
bool get updateNavbarColor => _updateNavbarColor;
|
||||
|
||||
ThemeModeObserver({ThemeMode initialTheme = ThemeMode.system, bool updateNavbarColor = true})
|
||||
: _themeMode = initialTheme,
|
||||
_updateNavbarColor = updateNavbarColor;
|
||||
|
||||
void changeTheme(ThemeMode mode, {bool updateNavbarColor = true}) {
|
||||
_themeMode = mode;
|
||||
_updateNavbarColor = updateNavbarColor;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
209
refilc/lib/theme/theme.dart
Normal file
209
refilc/lib/theme/theme.dart
Normal file
@@ -0,0 +1,209 @@
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/theme/colors/accent.dart';
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:refilc/theme/observer.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_color_utilities/material_color_utilities.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:google_fonts/google_fonts.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 _defaultFontFamily = "Montserrat";
|
||||
|
||||
static Color? _paletteAccentLight(CorePalette? palette) =>
|
||||
palette != null ? Color(palette.primary.get(70)) : null;
|
||||
static Color? _paletteHighlightLight(CorePalette? palette) =>
|
||||
palette != null ? Color(palette.neutral.get(100)) : null;
|
||||
static Color? _paletteBackgroundLight(CorePalette? palette) =>
|
||||
palette != null ? Color(palette.neutral.get(95)) : null;
|
||||
|
||||
static Color? _paletteAccentDark(CorePalette? palette) =>
|
||||
palette != null ? Color(palette.primary.get(80)) : null;
|
||||
static Color? _paletteBackgroundDark(CorePalette? palette) =>
|
||||
palette != null ? Color(palette.neutralVariant.get(10)) : null;
|
||||
static Color? _paletteHighlightDark(CorePalette? palette) =>
|
||||
palette != null ? Color(palette.neutralVariant.get(20)) : null;
|
||||
|
||||
static Map<String, TextTheme?> googleFontsMap = {
|
||||
"Merienda": GoogleFonts.meriendaTextTheme(),
|
||||
"M PLUS Code Latin": GoogleFonts.mPlusCodeLatinTextTheme(),
|
||||
"Figtree": GoogleFonts.figtreeTextTheme(),
|
||||
"Fira Code": GoogleFonts.firaCodeTextTheme(),
|
||||
"Vollkorn": GoogleFonts.vollkornTextTheme(),
|
||||
};
|
||||
|
||||
// Light Theme
|
||||
static ThemeData lightTheme(BuildContext context, {CorePalette? palette}) {
|
||||
var lightColors = AppColors.fromBrightness(Brightness.light);
|
||||
final settings = Provider.of<SettingsProvider>(context, listen: false);
|
||||
AccentColor accentColor = settings.accentColor;
|
||||
final customAccentColor =
|
||||
accentColor == AccentColor.custom ? settings.customAccentColor : null;
|
||||
Color accent = customAccentColor ??
|
||||
accentColorMap[accentColor] ??
|
||||
const Color(0x00000000);
|
||||
|
||||
if (accentColor == AccentColor.adaptive) {
|
||||
if (palette != null) accent = _paletteAccentLight(palette)!;
|
||||
} else {
|
||||
palette = null;
|
||||
}
|
||||
|
||||
Color backgroundColor = (accentColor == AccentColor.custom
|
||||
? settings.customBackgroundColor
|
||||
: _paletteBackgroundLight(palette)) ??
|
||||
lightColors.background;
|
||||
Color highlightColor = (accentColor == AccentColor.custom
|
||||
? settings.customHighlightColor
|
||||
: _paletteHighlightLight(palette)) ??
|
||||
lightColors.highlight;
|
||||
|
||||
return ThemeData(
|
||||
brightness: Brightness.light,
|
||||
useMaterial3: true,
|
||||
fontFamily: _defaultFontFamily,
|
||||
textTheme: googleFontsMap[settings.fontFamily],
|
||||
scaffoldBackgroundColor: backgroundColor,
|
||||
primaryColor: lightColors.filc,
|
||||
dividerColor: const Color(0x00000000),
|
||||
colorScheme: ColorScheme(
|
||||
primary: accent,
|
||||
onPrimary:
|
||||
(accent.computeLuminance() > 0.5 ? Colors.black : Colors.white)
|
||||
.withOpacity(.9),
|
||||
secondary: accent,
|
||||
onSecondary:
|
||||
(accent.computeLuminance() > 0.5 ? Colors.black : Colors.white)
|
||||
.withOpacity(.9),
|
||||
background: highlightColor,
|
||||
onBackground: Colors.black.withOpacity(.9),
|
||||
brightness: Brightness.light,
|
||||
error: lightColors.red,
|
||||
onError: Colors.white.withOpacity(.9),
|
||||
surface: highlightColor,
|
||||
onSurface: Colors.black.withOpacity(.9),
|
||||
),
|
||||
shadowColor: lightColors.shadow.withOpacity(.5),
|
||||
appBarTheme: AppBarTheme(backgroundColor: backgroundColor),
|
||||
indicatorColor: accent,
|
||||
iconTheme: IconThemeData(color: lightColors.text.withOpacity(.75)),
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
indicatorColor:
|
||||
accent.withOpacity(accentColor == AccentColor.adaptive ? 0.4 : 0.8),
|
||||
iconTheme:
|
||||
MaterialStateProperty.all(IconThemeData(color: lightColors.text)),
|
||||
backgroundColor: highlightColor,
|
||||
labelTextStyle: MaterialStateProperty.all(TextStyle(
|
||||
fontSize: 13.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: lightColors.text.withOpacity(0.8),
|
||||
)),
|
||||
labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
|
||||
height: 76.0,
|
||||
),
|
||||
sliderTheme: SliderThemeData(
|
||||
inactiveTrackColor: accent.withOpacity(.3),
|
||||
),
|
||||
progressIndicatorTheme: ProgressIndicatorThemeData(color: accent),
|
||||
expansionTileTheme: ExpansionTileThemeData(iconColor: accent),
|
||||
cardColor: highlightColor,
|
||||
bottomNavigationBarTheme: BottomNavigationBarThemeData(
|
||||
backgroundColor: Provider.of<ThemeModeObserver>(context, listen: false)
|
||||
.updateNavbarColor
|
||||
? backgroundColor
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Dark Theme
|
||||
static ThemeData darkTheme(BuildContext context, {CorePalette? palette}) {
|
||||
var darkColors = AppColors.fromBrightness(Brightness.dark);
|
||||
final settings = Provider.of<SettingsProvider>(context, listen: false);
|
||||
AccentColor accentColor = settings.accentColor;
|
||||
final customAccentColor =
|
||||
accentColor == AccentColor.custom ? settings.customAccentColor : null;
|
||||
Color accent = customAccentColor ??
|
||||
accentColorMap[accentColor] ??
|
||||
const Color(0x00000000);
|
||||
|
||||
if (accentColor == AccentColor.adaptive) {
|
||||
if (palette != null) accent = _paletteAccentDark(palette)!;
|
||||
} else {
|
||||
palette = null;
|
||||
}
|
||||
|
||||
Color backgroundColor = (accentColor == AccentColor.custom
|
||||
? settings.customBackgroundColor
|
||||
: _paletteBackgroundDark(palette)) ??
|
||||
darkColors.background;
|
||||
Color highlightColor = (accentColor == AccentColor.custom
|
||||
? settings.customHighlightColor
|
||||
: _paletteHighlightDark(palette)) ??
|
||||
darkColors.highlight;
|
||||
|
||||
return ThemeData(
|
||||
brightness: Brightness.dark,
|
||||
useMaterial3: true,
|
||||
fontFamily: _defaultFontFamily,
|
||||
textTheme: googleFontsMap[settings.fontFamily],
|
||||
scaffoldBackgroundColor: backgroundColor,
|
||||
primaryColor: darkColors.filc,
|
||||
dividerColor: const Color(0x00000000),
|
||||
colorScheme: ColorScheme(
|
||||
primary: accent,
|
||||
onPrimary:
|
||||
(accent.computeLuminance() > 0.5 ? Colors.black : Colors.white)
|
||||
.withOpacity(.9),
|
||||
secondary: accent,
|
||||
onSecondary:
|
||||
(accent.computeLuminance() > 0.5 ? Colors.black : Colors.white)
|
||||
.withOpacity(.9),
|
||||
background: highlightColor,
|
||||
onBackground: Colors.white.withOpacity(.9),
|
||||
brightness: Brightness.dark,
|
||||
error: darkColors.red,
|
||||
onError: Colors.black.withOpacity(.9),
|
||||
surface: highlightColor,
|
||||
onSurface: Colors.white.withOpacity(.9),
|
||||
),
|
||||
shadowColor: highlightColor.withOpacity(.5), //darkColors.shadow,
|
||||
appBarTheme: AppBarTheme(backgroundColor: backgroundColor),
|
||||
indicatorColor: accent,
|
||||
iconTheme: IconThemeData(color: darkColors.text.withOpacity(.75)),
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
indicatorColor:
|
||||
accent.withOpacity(accentColor == AccentColor.adaptive ? 0.4 : 0.8),
|
||||
iconTheme:
|
||||
MaterialStateProperty.all(IconThemeData(color: darkColors.text)),
|
||||
backgroundColor: highlightColor,
|
||||
labelTextStyle: MaterialStateProperty.all(TextStyle(
|
||||
fontSize: 13.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: darkColors.text.withOpacity(0.8),
|
||||
)),
|
||||
labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
|
||||
height: 76.0,
|
||||
),
|
||||
sliderTheme: SliderThemeData(
|
||||
inactiveTrackColor: accent.withOpacity(.3),
|
||||
),
|
||||
progressIndicatorTheme: ProgressIndicatorThemeData(color: accent),
|
||||
expansionTileTheme: ExpansionTileThemeData(iconColor: accent),
|
||||
cardColor: highlightColor,
|
||||
chipTheme: ChipThemeData(
|
||||
backgroundColor: accent.withOpacity(.2),
|
||||
elevation: 1,
|
||||
),
|
||||
bottomNavigationBarTheme: BottomNavigationBarThemeData(
|
||||
backgroundColor: Provider.of<ThemeModeObserver>(context, listen: false)
|
||||
.updateNavbarColor
|
||||
? backgroundColor
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
8
refilc/lib/ui/date_widget.dart
Normal file
8
refilc/lib/ui/date_widget.dart
Normal file
@@ -0,0 +1,8 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class DateWidget {
|
||||
final DateTime date;
|
||||
final Widget widget;
|
||||
final String? key;
|
||||
const DateWidget({required this.date, required this.widget, this.key});
|
||||
}
|
||||
198
refilc/lib/ui/filter/sort.dart
Normal file
198
refilc/lib/ui/filter/sort.dart
Normal file
@@ -0,0 +1,198 @@
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:refilc/ui/date_widget.dart';
|
||||
import 'package:refilc/ui/filter/widgets.dart';
|
||||
import 'package:refilc/ui/widgets/message/message_tile.dart';
|
||||
import 'package:refilc_kreta_api/models/message.dart';
|
||||
import 'package:refilc_mobile_ui/common/panel/panel.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/absence/absence_viewable.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/absence_group/absence_group_tile.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/cretification/certification_card.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/grade/new_grades.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:animated_list_plus/animated_list_plus.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/lesson/changed_lesson_tile.dart';
|
||||
import 'package:refilc/utils/format.dart';
|
||||
|
||||
// difference.inDays is not reliable
|
||||
bool _sameDate(DateTime a, DateTime b) =>
|
||||
(a.year == b.year && a.month == b.month && a.day == b.day);
|
||||
|
||||
List<Widget> sortDateWidgets(
|
||||
BuildContext context, {
|
||||
required List<DateWidget> dateWidgets,
|
||||
bool showTitle = true,
|
||||
bool showDivider = false,
|
||||
bool hasShadow = false,
|
||||
EdgeInsetsGeometry? padding,
|
||||
}) {
|
||||
dateWidgets.sort((a, b) => -a.date.compareTo(b.date));
|
||||
|
||||
List<Conversation> conversations = [];
|
||||
List<DateWidget> convMessages = [];
|
||||
|
||||
// Group messages into conversations
|
||||
for (var w in dateWidgets) {
|
||||
if (w.widget.runtimeType == MessageTile) {
|
||||
Message message = (w.widget as MessageTile).message;
|
||||
|
||||
if (message.conversationId != null) {
|
||||
convMessages.add(w);
|
||||
|
||||
Conversation conv = conversations.firstWhere(
|
||||
(e) => e.id == message.conversationId,
|
||||
orElse: () => Conversation(id: message.conversationId!));
|
||||
conv.add(message);
|
||||
if (conv.messages.length == 1) conversations.add(conv);
|
||||
}
|
||||
|
||||
if (conversations.any((c) => c.id == message.messageId)) {
|
||||
Conversation conv =
|
||||
conversations.firstWhere((e) => e.id == message.messageId);
|
||||
convMessages.add(w);
|
||||
conv.add(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove individual messages
|
||||
for (var e in convMessages) {
|
||||
dateWidgets.remove(e);
|
||||
}
|
||||
|
||||
// Add conversations
|
||||
for (var conv in conversations) {
|
||||
conv.sort();
|
||||
|
||||
dateWidgets.add(DateWidget(
|
||||
key: "${conv.newest.date.millisecondsSinceEpoch}-msg",
|
||||
date: conv.newest.date,
|
||||
widget: MessageTile(
|
||||
conv.newest,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
dateWidgets.sort((a, b) => -a.date.compareTo(b.date));
|
||||
|
||||
List<List<DateWidget>> groupedDateWidgets = [[]];
|
||||
for (var element in dateWidgets) {
|
||||
if (groupedDateWidgets.last.isNotEmpty) {
|
||||
if (!_sameDate(element.date, groupedDateWidgets.last.last.date)) {
|
||||
groupedDateWidgets.add([element]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
groupedDateWidgets.last.add(element);
|
||||
}
|
||||
|
||||
List<DateWidget> items = [];
|
||||
|
||||
if (groupedDateWidgets.first.isNotEmpty) {
|
||||
for (var elements in groupedDateWidgets) {
|
||||
bool cst = showTitle;
|
||||
|
||||
// Group Absence Tiles
|
||||
List<DateWidget> absenceTileWidgets = elements.where((element) {
|
||||
return element.widget is AbsenceViewable &&
|
||||
(element.widget as AbsenceViewable).absence.delay == 0;
|
||||
}).toList();
|
||||
List<AbsenceViewable> absenceTiles =
|
||||
absenceTileWidgets.map((e) => e.widget as AbsenceViewable).toList();
|
||||
if (absenceTiles.length > 1) {
|
||||
elements.removeWhere((element) =>
|
||||
element.widget.runtimeType == AbsenceViewable &&
|
||||
(element.widget as AbsenceViewable).absence.delay == 0);
|
||||
if (elements.isEmpty) {
|
||||
cst = false;
|
||||
}
|
||||
elements.add(
|
||||
DateWidget(
|
||||
widget: AbsenceGroupTile(
|
||||
absenceTiles,
|
||||
showDate: !cst,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
),
|
||||
date: absenceTileWidgets.first.date,
|
||||
key:
|
||||
"${absenceTileWidgets.first.date.millisecondsSinceEpoch}-absence-group"),
|
||||
);
|
||||
}
|
||||
|
||||
// Bring Lesson Tiles to front & sort by index asc
|
||||
List<DateWidget> lessonTiles = elements.where((element) {
|
||||
return element.widget.runtimeType == ChangedLessonTile;
|
||||
}).toList();
|
||||
lessonTiles.sort((a, b) => (a.widget as ChangedLessonTile)
|
||||
.lesson
|
||||
.lessonIndex
|
||||
.compareTo((b.widget as ChangedLessonTile).lesson.lessonIndex));
|
||||
elements.removeWhere(
|
||||
(element) => element.widget.runtimeType == ChangedLessonTile);
|
||||
elements.insertAll(0, lessonTiles);
|
||||
|
||||
final date = (elements + absenceTileWidgets).first.date;
|
||||
items.add(DateWidget(
|
||||
date: date,
|
||||
widget: Panel(
|
||||
isTransparent: true,
|
||||
key: ValueKey(date),
|
||||
padding: padding ?? const EdgeInsets.symmetric(vertical: 6.0),
|
||||
title: cst ? Text(date.format(context, forceToday: true)) : null,
|
||||
hasShadow: hasShadow,
|
||||
child: ImplicitlyAnimatedList<DateWidget>(
|
||||
areItemsTheSame: (a, b) => a.key == b.key,
|
||||
spawnIsolate: false,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, animation, item, index) => filterItemBuilder(
|
||||
context,
|
||||
animation,
|
||||
item.widget,
|
||||
index,
|
||||
len: elements.length,
|
||||
isAfterSeparated: index > 0 &&
|
||||
(elements[index - 1].widget is CertificationCard ||
|
||||
elements[index - 1].widget is NewGradesSurprise),
|
||||
isBeforeSeparated: (index < elements.length - 1) &&
|
||||
(elements[index + 1].widget is CertificationCard ||
|
||||
elements[index + 1].widget is NewGradesSurprise),
|
||||
),
|
||||
items: elements,
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
final nh = DateTime.now();
|
||||
final now =
|
||||
DateTime(nh.year, nh.month, nh.day).subtract(const Duration(seconds: 1));
|
||||
|
||||
if (showDivider &&
|
||||
items.any((i) => i.date.isBefore(now)) &&
|
||||
items.any((i) => i.date.isAfter(now))) {
|
||||
items.add(
|
||||
DateWidget(
|
||||
date: now,
|
||||
widget: Center(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
height: 3.0,
|
||||
width: 150.0,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
color: AppColors.of(context).text.withOpacity(.25),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Sort future dates asc, past dates desc
|
||||
items.sort((a, b) =>
|
||||
(a.date.isAfter(now) && b.date.isAfter(now) ? 1 : -1) *
|
||||
a.date.compareTo(b.date));
|
||||
|
||||
return items.map((e) => e.widget).toList();
|
||||
}
|
||||
280
refilc/lib/ui/filter/widgets.dart
Normal file
280
refilc/lib/ui/filter/widgets.dart
Normal file
@@ -0,0 +1,280 @@
|
||||
import 'package:refilc/api/providers/ad_provider.dart';
|
||||
import 'package:refilc/api/providers/update_provider.dart';
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/ui/date_widget.dart';
|
||||
import 'package:refilc/ui/filter/widgets/grades.dart' as grade_filter;
|
||||
import 'package:refilc/ui/filter/widgets/certifications.dart'
|
||||
as certification_filter;
|
||||
import 'package:refilc/ui/filter/widgets/messages.dart' as message_filter;
|
||||
import 'package:refilc/ui/filter/widgets/absences.dart' as absence_filter;
|
||||
import 'package:refilc/ui/filter/widgets/homework.dart' as homework_filter;
|
||||
import 'package:refilc/ui/filter/widgets/exams.dart' as exam_filter;
|
||||
import 'package:refilc/ui/filter/widgets/notes.dart' as note_filter;
|
||||
import 'package:refilc/ui/filter/widgets/events.dart' as event_filter;
|
||||
import 'package:refilc/ui/filter/widgets/lessons.dart' as lesson_filter;
|
||||
import 'package:refilc/ui/filter/widgets/update.dart' as update_filter;
|
||||
import 'package:refilc/ui/filter/widgets/missed_exams.dart'
|
||||
as missed_exam_filter;
|
||||
import 'package:refilc/ui/filter/widgets/ads.dart' as ad_filter;
|
||||
import 'package:refilc_kreta_api/models/week.dart';
|
||||
import 'package:refilc_kreta_api/providers/absence_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/event_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/exam_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/grade_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/homework_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/message_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/note_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/timetable_provider.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/cretification/certification_card.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/grade/new_grades.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/note/note_viewable.dart';
|
||||
import 'package:refilc_plus/providers/premium_provider.dart';
|
||||
import 'package:refilc_plus/ui/mobile/premium/premium_inline.dart';
|
||||
import 'package:refilc_mobile_ui/common/panel/panel.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:animated_list_plus/transitions.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
const List<FilterType> homeFilters = [
|
||||
FilterType.all,
|
||||
FilterType.grades,
|
||||
FilterType.exams,
|
||||
FilterType.messages,
|
||||
FilterType.absences
|
||||
];
|
||||
|
||||
enum FilterType {
|
||||
all,
|
||||
grades,
|
||||
newGrades,
|
||||
messages,
|
||||
absences,
|
||||
homework,
|
||||
exams,
|
||||
notes,
|
||||
events,
|
||||
lessons,
|
||||
updates,
|
||||
certifications,
|
||||
missedExams,
|
||||
ads,
|
||||
}
|
||||
|
||||
Future<List<DateWidget>> getFilterWidgets(FilterType activeData,
|
||||
{bool absencesNoExcused = false, required BuildContext context}) async {
|
||||
final gradeProvider = Provider.of<GradeProvider>(context);
|
||||
final timetableProvider = Provider.of<TimetableProvider>(context);
|
||||
final messageProvider = Provider.of<MessageProvider>(context);
|
||||
final absenceProvider = Provider.of<AbsenceProvider>(context);
|
||||
final homeworkProvider = Provider.of<HomeworkProvider>(context);
|
||||
final examProvider = Provider.of<ExamProvider>(context);
|
||||
final noteProvider = Provider.of<NoteProvider>(context);
|
||||
final eventProvider = Provider.of<EventProvider>(context);
|
||||
final updateProvider = Provider.of<UpdateProvider>(context);
|
||||
final settingsProvider = Provider.of<SettingsProvider>(context);
|
||||
final adProvider = Provider.of<AdProvider>(context);
|
||||
|
||||
List<DateWidget> items = [];
|
||||
|
||||
switch (activeData) {
|
||||
// All
|
||||
case FilterType.all:
|
||||
final all = await Future.wait<List<DateWidget>>([
|
||||
getFilterWidgets(FilterType.grades, context: context),
|
||||
getFilterWidgets(FilterType.lessons, context: context),
|
||||
getFilterWidgets(FilterType.messages, context: context),
|
||||
getFilterWidgets(FilterType.absences,
|
||||
context: context, absencesNoExcused: true),
|
||||
getFilterWidgets(FilterType.homework, context: context),
|
||||
getFilterWidgets(FilterType.exams, context: context),
|
||||
getFilterWidgets(FilterType.updates, context: context),
|
||||
getFilterWidgets(FilterType.certifications, context: context),
|
||||
getFilterWidgets(FilterType.missedExams, context: context),
|
||||
getFilterWidgets(FilterType.ads, context: context),
|
||||
]);
|
||||
items = all.expand((x) => x).toList();
|
||||
|
||||
break;
|
||||
|
||||
// Grades
|
||||
case FilterType.grades:
|
||||
if (!settingsProvider.gradeOpeningFun) {
|
||||
gradeProvider.seenAll();
|
||||
}
|
||||
items = grade_filter.getWidgets(
|
||||
gradeProvider.grades, gradeProvider.lastSeenDate);
|
||||
if (settingsProvider.gradeOpeningFun) {
|
||||
items.addAll(
|
||||
// ignore: use_build_context_synchronously
|
||||
await getFilterWidgets(FilterType.newGrades, context: context));
|
||||
}
|
||||
break;
|
||||
|
||||
// Grades
|
||||
case FilterType.newGrades:
|
||||
items = grade_filter.getNewWidgets(
|
||||
gradeProvider.grades, gradeProvider.lastSeenDate);
|
||||
break;
|
||||
|
||||
// Certifications
|
||||
case FilterType.certifications:
|
||||
items = certification_filter.getWidgets(gradeProvider.grades);
|
||||
break;
|
||||
|
||||
// Messages
|
||||
case FilterType.messages:
|
||||
items = message_filter.getWidgets(
|
||||
messageProvider.messages,
|
||||
noteProvider.notes,
|
||||
eventProvider.events,
|
||||
);
|
||||
break;
|
||||
|
||||
// Absences
|
||||
case FilterType.absences:
|
||||
items = absence_filter.getWidgets(absenceProvider.absences,
|
||||
noExcused: absencesNoExcused);
|
||||
break;
|
||||
|
||||
// Homework
|
||||
case FilterType.homework:
|
||||
items = homework_filter.getWidgets(homeworkProvider.homework, context);
|
||||
break;
|
||||
|
||||
// Exams
|
||||
case FilterType.exams:
|
||||
items = exam_filter.getWidgets(examProvider.exams);
|
||||
break;
|
||||
|
||||
// Notes
|
||||
case FilterType.notes:
|
||||
items = note_filter.getWidgets(noteProvider.notes);
|
||||
break;
|
||||
|
||||
// Events
|
||||
case FilterType.events:
|
||||
items = event_filter.getWidgets(eventProvider.events);
|
||||
break;
|
||||
|
||||
// Changed Lessons
|
||||
case FilterType.lessons:
|
||||
items = lesson_filter
|
||||
.getWidgets(timetableProvider.getWeek(Week.current()) ?? []);
|
||||
break;
|
||||
|
||||
// Updates
|
||||
case FilterType.updates:
|
||||
if (updateProvider.available) {
|
||||
items = [update_filter.getWidget(updateProvider.releases.first)];
|
||||
}
|
||||
break;
|
||||
|
||||
// Missed Exams
|
||||
case FilterType.missedExams:
|
||||
items = missed_exam_filter
|
||||
.getWidgets(timetableProvider.getWeek(Week.current()) ?? []);
|
||||
break;
|
||||
|
||||
// Ads
|
||||
case FilterType.ads:
|
||||
if (adProvider.available) {
|
||||
items = ad_filter.getWidgets(adProvider.ads);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
Widget filterItemBuilder(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Widget item,
|
||||
int index, {
|
||||
int len = 0,
|
||||
bool isAfterSeparated = false,
|
||||
bool isBeforeSeparated = false,
|
||||
}) {
|
||||
if (item.key == const Key("\$premium")) {
|
||||
return Provider.of<PremiumProvider>(context, listen: false).hasPremium ||
|
||||
DateTime.now().weekday <= 5
|
||||
? const SizedBox()
|
||||
: const Padding(
|
||||
padding: EdgeInsets.only(bottom: 24.0),
|
||||
child: PremiumInline(features: [
|
||||
PremiumInlineFeature.nickname,
|
||||
PremiumInlineFeature.theme,
|
||||
PremiumInlineFeature.widget,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
final wrappedItem = SizeFadeTransition(
|
||||
curve: Curves.easeInOutCubic,
|
||||
animation: animation,
|
||||
child: item,
|
||||
);
|
||||
|
||||
bool separated = item is CertificationCard || item is NewGradesSurprise;
|
||||
|
||||
return item is Panel
|
||||
// Re-add & animate shadow
|
||||
? AnimatedBuilder(
|
||||
animation: animation,
|
||||
child: wrappedItem,
|
||||
builder: (context, child) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12.0),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: [
|
||||
if (Provider.of<SettingsProvider>(context, listen: false)
|
||||
.shadowEffect)
|
||||
BoxShadow(
|
||||
offset: const Offset(0, 21),
|
||||
blurRadius: 23.0,
|
||||
color: Theme.of(context).shadowColor.withOpacity(
|
||||
Theme.of(context).shadowColor.opacity *
|
||||
CurvedAnimation(
|
||||
parent: CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.easeInOutCubic),
|
||||
curve: const Interval(2 / 3, 1.0),
|
||||
).value,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
})
|
||||
: (len > 0
|
||||
? Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: index == 0
|
||||
? 0.0
|
||||
: (separated || isAfterSeparated ? 9.0 : 6.0)),
|
||||
child: Container(
|
||||
padding: item is NoteViewable
|
||||
? const EdgeInsets.symmetric(vertical: 8.0)
|
||||
: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: separated || isAfterSeparated
|
||||
? const Radius.circular(16.0)
|
||||
: (index == 0
|
||||
? const Radius.circular(16.0)
|
||||
: const Radius.circular(8.0)),
|
||||
bottom: separated || isBeforeSeparated
|
||||
? const Radius.circular(16.0)
|
||||
: (index + 1 == len
|
||||
? const Radius.circular(16.0)
|
||||
: const Radius.circular(8.0)),
|
||||
),
|
||||
),
|
||||
child: wrappedItem,
|
||||
),
|
||||
)
|
||||
: wrappedItem);
|
||||
}
|
||||
19
refilc/lib/ui/filter/widgets/absences.dart
Normal file
19
refilc/lib/ui/filter/widgets/absences.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
import 'package:refilc/ui/date_widget.dart';
|
||||
import 'package:refilc_kreta_api/models/absence.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/absence/absence_viewable.dart'
|
||||
as mobile;
|
||||
|
||||
List<DateWidget> getWidgets(List<Absence> providerAbsences,
|
||||
{bool noExcused = false}) {
|
||||
List<DateWidget> items = [];
|
||||
providerAbsences
|
||||
.where((a) => !noExcused || a.state != Justification.excused)
|
||||
.forEach((absence) {
|
||||
items.add(DateWidget(
|
||||
key: absence.id,
|
||||
date: absence.date,
|
||||
widget: mobile.AbsenceViewable(absence),
|
||||
));
|
||||
});
|
||||
return items;
|
||||
}
|
||||
24
refilc/lib/ui/filter/widgets/ads.dart
Normal file
24
refilc/lib/ui/filter/widgets/ads.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:refilc/models/ad.dart';
|
||||
import 'package:refilc/ui/date_widget.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/ad/ad_viewable.dart' as mobile;
|
||||
|
||||
List<DateWidget> getWidgets(List<Ad> providerAds) {
|
||||
List<DateWidget> items = [];
|
||||
|
||||
if (providerAds.isNotEmpty) {
|
||||
for (var ad in providerAds) {
|
||||
if (ad.date.isBefore(DateTime.now()) &&
|
||||
ad.expireDate.isAfter(DateTime.now())) {
|
||||
providerAds.sort((a, b) => -a.date.compareTo(b.date));
|
||||
|
||||
items.add(DateWidget(
|
||||
key: ad.description,
|
||||
date: ad.date,
|
||||
widget: mobile.AdViewable(ad),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
29
refilc/lib/ui/filter/widgets/certifications.dart
Normal file
29
refilc/lib/ui/filter/widgets/certifications.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
import 'package:refilc/ui/date_widget.dart';
|
||||
import 'package:refilc_kreta_api/models/grade.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/cretification/certification_card.dart'
|
||||
as mobile;
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
List<DateWidget> getWidgets(List<Grade> providerGrades) {
|
||||
List<DateWidget> items = [];
|
||||
for (var gradeType in GradeType.values) {
|
||||
if ([GradeType.midYear, GradeType.unknown, GradeType.levelExam]
|
||||
.contains(gradeType)) continue;
|
||||
|
||||
List<Grade> grades =
|
||||
providerGrades.where((grade) => grade.type == gradeType).toList();
|
||||
if (grades.isNotEmpty) {
|
||||
grades.sort((a, b) => -a.date.compareTo(b.date));
|
||||
|
||||
items.add(DateWidget(
|
||||
date: grades.first.date,
|
||||
key: 'certification${const Uuid().v4()}',
|
||||
widget: mobile.CertificationCard(
|
||||
grades,
|
||||
gradeType: gradeType,
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
16
refilc/lib/ui/filter/widgets/events.dart
Normal file
16
refilc/lib/ui/filter/widgets/events.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:refilc/ui/date_widget.dart';
|
||||
import 'package:refilc_kreta_api/models/event.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/event/event_viewable.dart'
|
||||
as mobile;
|
||||
|
||||
List<DateWidget> getWidgets(List<Event> providerEvents) {
|
||||
List<DateWidget> items = [];
|
||||
for (var event in providerEvents) {
|
||||
items.add(DateWidget(
|
||||
key: event.id,
|
||||
date: event.start,
|
||||
widget: mobile.EventViewable(event),
|
||||
));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
16
refilc/lib/ui/filter/widgets/exams.dart
Normal file
16
refilc/lib/ui/filter/widgets/exams.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:refilc/ui/date_widget.dart';
|
||||
import 'package:refilc_kreta_api/models/exam.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/exam/exam_viewable.dart'
|
||||
as mobile;
|
||||
|
||||
List<DateWidget> getWidgets(List<Exam> providerExams) {
|
||||
List<DateWidget> items = [];
|
||||
for (var exam in providerExams) {
|
||||
items.add(DateWidget(
|
||||
key: exam.id,
|
||||
date: exam.writeDate.year != 0 ? exam.writeDate : exam.date,
|
||||
widget: mobile.ExamViewable(exam),
|
||||
));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
53
refilc/lib/ui/filter/widgets/grades.dart
Normal file
53
refilc/lib/ui/filter/widgets/grades.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'package:refilc/ui/date_widget.dart';
|
||||
import 'package:refilc/utils/platform.dart';
|
||||
import 'package:refilc_kreta_api/models/grade.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/grade/grade_viewable.dart'
|
||||
as mobile;
|
||||
import 'package:refilc_mobile_ui/common/widgets/grade/new_grades.dart'
|
||||
as mobile;
|
||||
import 'package:refilc_desktop_ui/common/widgets/grade/grade_viewable.dart'
|
||||
as desktop;
|
||||
|
||||
List<DateWidget> getWidgets(
|
||||
List<Grade> providerGrades, DateTime? lastSeenDate) {
|
||||
List<DateWidget> items = [];
|
||||
for (var grade in providerGrades) {
|
||||
final surprise =
|
||||
(!(lastSeenDate != null && grade.date.isAfter(lastSeenDate)) ||
|
||||
grade.value.value == 0);
|
||||
if (grade.type == GradeType.midYear && surprise) {
|
||||
items.add(DateWidget(
|
||||
key: grade.id,
|
||||
date: grade.date,
|
||||
widget: PlatformUtils.isMobile
|
||||
? mobile.GradeViewable(grade)
|
||||
: desktop.GradeViewable(grade),
|
||||
));
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
List<DateWidget> getNewWidgets(
|
||||
List<Grade> providerGrades, DateTime? lastSeenDate) {
|
||||
List<DateWidget> items = [];
|
||||
List<Grade> newGrades = [];
|
||||
for (var grade in providerGrades) {
|
||||
final surprise =
|
||||
!(lastSeenDate != null && !grade.date.isAfter(lastSeenDate)) &&
|
||||
grade.value.value != 0 &&
|
||||
grade.value.weight != 0;
|
||||
if (grade.type == GradeType.midYear && surprise) {
|
||||
newGrades.add(grade);
|
||||
}
|
||||
}
|
||||
newGrades.sort((a, b) => a.date.compareTo(b.date));
|
||||
if (newGrades.isNotEmpty) {
|
||||
items.add(DateWidget(
|
||||
key: newGrades.last.id,
|
||||
date: newGrades.last.date,
|
||||
widget: mobile.NewGradesSurprise(newGrades),
|
||||
));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
21
refilc/lib/ui/filter/widgets/homework.dart
Normal file
21
refilc/lib/ui/filter/widgets/homework.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
import 'package:refilc/ui/date_widget.dart';
|
||||
import 'package:refilc_kreta_api/models/homework.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/homework/homework_viewable.dart'
|
||||
as mobile;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
List<DateWidget> getWidgets(
|
||||
List<Homework> providerHomework, BuildContext context) {
|
||||
List<DateWidget> items = [];
|
||||
|
||||
for (var homework in providerHomework) {
|
||||
items.add(DateWidget(
|
||||
key: homework.id,
|
||||
date: homework.deadline.year != 0 ? homework.deadline : homework.date,
|
||||
widget: mobile.HomeworkViewable(
|
||||
homework,
|
||||
),
|
||||
));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
19
refilc/lib/ui/filter/widgets/lessons.dart
Normal file
19
refilc/lib/ui/filter/widgets/lessons.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
import 'package:refilc/ui/date_widget.dart';
|
||||
import 'package:refilc_kreta_api/models/lesson.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/lesson/changed_lesson_viewable.dart'
|
||||
as mobile;
|
||||
|
||||
List<DateWidget> getWidgets(List<Lesson> providerLessons) {
|
||||
List<DateWidget> items = [];
|
||||
providerLessons
|
||||
.where((l) => l.isChanged && l.start.isAfter(DateTime.now()))
|
||||
.forEach((lesson) {
|
||||
items.add(DateWidget(
|
||||
key: lesson.id,
|
||||
date: DateTime(lesson.date.year, lesson.date.month, lesson.date.day,
|
||||
lesson.start.hour, lesson.start.minute),
|
||||
widget: mobile.ChangedLessonViewable(lesson),
|
||||
));
|
||||
});
|
||||
return items;
|
||||
}
|
||||
25
refilc/lib/ui/filter/widgets/messages.dart
Normal file
25
refilc/lib/ui/filter/widgets/messages.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'package:refilc/ui/date_widget.dart';
|
||||
import 'package:refilc/ui/filter/widgets/notes.dart' as note_filter;
|
||||
import 'package:refilc/ui/filter/widgets/events.dart' as event_filter;
|
||||
import 'package:refilc_kreta_api/models/event.dart';
|
||||
import 'package:refilc_kreta_api/models/message.dart';
|
||||
import 'package:refilc_kreta_api/models/note.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/message/message_viewable.dart'
|
||||
as mobile;
|
||||
|
||||
List<DateWidget> getWidgets(List<Message> providerMessages,
|
||||
List<Note> providerNotes, List<Event> providerEvents) {
|
||||
List<DateWidget> items = [];
|
||||
for (var message in providerMessages) {
|
||||
if (message.type == MessageType.inbox) {
|
||||
items.add(DateWidget(
|
||||
key: "${message.id}",
|
||||
date: message.date,
|
||||
widget: mobile.MessageViewable(message),
|
||||
));
|
||||
}
|
||||
}
|
||||
items.addAll(note_filter.getWidgets(providerNotes));
|
||||
items.addAll(event_filter.getWidgets(providerEvents));
|
||||
return items;
|
||||
}
|
||||
35
refilc/lib/ui/filter/widgets/missed_exams.dart
Normal file
35
refilc/lib/ui/filter/widgets/missed_exams.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
import 'package:refilc/utils/format.dart';
|
||||
import 'package:refilc/ui/date_widget.dart';
|
||||
import 'package:refilc_kreta_api/models/lesson.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/missed_exam/missed_exam_viewable.dart';
|
||||
|
||||
List<DateWidget> getWidgets(List<Lesson> providerLessons) {
|
||||
List<DateWidget> items = [];
|
||||
List<Lesson> missedExams = [];
|
||||
|
||||
for (var lesson in providerLessons) {
|
||||
final desc = lesson.description.toLowerCase().specialChars();
|
||||
// Check if lesson description includes hints for an exam written during the lesson
|
||||
if (!lesson.studentPresence &&
|
||||
(lesson.exam != "" ||
|
||||
desc.contains("dolgozat") ||
|
||||
desc.contains("feleles") ||
|
||||
desc.contains("temazaro") ||
|
||||
desc.contains("szamonkeres") ||
|
||||
desc == "tz") &&
|
||||
!(desc.contains("felkeszules") || desc.contains("gyakorlas"))) {
|
||||
missedExams.add(lesson);
|
||||
}
|
||||
}
|
||||
|
||||
if (missedExams.isNotEmpty) {
|
||||
missedExams.sort((a, b) => -a.date.compareTo(b.date));
|
||||
|
||||
items.add(DateWidget(
|
||||
date: missedExams.first.date,
|
||||
widget: MissedExamViewable(missedExams),
|
||||
));
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
16
refilc/lib/ui/filter/widgets/notes.dart
Normal file
16
refilc/lib/ui/filter/widgets/notes.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:refilc/ui/date_widget.dart';
|
||||
import 'package:refilc_kreta_api/models/note.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/note/note_viewable.dart'
|
||||
as mobile;
|
||||
|
||||
List<DateWidget> getWidgets(List<Note> providerNotes) {
|
||||
List<DateWidget> items = [];
|
||||
for (var note in providerNotes) {
|
||||
items.add(DateWidget(
|
||||
key: note.id,
|
||||
date: note.date,
|
||||
widget: mobile.NoteViewable(note),
|
||||
));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
11
refilc/lib/ui/filter/widgets/update.dart
Normal file
11
refilc/lib/ui/filter/widgets/update.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
import 'package:refilc/models/release.dart';
|
||||
import 'package:refilc/ui/date_widget.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/update/update_viewable.dart'
|
||||
as mobile;
|
||||
|
||||
DateWidget getWidget(Release providerRelease) {
|
||||
return DateWidget(
|
||||
date: DateTime.now(),
|
||||
widget: mobile.UpdateViewable(providerRelease),
|
||||
);
|
||||
}
|
||||
151
refilc/lib/ui/flutter_colorpicker/block_picker.dart
Normal file
151
refilc/lib/ui/flutter_colorpicker/block_picker.dart
Normal file
@@ -0,0 +1,151 @@
|
||||
// FROM: https://pub.dev/packages/flutter_colorpicker
|
||||
// FROM: https://pub.dev/packages/flutter_colorpicker
|
||||
// FROM: https://pub.dev/packages/flutter_colorpicker
|
||||
// FROM: https://pub.dev/packages/flutter_colorpicker
|
||||
|
||||
/// Blocky Color Picker
|
||||
|
||||
library block_colorpicker;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:refilc/theme/colors/accent.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
/// Child widget for layout builder.
|
||||
typedef PickerItem = Widget Function(Color color);
|
||||
|
||||
/// Customize the layout.
|
||||
typedef PickerLayoutBuilder = Widget Function(
|
||||
BuildContext context, List<Color> colors, PickerItem child);
|
||||
|
||||
/// Customize the item shape.
|
||||
typedef PickerItemBuilder = Widget Function(
|
||||
Color color, bool isCurrentColor, void Function() changeColor);
|
||||
|
||||
// Provide a list of colors for block color picker.
|
||||
// const List<Color> _defaultColors = [
|
||||
// Colors.red,
|
||||
// Colors.pink,
|
||||
// Colors.purple,
|
||||
// Colors.deepPurple,
|
||||
// Colors.indigo,
|
||||
// Colors.blue,
|
||||
// Colors.lightBlue,
|
||||
// Colors.cyan,
|
||||
// Colors.teal,
|
||||
// Colors.green,
|
||||
// Colors.lightGreen,
|
||||
// Colors.lime,
|
||||
// Colors.yellow,
|
||||
// Colors.amber,
|
||||
// Colors.orange,
|
||||
// Colors.deepOrange,
|
||||
// Colors.brown,
|
||||
// Colors.grey,
|
||||
// Colors.blueGrey,
|
||||
// Colors.black,
|
||||
// ];
|
||||
|
||||
// Provide a layout for [BlockPicker].
|
||||
Widget _defaultLayoutBuilder(
|
||||
BuildContext context, List<Color> colors, PickerItem child) {
|
||||
Orientation orientation = MediaQuery.of(context).orientation;
|
||||
|
||||
return SizedBox(
|
||||
width: 300,
|
||||
height: orientation == Orientation.portrait ? 360 : 200,
|
||||
child: GridView.count(
|
||||
crossAxisCount: orientation == Orientation.portrait ? 4 : 6,
|
||||
crossAxisSpacing: 5,
|
||||
mainAxisSpacing: 5,
|
||||
children: [for (Color color in colors) child(color)],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Provide a shape for [BlockPicker].
|
||||
Widget _defaultItemBuilder(
|
||||
Color color, bool isCurrentColor, void Function() changeColor) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(7),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: color,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: color.withOpacity(0.8),
|
||||
offset: const Offset(1, 2),
|
||||
blurRadius: 5)
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: changeColor,
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 210),
|
||||
opacity: isCurrentColor ? 1 : 0,
|
||||
child: Icon(Icons.done,
|
||||
color: useWhiteForeground(color) ? Colors.white : Colors.black),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// The blocky color picker you can alter the layout and shape.
|
||||
class BlockPicker extends StatefulWidget {
|
||||
BlockPicker({
|
||||
super.key,
|
||||
required this.pickerColor,
|
||||
required this.onColorChanged,
|
||||
this.useInShowDialog = true,
|
||||
this.layoutBuilder = _defaultLayoutBuilder,
|
||||
this.itemBuilder = _defaultItemBuilder,
|
||||
});
|
||||
|
||||
final Color? pickerColor;
|
||||
final ValueChanged<Color> onColorChanged;
|
||||
final List<Color> availableColors = accentColorMap.values.toList();
|
||||
final bool useInShowDialog;
|
||||
final PickerLayoutBuilder layoutBuilder;
|
||||
final PickerItemBuilder itemBuilder;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _BlockPickerState();
|
||||
}
|
||||
|
||||
class _BlockPickerState extends State<BlockPicker> {
|
||||
Color? _currentColor;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_currentColor = widget.pickerColor;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void changeColor(Color color) {
|
||||
setState(() => _currentColor = color);
|
||||
widget.onColorChanged(color);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.layoutBuilder(
|
||||
context,
|
||||
widget.availableColors,
|
||||
(Color color) => widget.itemBuilder(
|
||||
color,
|
||||
(_currentColor != null &&
|
||||
(widget.useInShowDialog ? true : widget.pickerColor != null))
|
||||
? (_currentColor?.value == color.value) &&
|
||||
(widget.useInShowDialog
|
||||
? true
|
||||
: widget.pickerColor?.value == color.value)
|
||||
: false,
|
||||
() => changeColor(color),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
470
refilc/lib/ui/flutter_colorpicker/colorpicker.dart
Normal file
470
refilc/lib/ui/flutter_colorpicker/colorpicker.dart
Normal file
@@ -0,0 +1,470 @@
|
||||
// FROM: https://pub.dev/packages/flutter_colorpicker
|
||||
// FROM: https://pub.dev/packages/flutter_colorpicker
|
||||
// FROM: https://pub.dev/packages/flutter_colorpicker
|
||||
// FROM: https://pub.dev/packages/flutter_colorpicker
|
||||
|
||||
/// HSV(HSB)/HSL Color Picker example
|
||||
///
|
||||
/// You can create your own layout by importing `picker.dart`.
|
||||
|
||||
// ignore_for_file: use_build_context_synchronously
|
||||
|
||||
library hsv_picker;
|
||||
|
||||
import 'package:refilc/models/shared_theme.dart';
|
||||
import 'package:refilc_mobile_ui/common/custom_snack_bar.dart';
|
||||
import 'package:refilc_kreta_api/providers/share_provider.dart';
|
||||
import 'package:refilc/ui/flutter_colorpicker/block_picker.dart';
|
||||
import 'package:refilc/ui/flutter_colorpicker/palette.dart';
|
||||
import 'package:refilc/ui/flutter_colorpicker/utils.dart';
|
||||
import 'package:refilc_mobile_ui/screens/settings/theme_screen.dart';
|
||||
import 'package:refilc_mobile_ui/screens/settings/theme_screen.i18n.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class FilcColorPicker extends StatefulWidget {
|
||||
const FilcColorPicker({
|
||||
super.key,
|
||||
required this.colorMode,
|
||||
required this.pickerColor,
|
||||
required this.onColorChanged,
|
||||
required this.onColorChangeEnd,
|
||||
this.pickerHsvColor,
|
||||
this.onHsvColorChanged,
|
||||
this.paletteType = PaletteType.hsvWithHue,
|
||||
this.enableAlpha = true,
|
||||
@Deprecated('Use empty list in [labelTypes] to disable label.')
|
||||
this.showLabel = true,
|
||||
this.labelTypes = const [
|
||||
ColorLabelType.rgb,
|
||||
ColorLabelType.hsv,
|
||||
ColorLabelType.hsl
|
||||
],
|
||||
@Deprecated(
|
||||
'Use Theme.of(context).textTheme.bodyText1 & 2 to alter text style.')
|
||||
this.labelTextStyle,
|
||||
this.displayThumbColor = false,
|
||||
this.portraitOnly = false,
|
||||
this.colorPickerWidth = 300.0,
|
||||
this.pickerAreaHeightPercent = 1.0,
|
||||
this.pickerAreaBorderRadius = const BorderRadius.all(Radius.zero),
|
||||
this.hexInputBar = false,
|
||||
this.hexInputController,
|
||||
this.colorHistory,
|
||||
this.onHistoryChanged,
|
||||
required this.onThemeIdProvided,
|
||||
});
|
||||
|
||||
final CustomColorMode colorMode;
|
||||
final Color pickerColor;
|
||||
final ValueChanged<Color> onColorChanged;
|
||||
final void Function(Color color, {bool? adaptive}) onColorChangeEnd;
|
||||
final HSVColor? pickerHsvColor;
|
||||
final ValueChanged<HSVColor>? onHsvColorChanged;
|
||||
final PaletteType paletteType;
|
||||
final bool enableAlpha;
|
||||
final bool showLabel;
|
||||
final List<ColorLabelType> labelTypes;
|
||||
final TextStyle? labelTextStyle;
|
||||
final bool displayThumbColor;
|
||||
final bool portraitOnly;
|
||||
final double colorPickerWidth;
|
||||
final double pickerAreaHeightPercent;
|
||||
final BorderRadius pickerAreaBorderRadius;
|
||||
final bool hexInputBar;
|
||||
final TextEditingController? hexInputController;
|
||||
final List<Color>? colorHistory;
|
||||
final ValueChanged<List<Color>>? onHistoryChanged;
|
||||
final void Function(SharedTheme theme) onThemeIdProvided;
|
||||
|
||||
@override
|
||||
FilcColorPickerState createState() => FilcColorPickerState();
|
||||
}
|
||||
|
||||
class FilcColorPickerState extends State<FilcColorPicker> {
|
||||
final idController = TextEditingController();
|
||||
|
||||
late final ShareProvider shareProvider;
|
||||
|
||||
HSVColor currentHsvColor = const HSVColor.fromAHSV(0.0, 0.0, 0.0, 0.0);
|
||||
List<Color> colorHistory = [];
|
||||
bool isAdvancedView = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
currentHsvColor = (widget.pickerHsvColor != null)
|
||||
? widget.pickerHsvColor as HSVColor
|
||||
: HSVColor.fromColor(widget.pickerColor);
|
||||
// If there's no initial text in `hexInputController`,
|
||||
if (widget.hexInputController?.text.isEmpty == true) {
|
||||
// set it to the current's color HEX value.
|
||||
widget.hexInputController?.text = colorToHex(
|
||||
currentHsvColor.toColor(),
|
||||
enableAlpha: widget.enableAlpha,
|
||||
);
|
||||
}
|
||||
// Listen to the text input, If there is an `hexInputController` provided.
|
||||
widget.hexInputController?.addListener(colorPickerTextInputListener);
|
||||
if (widget.colorHistory != null && widget.onHistoryChanged != null) {
|
||||
colorHistory = widget.colorHistory ?? [];
|
||||
}
|
||||
shareProvider = Provider.of<ShareProvider>(context, listen: false);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(FilcColorPicker oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
currentHsvColor = (widget.pickerHsvColor != null)
|
||||
? widget.pickerHsvColor as HSVColor
|
||||
: HSVColor.fromColor(widget.pickerColor);
|
||||
}
|
||||
|
||||
void colorPickerTextInputListener() {
|
||||
// It can't be null really, since it's only listening if the controller
|
||||
// is provided, but it may help to calm the Dart analyzer in the future.
|
||||
if (widget.hexInputController == null) return;
|
||||
// If a user is inserting/typing any text — try to get the color value from it,
|
||||
// and interpret its transparency, dependent on the widget's settings.
|
||||
final Color? color = colorFromHex(widget.hexInputController!.text,
|
||||
enableAlpha: widget.enableAlpha);
|
||||
// If it's the valid color:
|
||||
if (color != null) {
|
||||
// set it as the current color and
|
||||
setState(() => currentHsvColor = HSVColor.fromColor(color));
|
||||
// notify with a callback.
|
||||
widget.onColorChanged(color);
|
||||
if (widget.onHsvColorChanged != null) {
|
||||
widget.onHsvColorChanged!(currentHsvColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.hexInputController?.removeListener(colorPickerTextInputListener);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget colorPickerSlider(TrackType trackType) {
|
||||
return ColorPickerSlider(
|
||||
trackType,
|
||||
currentHsvColor,
|
||||
(HSVColor color) {
|
||||
// Update text in `hexInputController` if provided.
|
||||
widget.hexInputController?.text =
|
||||
colorToHex(color.toColor(), enableAlpha: widget.enableAlpha);
|
||||
setState(() => currentHsvColor = color);
|
||||
widget.onColorChanged(currentHsvColor.toColor());
|
||||
if (widget.onHsvColorChanged != null) {
|
||||
widget.onHsvColorChanged!(currentHsvColor);
|
||||
}
|
||||
},
|
||||
() => widget.onColorChangeEnd(currentHsvColor.toColor()),
|
||||
(p) {
|
||||
ScaffoldMessenger.of(context).clearSnackBars();
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(
|
||||
"Move the ${p == 0 ? 'Saturation (second)' : 'Value (third)'} slider first.",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: AppColors.of(context).text,
|
||||
fontWeight: FontWeight.w600)),
|
||||
backgroundColor: AppColors.of(context).background));
|
||||
},
|
||||
displayThumbColor: widget.displayThumbColor,
|
||||
);
|
||||
}
|
||||
|
||||
void onColorChanging(HSVColor color) {
|
||||
// Update text in `hexInputController` if provided.
|
||||
widget.hexInputController?.text =
|
||||
colorToHex(color.toColor(), enableAlpha: widget.enableAlpha);
|
||||
setState(() => currentHsvColor = color);
|
||||
widget.onColorChanged(currentHsvColor.toColor());
|
||||
if (widget.onHsvColorChanged != null) {
|
||||
widget.onHsvColorChanged!(currentHsvColor);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (MediaQuery.of(context).orientation == Orientation.portrait ||
|
||||
widget.portraitOnly) {
|
||||
return Column(
|
||||
children: [
|
||||
if (widget.colorMode != CustomColorMode.theme &&
|
||||
widget.colorMode != CustomColorMode.enterId)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 12.0, right: 12.0),
|
||||
child: SizedBox(
|
||||
height: 45.0,
|
||||
width: double.infinity,
|
||||
child: colorPickerSlider(TrackType.hue),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 12.0, right: 12.0),
|
||||
child: SizedBox(
|
||||
height: 45.0,
|
||||
width: double.infinity,
|
||||
child: colorPickerSlider(TrackType.saturation),
|
||||
),
|
||||
),
|
||||
if (isAdvancedView)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 12.0, right: 12.0),
|
||||
child: SizedBox(
|
||||
height: 45.0,
|
||||
width: double.infinity,
|
||||
child: colorPickerSlider(TrackType.value),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isAdvancedView &&
|
||||
widget.colorMode != CustomColorMode.theme &&
|
||||
widget.colorMode != CustomColorMode.enterId)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 6.0),
|
||||
child: ColorPickerInput(
|
||||
currentHsvColor.toColor(),
|
||||
(Color color) {
|
||||
setState(() => currentHsvColor = HSVColor.fromColor(color));
|
||||
widget.onColorChanged(currentHsvColor.toColor());
|
||||
if (widget.onHsvColorChanged != null) {
|
||||
widget.onHsvColorChanged!(currentHsvColor);
|
||||
}
|
||||
},
|
||||
enableAlpha: false,
|
||||
embeddedText: false,
|
||||
),
|
||||
),
|
||||
if (widget.colorMode == CustomColorMode.enterId)
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 10.0, horizontal: 10.0),
|
||||
child: Column(
|
||||
children: [
|
||||
TextField(
|
||||
autocorrect: false,
|
||||
autofocus: true,
|
||||
onEditingComplete: () async {
|
||||
SharedTheme? theme = await shareProvider.getThemeById(
|
||||
context,
|
||||
id: idController.text.replaceAll(' ', ''),
|
||||
);
|
||||
|
||||
if (theme != null) {
|
||||
widget.onThemeIdProvided(theme);
|
||||
idController.clear();
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
CustomSnackBar(
|
||||
content: Text("theme_not_found".i18n,
|
||||
style: const TextStyle(color: Colors.white)),
|
||||
backgroundColor: AppColors.of(context).red,
|
||||
context: context,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
controller: idController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'theme_id'.i18n,
|
||||
),
|
||||
),
|
||||
// MaterialActionButton(
|
||||
// child: Row(
|
||||
// mainAxisSize: MainAxisSize.min,
|
||||
// children: [
|
||||
// Text('check_id'.i18n),
|
||||
// ],
|
||||
// ),
|
||||
// backgroundColor: AppColors.of(context).filc,
|
||||
// onPressed: () {},
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.colorMode != CustomColorMode.enterId)
|
||||
SizedBox(
|
||||
height: 70 * (widget.colorMode == CustomColorMode.theme ? 2 : 1),
|
||||
child: BlockPicker(
|
||||
pickerColor: Colors.red,
|
||||
layoutBuilder: (context, colors, child) {
|
||||
return GridView.count(
|
||||
shrinkWrap: true,
|
||||
crossAxisCount:
|
||||
widget.colorMode == CustomColorMode.theme ? 2 : 1,
|
||||
scrollDirection: Axis.horizontal,
|
||||
crossAxisSpacing: 15,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
mainAxisSpacing: 15,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0, vertical: 8.0),
|
||||
children: List.generate(
|
||||
colors.toSet().length +
|
||||
(widget.colorMode == CustomColorMode.theme ? 1 : 0),
|
||||
(index) {
|
||||
if (widget.colorMode == CustomColorMode.theme) {
|
||||
if (index == 0) {
|
||||
return GestureDetector(
|
||||
onTap: () => widget.onColorChangeEnd(
|
||||
Colors.transparent,
|
||||
adaptive: true),
|
||||
child: ColorIndicator(
|
||||
HSVColor.fromColor(
|
||||
const Color.fromARGB(255, 255, 238, 177)),
|
||||
icon: CupertinoIcons.wand_stars,
|
||||
currentHsvColor: currentHsvColor,
|
||||
width: 30,
|
||||
height: 30,
|
||||
adaptive: true),
|
||||
);
|
||||
}
|
||||
index--;
|
||||
}
|
||||
return GestureDetector(
|
||||
onTap: () => widget.onColorChangeEnd(colors[index]),
|
||||
child: ColorIndicator(HSVColor.fromColor(colors[index]),
|
||||
currentHsvColor: currentHsvColor,
|
||||
width: 30,
|
||||
height: 30),
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
onColorChanged: (c) => {},
|
||||
),
|
||||
),
|
||||
if (widget.colorMode != CustomColorMode.theme &&
|
||||
widget.colorMode != CustomColorMode.enterId)
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
customBorder: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
onTap: () => setState(() {
|
||||
isAdvancedView = !isAdvancedView;
|
||||
}),
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(left: 8.0, right: 8.0, top: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Switch(
|
||||
onChanged: (v) => setState(() => isAdvancedView = v),
|
||||
value: isAdvancedView,
|
||||
),
|
||||
const SizedBox(width: 12.0),
|
||||
Text(
|
||||
"advanced".i18n,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16.0,
|
||||
color: AppColors.of(context)
|
||||
.text
|
||||
.withOpacity(isAdvancedView ? 1.0 : .5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Row(
|
||||
children: [
|
||||
//SizedBox(width: widget.colorPickerWidth, height: widget.colorPickerWidth * widget.pickerAreaHeightPercent, child: colorPicker()),
|
||||
Column(
|
||||
children: [
|
||||
Row(
|
||||
children: <Widget>[
|
||||
const SizedBox(width: 20.0),
|
||||
GestureDetector(
|
||||
onTap: () => setState(() {
|
||||
if (widget.onHistoryChanged != null &&
|
||||
!colorHistory.contains(currentHsvColor.toColor())) {
|
||||
colorHistory.add(currentHsvColor.toColor());
|
||||
widget.onHistoryChanged!(colorHistory);
|
||||
}
|
||||
}),
|
||||
child: ColorIndicator(currentHsvColor),
|
||||
),
|
||||
Column(
|
||||
children: <Widget>[
|
||||
//SizedBox(height: 40.0, width: 260.0, child: sliderByPaletteType()),
|
||||
if (widget.enableAlpha)
|
||||
SizedBox(
|
||||
height: 40.0,
|
||||
width: 260.0,
|
||||
child: colorPickerSlider(TrackType.alpha)),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 10.0),
|
||||
],
|
||||
),
|
||||
if (colorHistory.isNotEmpty)
|
||||
SizedBox(
|
||||
width: widget.colorPickerWidth,
|
||||
height: 50,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: <Widget>[
|
||||
for (Color color in colorHistory)
|
||||
Padding(
|
||||
key: Key(color.hashCode.toString()),
|
||||
padding: const EdgeInsets.fromLTRB(15, 18, 0, 0),
|
||||
child: Center(
|
||||
child: GestureDetector(
|
||||
onTap: () =>
|
||||
onColorChanging(HSVColor.fromColor(color)),
|
||||
onLongPress: () {
|
||||
if (colorHistory.remove(color)) {
|
||||
widget.onHistoryChanged!(colorHistory);
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
child: ColorIndicator(HSVColor.fromColor(color),
|
||||
width: 30, height: 30),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
]),
|
||||
),
|
||||
const SizedBox(height: 20.0),
|
||||
if (widget.hexInputBar)
|
||||
ColorPickerInput(
|
||||
currentHsvColor.toColor(),
|
||||
(Color color) {
|
||||
setState(() => currentHsvColor = HSVColor.fromColor(color));
|
||||
widget.onColorChanged(currentHsvColor.toColor());
|
||||
if (widget.onHsvColorChanged != null) {
|
||||
widget.onHsvColorChanged!(currentHsvColor);
|
||||
}
|
||||
},
|
||||
enableAlpha: widget.enableAlpha,
|
||||
embeddedText: false,
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
174
refilc/lib/ui/flutter_colorpicker/colors.dart
Normal file
174
refilc/lib/ui/flutter_colorpicker/colors.dart
Normal file
@@ -0,0 +1,174 @@
|
||||
// FROM: https://pub.dev/packages/flutter_colorpicker
|
||||
// FROM: https://pub.dev/packages/flutter_colorpicker
|
||||
// FROM: https://pub.dev/packages/flutter_colorpicker
|
||||
// FROM: https://pub.dev/packages/flutter_colorpicker
|
||||
|
||||
import 'dart:ui';
|
||||
|
||||
/// X11 Colors
|
||||
///
|
||||
/// https://en.wikipedia.org/wiki/X11_color_names
|
||||
|
||||
const Map<String, Color> x11Colors = {
|
||||
'aliceblue': Color(0xfff0f8ff),
|
||||
'antiquewhite': Color(0xfffaebd7),
|
||||
'aqua': Color(0xff00ffff),
|
||||
'aquamarine': Color(0xff7fffd4),
|
||||
'azure': Color(0xfff0ffff),
|
||||
'beige': Color(0xfff5f5dc),
|
||||
'bisque': Color(0xffffe4c4),
|
||||
'black': Color(0xff000000),
|
||||
'blanchedalmond': Color(0xffffebcd),
|
||||
'blue': Color(0xff0000ff),
|
||||
'blueviolet': Color(0xff8a2be2),
|
||||
'brown': Color(0xffa52a2a),
|
||||
'burlywood': Color(0xffdeb887),
|
||||
'cadetblue': Color(0xff5f9ea0),
|
||||
'chartreuse': Color(0xff7fff00),
|
||||
'chocolate': Color(0xffd2691e),
|
||||
'coral': Color(0xffff7f50),
|
||||
'cornflower': Color(0xff6495ed),
|
||||
'cornflowerblue': Color(0xff6495ed),
|
||||
'cornsilk': Color(0xfffff8dc),
|
||||
'crimson': Color(0xffdc143c),
|
||||
'cyan': Color(0xff00ffff),
|
||||
'darkblue': Color(0xff00008b),
|
||||
'darkcyan': Color(0xff008b8b),
|
||||
'darkgoldenrod': Color(0xffb8860b),
|
||||
'darkgray': Color(0xffa9a9a9),
|
||||
'darkgreen': Color(0xff006400),
|
||||
'darkgrey': Color(0xffa9a9a9),
|
||||
'darkkhaki': Color(0xffbdb76b),
|
||||
'darkmagenta': Color(0xff8b008b),
|
||||
'darkolivegreen': Color(0xff556b2f),
|
||||
'darkorange': Color(0xffff8c00),
|
||||
'darkorchid': Color(0xff9932cc),
|
||||
'darkred': Color(0xff8b0000),
|
||||
'darksalmon': Color(0xffe9967a),
|
||||
'darkseagreen': Color(0xff8fbc8f),
|
||||
'darkslateblue': Color(0xff483d8b),
|
||||
'darkslategray': Color(0xff2f4f4f),
|
||||
'darkslategrey': Color(0xff2f4f4f),
|
||||
'darkturquoise': Color(0xff00ced1),
|
||||
'darkviolet': Color(0xff9400d3),
|
||||
'deeppink': Color(0xffff1493),
|
||||
'deepskyblue': Color(0xff00bfff),
|
||||
'dimgray': Color(0xff696969),
|
||||
'dimgrey': Color(0xff696969),
|
||||
'dodgerblue': Color(0xff1e90ff),
|
||||
'firebrick': Color(0xffb22222),
|
||||
'floralwhite': Color(0xfffffaf0),
|
||||
'forestgreen': Color(0xff228b22),
|
||||
'fuchsia': Color(0xffff00ff),
|
||||
'gainsboro': Color(0xffdcdcdc),
|
||||
'ghostwhite': Color(0xfff8f8ff),
|
||||
'gold': Color(0xffffd700),
|
||||
'goldenrod': Color(0xffdaa520),
|
||||
'gray': Color(0xff808080),
|
||||
'green': Color(0xff008000),
|
||||
'greenyellow': Color(0xffadff2f),
|
||||
'grey': Color(0xff808080),
|
||||
'honeydew': Color(0xfff0fff0),
|
||||
'hotpink': Color(0xffff69b4),
|
||||
'indianred': Color(0xffcd5c5c),
|
||||
'indigo': Color(0xff4b0082),
|
||||
'ivory': Color(0xfffffff0),
|
||||
'khaki': Color(0xfff0e68c),
|
||||
'laserlemon': Color(0xffffff54),
|
||||
'lavender': Color(0xffe6e6fa),
|
||||
'lavenderblush': Color(0xfffff0f5),
|
||||
'lawngreen': Color(0xff7cfc00),
|
||||
'lemonchiffon': Color(0xfffffacd),
|
||||
'lightblue': Color(0xffadd8e6),
|
||||
'lightcoral': Color(0xfff08080),
|
||||
'lightcyan': Color(0xffe0ffff),
|
||||
'lightgoldenrod': Color(0xfffafad2),
|
||||
'lightgoldenrodyellow': Color(0xfffafad2),
|
||||
'lightgray': Color(0xffd3d3d3),
|
||||
'lightgreen': Color(0xff90ee90),
|
||||
'lightgrey': Color(0xffd3d3d3),
|
||||
'lightpink': Color(0xffffb6c1),
|
||||
'lightsalmon': Color(0xffffa07a),
|
||||
'lightseagreen': Color(0xff20b2aa),
|
||||
'lightskyblue': Color(0xff87cefa),
|
||||
'lightslategray': Color(0xff778899),
|
||||
'lightslategrey': Color(0xff778899),
|
||||
'lightsteelblue': Color(0xffb0c4de),
|
||||
'lightyellow': Color(0xffffffe0),
|
||||
'lime': Color(0xff00ff00),
|
||||
'limegreen': Color(0xff32cd32),
|
||||
'linen': Color(0xfffaf0e6),
|
||||
'magenta': Color(0xffff00ff),
|
||||
'maroon': Color(0xff800000),
|
||||
'maroon2': Color(0xff7f0000),
|
||||
'maroon3': Color(0xffb03060),
|
||||
'mediumaquamarine': Color(0xff66cdaa),
|
||||
'mediumblue': Color(0xff0000cd),
|
||||
'mediumorchid': Color(0xffba55d3),
|
||||
'mediumpurple': Color(0xff9370db),
|
||||
'mediumseagreen': Color(0xff3cb371),
|
||||
'mediumslateblue': Color(0xff7b68ee),
|
||||
'mediumspringgreen': Color(0xff00fa9a),
|
||||
'mediumturquoise': Color(0xff48d1cc),
|
||||
'mediumvioletred': Color(0xffc71585),
|
||||
'midnightblue': Color(0xff191970),
|
||||
'mintcream': Color(0xfff5fffa),
|
||||
'mistyrose': Color(0xffffe4e1),
|
||||
'moccasin': Color(0xffffe4b5),
|
||||
'navajowhite': Color(0xffffdead),
|
||||
'navy': Color(0xff000080),
|
||||
'oldlace': Color(0xfffdf5e6),
|
||||
'olive': Color(0xff808000),
|
||||
'olivedrab': Color(0xff6b8e23),
|
||||
'orange': Color(0xffffa500),
|
||||
'orangered': Color(0xffff4500),
|
||||
'orchid': Color(0xffda70d6),
|
||||
'palegoldenrod': Color(0xffeee8aa),
|
||||
'palegreen': Color(0xff98fb98),
|
||||
'paleturquoise': Color(0xffafeeee),
|
||||
'palevioletred': Color(0xffdb7093),
|
||||
'papayawhip': Color(0xffffefd5),
|
||||
'peachpuff': Color(0xffffdab9),
|
||||
'peru': Color(0xffcd853f),
|
||||
'pink': Color(0xffffc0cb),
|
||||
'plum': Color(0xffdda0dd),
|
||||
'powderblue': Color(0xffb0e0e6),
|
||||
'purple': Color(0xff800080),
|
||||
'purple2': Color(0xff7f007f),
|
||||
'purple3': Color(0xffa020f0),
|
||||
'rebeccapurple': Color(0xff663399),
|
||||
'red': Color(0xffff0000),
|
||||
'rosybrown': Color(0xffbc8f8f),
|
||||
'royalblue': Color(0xff4169e1),
|
||||
'saddlebrown': Color(0xff8b4513),
|
||||
'salmon': Color(0xfffa8072),
|
||||
'sandybrown': Color(0xfff4a460),
|
||||
'seagreen': Color(0xff2e8b57),
|
||||
'seashell': Color(0xfffff5ee),
|
||||
'sienna': Color(0xffa0522d),
|
||||
'silver': Color(0xffc0c0c0),
|
||||
'skyblue': Color(0xff87ceeb),
|
||||
'slateblue': Color(0xff6a5acd),
|
||||
'slategray': Color(0xff708090),
|
||||
'slategrey': Color(0xff708090),
|
||||
'snow': Color(0xfffffafa),
|
||||
'springgreen': Color(0xff00ff7f),
|
||||
'steelblue': Color(0xff4682b4),
|
||||
'tan': Color(0xffd2b48c),
|
||||
'teal': Color(0xff008080),
|
||||
'thistle': Color(0xffd8bfd8),
|
||||
'tomato': Color(0xffff6347),
|
||||
'turquoise': Color(0xff40e0d0),
|
||||
'violet': Color(0xffee82ee),
|
||||
'wheat': Color(0xfff5deb3),
|
||||
'white': Color(0xffffffff),
|
||||
'whitesmoke': Color(0xfff5f5f5),
|
||||
'yellow': Color(0xffffff00),
|
||||
'yellowgreen': Color(0xff9acd32),
|
||||
};
|
||||
|
||||
Color? colorFromName(String val) => x11Colors[val.trim().replaceAll(' ', '').toLowerCase()];
|
||||
|
||||
extension ColorExtension on String {
|
||||
Color? toColor() => colorFromName(this);
|
||||
}
|
||||
821
refilc/lib/ui/flutter_colorpicker/palette.dart
Normal file
821
refilc/lib/ui/flutter_colorpicker/palette.dart
Normal file
@@ -0,0 +1,821 @@
|
||||
// FROM: https://pub.dev/packages/flutter_colorpicker
|
||||
// FROM: https://pub.dev/packages/flutter_colorpicker
|
||||
// FROM: https://pub.dev/packages/flutter_colorpicker
|
||||
// FROM: https://pub.dev/packages/flutter_colorpicker
|
||||
|
||||
// ignore: dangling_library_doc_comments
|
||||
/// The components of HSV Color Picker
|
||||
///
|
||||
/// Try to create a Color Picker with other layout on your own :)
|
||||
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/theme/colors/accent.dart';
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
/// Palette types for color picker area widget.
|
||||
enum PaletteType {
|
||||
hsv,
|
||||
hsvWithHue,
|
||||
hsvWithValue,
|
||||
hsvWithSaturation,
|
||||
hsl,
|
||||
hslWithHue,
|
||||
hslWithLightness,
|
||||
hslWithSaturation,
|
||||
rgbWithBlue,
|
||||
rgbWithGreen,
|
||||
rgbWithRed,
|
||||
hueWheel,
|
||||
}
|
||||
|
||||
/// Track types for slider picker.
|
||||
enum TrackType {
|
||||
hue,
|
||||
saturation,
|
||||
saturationForHSL,
|
||||
value,
|
||||
lightness,
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
alpha,
|
||||
}
|
||||
|
||||
enum FilcTrackType {
|
||||
hue,
|
||||
saturation,
|
||||
value,
|
||||
}
|
||||
|
||||
/// Color information label type.
|
||||
enum ColorLabelType { hex, rgb, hsv, hsl }
|
||||
|
||||
/// Types for slider picker widget.
|
||||
enum ColorModel { rgb, hsv, hsl }
|
||||
// enum ColorSpace { rgb, hsv, hsl, hsp, okhsv, okhsl, xyz, yuv, lab, lch, cmyk }
|
||||
|
||||
/// Painter for SV mixture.
|
||||
class HSVWithHueColorPainter extends CustomPainter {
|
||||
const HSVWithHueColorPainter(this.hsvColor, {this.pointerColor});
|
||||
|
||||
final HSVColor hsvColor;
|
||||
final Color? pointerColor;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final Rect rect = Offset.zero & size;
|
||||
const Gradient gradientV = LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Colors.white, Colors.black],
|
||||
);
|
||||
final Gradient gradientH = LinearGradient(
|
||||
colors: [
|
||||
Colors.white,
|
||||
HSVColor.fromAHSV(1.0, hsvColor.hue, 1.0, 1.0).toColor(),
|
||||
],
|
||||
);
|
||||
canvas.drawRect(rect, Paint()..shader = gradientV.createShader(rect));
|
||||
canvas.drawRect(
|
||||
rect,
|
||||
Paint()
|
||||
..blendMode = BlendMode.multiply
|
||||
..shader = gradientH.createShader(rect),
|
||||
);
|
||||
|
||||
canvas.drawCircle(
|
||||
Offset(
|
||||
size.width * hsvColor.saturation, size.height * (1 - hsvColor.value)),
|
||||
size.height * 0.04,
|
||||
Paint()
|
||||
..color = pointerColor ??
|
||||
(useWhiteForeground(hsvColor.toColor())
|
||||
? Colors.white
|
||||
: Colors.black)
|
||||
..strokeWidth = 1.5
|
||||
..style = PaintingStyle.stroke,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
class _SliderLayout extends MultiChildLayoutDelegate {
|
||||
static const String track = 'track';
|
||||
static const String thumb = 'thumb';
|
||||
static const String gestureContainer = 'gesturecontainer';
|
||||
|
||||
@override
|
||||
void performLayout(Size size) {
|
||||
layoutChild(
|
||||
track,
|
||||
BoxConstraints.tightFor(
|
||||
width: size.width + 3,
|
||||
height: size.height / 1.5,
|
||||
),
|
||||
);
|
||||
positionChild(track, const Offset(-2.0, 0));
|
||||
layoutChild(
|
||||
thumb,
|
||||
const BoxConstraints.tightFor(width: 5.5, height: 10.5),
|
||||
);
|
||||
positionChild(thumb, Offset(0.0, (size.height / 1.5) / 2 - 4.5));
|
||||
layoutChild(
|
||||
gestureContainer,
|
||||
BoxConstraints.tightFor(width: size.width, height: size.height),
|
||||
);
|
||||
positionChild(gestureContainer, Offset.zero);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRelayout(_SliderLayout oldDelegate) => false;
|
||||
}
|
||||
|
||||
/// Painter for all kinds of track types.
|
||||
class TrackPainter extends CustomPainter {
|
||||
const TrackPainter(this.trackType, this.hsvColor);
|
||||
|
||||
final TrackType trackType;
|
||||
final HSVColor hsvColor;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final Rect rect = Offset.zero & size;
|
||||
if (trackType == TrackType.alpha) {
|
||||
final Size chessSize = Size(size.height / 2, size.height / 2);
|
||||
Paint chessPaintB = Paint()..color = const Color(0xffcccccc);
|
||||
Paint chessPaintW = Paint()..color = Colors.white;
|
||||
List.generate((size.height / chessSize.height).round(), (int y) {
|
||||
List.generate((size.width / chessSize.width).round(), (int x) {
|
||||
canvas.drawRect(
|
||||
Offset(chessSize.width * x, chessSize.width * y) & chessSize,
|
||||
(x + y) % 2 != 0 ? chessPaintW : chessPaintB,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
switch (trackType) {
|
||||
case TrackType.hue:
|
||||
final List<Color> colors = [
|
||||
const HSVColor.fromAHSV(1.0, 0.0, 1.0, 1.0).toColor(),
|
||||
const HSVColor.fromAHSV(1.0, 60.0, 1.0, 1.0).toColor(),
|
||||
const HSVColor.fromAHSV(1.0, 120.0, 1.0, 1.0).toColor(),
|
||||
const HSVColor.fromAHSV(1.0, 180.0, 1.0, 1.0).toColor(),
|
||||
const HSVColor.fromAHSV(1.0, 240.0, 1.0, 1.0).toColor(),
|
||||
const HSVColor.fromAHSV(1.0, 300.0, 1.0, 1.0).toColor(),
|
||||
const HSVColor.fromAHSV(1.0, 360.0, 1.0, 1.0).toColor(),
|
||||
];
|
||||
Gradient gradient = LinearGradient(colors: colors);
|
||||
canvas.drawRect(rect, Paint()..shader = gradient.createShader(rect));
|
||||
break;
|
||||
case TrackType.saturation:
|
||||
final List<Color> colors = [
|
||||
HSVColor.fromAHSV(1.0, hsvColor.hue, 0.0, 1.0).toColor(),
|
||||
HSVColor.fromAHSV(1.0, hsvColor.hue, 1.0, 1.0).toColor(),
|
||||
];
|
||||
Gradient gradient = LinearGradient(colors: colors);
|
||||
canvas.drawRect(rect, Paint()..shader = gradient.createShader(rect));
|
||||
break;
|
||||
case TrackType.saturationForHSL:
|
||||
final List<Color> colors = [
|
||||
HSLColor.fromAHSL(1.0, hsvColor.hue, 0.0, 0.5).toColor(),
|
||||
HSLColor.fromAHSL(1.0, hsvColor.hue, 1.0, 0.5).toColor(),
|
||||
];
|
||||
Gradient gradient = LinearGradient(colors: colors);
|
||||
canvas.drawRect(rect, Paint()..shader = gradient.createShader(rect));
|
||||
break;
|
||||
case TrackType.value:
|
||||
final List<Color> colors = [
|
||||
HSVColor.fromAHSV(1.0, hsvColor.hue, 1.0, 0.0).toColor(),
|
||||
HSVColor.fromAHSV(1.0, hsvColor.hue, 1.0, 1.0).toColor(),
|
||||
];
|
||||
Gradient gradient = LinearGradient(colors: colors);
|
||||
canvas.drawRect(rect, Paint()..shader = gradient.createShader(rect));
|
||||
break;
|
||||
case TrackType.lightness:
|
||||
final List<Color> colors = [
|
||||
HSLColor.fromAHSL(1.0, hsvColor.hue, 1.0, 0.0).toColor(),
|
||||
HSLColor.fromAHSL(1.0, hsvColor.hue, 1.0, 0.5).toColor(),
|
||||
HSLColor.fromAHSL(1.0, hsvColor.hue, 1.0, 1.0).toColor(),
|
||||
];
|
||||
Gradient gradient = LinearGradient(colors: colors);
|
||||
canvas.drawRect(rect, Paint()..shader = gradient.createShader(rect));
|
||||
break;
|
||||
case TrackType.red:
|
||||
final List<Color> colors = [
|
||||
hsvColor.toColor().withRed(0).withOpacity(1.0),
|
||||
hsvColor.toColor().withRed(255).withOpacity(1.0),
|
||||
];
|
||||
Gradient gradient = LinearGradient(colors: colors);
|
||||
canvas.drawRect(rect, Paint()..shader = gradient.createShader(rect));
|
||||
break;
|
||||
case TrackType.green:
|
||||
final List<Color> colors = [
|
||||
hsvColor.toColor().withGreen(0).withOpacity(1.0),
|
||||
hsvColor.toColor().withGreen(255).withOpacity(1.0),
|
||||
];
|
||||
Gradient gradient = LinearGradient(colors: colors);
|
||||
canvas.drawRect(rect, Paint()..shader = gradient.createShader(rect));
|
||||
break;
|
||||
case TrackType.blue:
|
||||
final List<Color> colors = [
|
||||
hsvColor.toColor().withBlue(0).withOpacity(1.0),
|
||||
hsvColor.toColor().withBlue(255).withOpacity(1.0),
|
||||
];
|
||||
Gradient gradient = LinearGradient(colors: colors);
|
||||
canvas.drawRect(rect, Paint()..shader = gradient.createShader(rect));
|
||||
break;
|
||||
case TrackType.alpha:
|
||||
final List<Color> colors = [
|
||||
hsvColor.toColor().withOpacity(0.0),
|
||||
hsvColor.toColor().withOpacity(1.0),
|
||||
];
|
||||
Gradient gradient = LinearGradient(colors: colors);
|
||||
canvas.drawRect(rect, Paint()..shader = gradient.createShader(rect));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
/// Painter for thumb of slider.
|
||||
class ThumbPainter extends CustomPainter {
|
||||
const ThumbPainter({this.thumbColor, this.fullThumbColor = false});
|
||||
|
||||
final Color? thumbColor;
|
||||
final bool fullThumbColor;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
canvas.drawShadow(
|
||||
Path()
|
||||
..addOval(
|
||||
Rect.fromCircle(
|
||||
center: const Offset(0.5, 2.0), radius: size.width * 1.8),
|
||||
),
|
||||
Colors.black,
|
||||
3.0,
|
||||
true,
|
||||
);
|
||||
canvas.drawCircle(
|
||||
Offset(0.0, size.height * 0.4),
|
||||
size.height,
|
||||
Paint()
|
||||
..color = Colors.white
|
||||
..style = PaintingStyle.fill);
|
||||
if (thumbColor != null) {
|
||||
canvas.drawCircle(
|
||||
Offset(0.0, size.height * 0.4),
|
||||
size.height * (fullThumbColor ? 1.0 : 0.65),
|
||||
Paint()
|
||||
..color = thumbColor!
|
||||
..style = PaintingStyle.fill);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
/// Painter for chess type alpha background in color indicator widget.
|
||||
class IndicatorPainter extends CustomPainter {
|
||||
const IndicatorPainter(this.color);
|
||||
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final Size chessSize = Size(size.width / 10, size.height / 10);
|
||||
final Paint chessPaintB = Paint()..color = const Color(0xFFCCCCCC);
|
||||
final Paint chessPaintW = Paint()..color = Colors.white;
|
||||
List.generate((size.height / chessSize.height).round(), (int y) {
|
||||
List.generate((size.width / chessSize.width).round(), (int x) {
|
||||
canvas.drawRect(
|
||||
Offset(chessSize.width * x, chessSize.height * y) & chessSize,
|
||||
(x + y) % 2 != 0 ? chessPaintW : chessPaintB,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
canvas.drawCircle(
|
||||
Offset(size.width / 2, size.height / 2),
|
||||
size.height / 2,
|
||||
Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.fill);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
/// Provide hex input wiget for 3/6/8 digits.
|
||||
class ColorPickerInput extends StatefulWidget {
|
||||
const ColorPickerInput(
|
||||
this.color,
|
||||
this.onColorChanged, {
|
||||
super.key,
|
||||
this.enableAlpha = true,
|
||||
this.embeddedText = false,
|
||||
this.disable = false,
|
||||
});
|
||||
|
||||
final Color color;
|
||||
final ValueChanged<Color> onColorChanged;
|
||||
final bool enableAlpha;
|
||||
final bool embeddedText;
|
||||
final bool disable;
|
||||
|
||||
@override
|
||||
ColorPickerInputState createState() => ColorPickerInputState();
|
||||
}
|
||||
|
||||
class ColorPickerInputState extends State<ColorPickerInput> {
|
||||
TextEditingController textEditingController = TextEditingController();
|
||||
int inputColor = 0;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
textEditingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (inputColor != widget.color.value) {
|
||||
textEditingController.text =
|
||||
'#${widget.color.red.toRadixString(16).toUpperCase().padLeft(2, '0')}${widget.color.green.toRadixString(16).toUpperCase().padLeft(2, '0')}${widget.color.blue.toRadixString(16).toUpperCase().padLeft(2, '0')}${widget.enableAlpha ? widget.color.alpha.toRadixString(16).toUpperCase().padLeft(2, '0') : ''}';
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 6.0, left: 12.0, right: 12.0),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: TextField(
|
||||
enabled: !widget.disable,
|
||||
controller: textEditingController,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
inputFormatters: [
|
||||
UpperCaseTextFormatter(),
|
||||
FilteringTextInputFormatter.allow(RegExp(kValidHexPattern)),
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
filled: true,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
borderSide:
|
||||
const BorderSide(color: Colors.transparent, width: 0.0),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
borderSide:
|
||||
const BorderSide(color: Colors.transparent, width: 0.0),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
borderSide:
|
||||
const BorderSide(color: Colors.transparent, width: 0.0),
|
||||
),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(vertical: 8.0, horizontal: 8.0),
|
||||
fillColor: AppColors.of(context).text.withOpacity(.1),
|
||||
),
|
||||
onChanged: (String value) {
|
||||
String input = value;
|
||||
if (value.length == 9) {
|
||||
input = value.split('').getRange(7, 9).join() +
|
||||
value.split('').getRange(1, 7).join();
|
||||
}
|
||||
final Color? color = colorFromHex(input);
|
||||
if (color != null) {
|
||||
widget.onColorChanged(color);
|
||||
inputColor = color.value;
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*class ValueColorPickerSlider extends StatefulWidget {
|
||||
ValueColorPickerSlider(this.trackType, this.initialHsvColor, this.onProgressChanged, this.onColorChangeEnd, {Key? key}) : super(key: key);
|
||||
|
||||
final TrackType trackType;
|
||||
final HSVColor initialHsvColor;
|
||||
final void Function(double progress) onProgressChanged;
|
||||
final void Function() onColorChangeEnd;
|
||||
|
||||
@override
|
||||
State<ValueColorPickerSlider> createState() => _ValueColorPickerSliderState();
|
||||
}
|
||||
|
||||
class _ValueColorPickerSliderState extends State<ValueColorPickerSlider> {
|
||||
HSVColor hsvColor = HSVColor.fromColor(Colors.red);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
hsvColor = widget.initialHsvColor;
|
||||
}
|
||||
|
||||
void slideEvent(RenderBox getBox, BoxConstraints box, Offset globalPosition) {
|
||||
double localDx = getBox.globalToLocal(globalPosition).dx - 15.0;
|
||||
double progress = localDx.clamp(0.0, box.maxWidth - 30.0) / (box.maxWidth - 30.0);
|
||||
setState(() {
|
||||
switch (widget.trackType) {
|
||||
case TrackType.hue:
|
||||
hsvColor = hsvColor.withHue(progress * 359);
|
||||
break;
|
||||
case TrackType.saturation:
|
||||
hsvColor = hsvColor.withSaturation(progress);
|
||||
break;
|
||||
case TrackType.value:
|
||||
hsvColor = hsvColor.withValue(progress);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
widget.onProgressChanged(progress);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: (BuildContext context, BoxConstraints box) {
|
||||
double thumbOffset = 15.0;
|
||||
Color thumbColor = Colors.white;
|
||||
switch (widget.trackType) {
|
||||
case TrackType.hue:
|
||||
thumbOffset += (box.maxWidth - 30.0) * hsvColor.hue / 360;
|
||||
break;
|
||||
case TrackType.saturation:
|
||||
thumbOffset += (box.maxWidth - 30.0) * hsvColor.saturation;
|
||||
break;
|
||||
case TrackType.value:
|
||||
thumbOffset += (box.maxWidth - 30.0) * hsvColor.value;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return CustomMultiChildLayout(
|
||||
delegate: _SliderLayout(),
|
||||
children: <Widget>[
|
||||
LayoutId(
|
||||
id: _SliderLayout.track,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(50.0)),
|
||||
child: CustomPaint(
|
||||
painter: TrackPainter(
|
||||
TrackType.values.firstWhere((element) => element == widget.trackType),
|
||||
hsvColor,
|
||||
)),
|
||||
),
|
||||
),
|
||||
LayoutId(
|
||||
id: _SliderLayout.thumb,
|
||||
child: Transform.translate(
|
||||
offset: Offset(thumbOffset, 0.0),
|
||||
child: CustomPaint(
|
||||
painter: ThumbPainter(
|
||||
thumbColor: thumbColor,
|
||||
fullThumbColor: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
LayoutId(
|
||||
id: _SliderLayout.gestureContainer,
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints box) {
|
||||
RenderBox? getBox = context.findRenderObject() as RenderBox?;
|
||||
return GestureDetector(
|
||||
onPanDown: (DragDownDetails details) => getBox != null ? slideEvent(getBox, box, details.globalPosition) : null,
|
||||
onPanEnd: (details) => widget.onColorChangeEnd(),
|
||||
onPanUpdate: (DragUpdateDetails details) => getBox != null ? slideEvent(getBox, box, details.globalPosition) : null,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}*/
|
||||
|
||||
/// 9 track types for slider picker widget.
|
||||
class ColorPickerSlider extends StatelessWidget {
|
||||
const ColorPickerSlider(
|
||||
this.trackType,
|
||||
this.hsvColor,
|
||||
this.onColorChanged,
|
||||
this.onColorChangeEnd,
|
||||
this.onProblem, {
|
||||
super.key,
|
||||
this.displayThumbColor = false,
|
||||
this.fullThumbColor = false,
|
||||
});
|
||||
|
||||
final TrackType trackType;
|
||||
final HSVColor hsvColor;
|
||||
final ValueChanged<HSVColor> onColorChanged;
|
||||
final void Function() onColorChangeEnd;
|
||||
final void Function(int v) onProblem;
|
||||
final bool displayThumbColor;
|
||||
final bool fullThumbColor;
|
||||
|
||||
void slideEvent(RenderBox getBox, BoxConstraints box, Offset globalPosition) {
|
||||
double localDx = getBox.globalToLocal(globalPosition).dx - 15.0;
|
||||
double progress =
|
||||
localDx.clamp(0.0, box.maxWidth - 30.0) / (box.maxWidth - 30.0);
|
||||
switch (trackType) {
|
||||
case TrackType.hue:
|
||||
// 360 is the same as zero
|
||||
// if set to 360, sliding to end goes to zero
|
||||
final newColor = hsvColor.withHue(progress * 359);
|
||||
if (newColor.saturation == 0) {
|
||||
onProblem(0);
|
||||
return;
|
||||
}
|
||||
onColorChanged(newColor);
|
||||
break;
|
||||
case TrackType.saturation:
|
||||
final newColor = hsvColor.withSaturation(progress);
|
||||
if (newColor.value == 0) {
|
||||
onProblem(1);
|
||||
return;
|
||||
}
|
||||
onColorChanged(newColor);
|
||||
break;
|
||||
case TrackType.value:
|
||||
onColorChanged(hsvColor.withValue(progress));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: (BuildContext context, BoxConstraints box) {
|
||||
double thumbOffset = 15.0;
|
||||
Color thumbColor;
|
||||
switch (trackType) {
|
||||
case TrackType.hue:
|
||||
thumbOffset += (box.maxWidth - 30.0) * hsvColor.hue / 360;
|
||||
thumbColor = HSVColor.fromAHSV(1.0, hsvColor.hue, 1.0, 1.0).toColor();
|
||||
break;
|
||||
case TrackType.saturation:
|
||||
thumbOffset += (box.maxWidth - 30.0) * hsvColor.saturation;
|
||||
thumbColor =
|
||||
HSVColor.fromAHSV(1.0, hsvColor.hue, hsvColor.saturation, 1.0)
|
||||
.toColor();
|
||||
break;
|
||||
case TrackType.saturationForHSL:
|
||||
thumbOffset += (box.maxWidth - 30.0) * hsvToHsl(hsvColor).saturation;
|
||||
thumbColor = HSLColor.fromAHSL(
|
||||
1.0, hsvColor.hue, hsvToHsl(hsvColor).saturation, 0.5)
|
||||
.toColor();
|
||||
break;
|
||||
case TrackType.value:
|
||||
thumbOffset += (box.maxWidth - 30.0) * hsvColor.value;
|
||||
thumbColor = HSVColor.fromAHSV(1.0, hsvColor.hue, 1.0, hsvColor.value)
|
||||
.toColor();
|
||||
break;
|
||||
case TrackType.lightness:
|
||||
thumbOffset += (box.maxWidth - 30.0) * hsvToHsl(hsvColor).lightness;
|
||||
thumbColor = HSLColor.fromAHSL(
|
||||
1.0, hsvColor.hue, 1.0, hsvToHsl(hsvColor).lightness)
|
||||
.toColor();
|
||||
break;
|
||||
case TrackType.red:
|
||||
thumbOffset += (box.maxWidth - 30.0) * hsvColor.toColor().red / 0xff;
|
||||
thumbColor = hsvColor.toColor().withOpacity(1.0);
|
||||
break;
|
||||
case TrackType.green:
|
||||
thumbOffset +=
|
||||
(box.maxWidth - 30.0) * hsvColor.toColor().green / 0xff;
|
||||
thumbColor = hsvColor.toColor().withOpacity(1.0);
|
||||
break;
|
||||
case TrackType.blue:
|
||||
thumbOffset += (box.maxWidth - 30.0) * hsvColor.toColor().blue / 0xff;
|
||||
thumbColor = hsvColor.toColor().withOpacity(1.0);
|
||||
break;
|
||||
case TrackType.alpha:
|
||||
thumbOffset += (box.maxWidth - 30.0) * hsvColor.toColor().opacity;
|
||||
thumbColor = hsvColor.toColor().withOpacity(hsvColor.alpha);
|
||||
break;
|
||||
}
|
||||
|
||||
return CustomMultiChildLayout(
|
||||
delegate: _SliderLayout(),
|
||||
children: <Widget>[
|
||||
LayoutId(
|
||||
id: _SliderLayout.track,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(50.0)),
|
||||
child: CustomPaint(
|
||||
painter: TrackPainter(
|
||||
trackType,
|
||||
hsvColor,
|
||||
)),
|
||||
),
|
||||
),
|
||||
LayoutId(
|
||||
id: _SliderLayout.thumb,
|
||||
child: Transform.translate(
|
||||
offset: Offset(thumbOffset, 0.0),
|
||||
child: CustomPaint(
|
||||
painter: ThumbPainter(
|
||||
thumbColor: displayThumbColor ? thumbColor : null,
|
||||
fullThumbColor: fullThumbColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
LayoutId(
|
||||
id: _SliderLayout.gestureContainer,
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints box) {
|
||||
RenderBox? getBox = context.findRenderObject() as RenderBox?;
|
||||
return GestureDetector(
|
||||
onPanDown: (DragDownDetails details) => getBox != null
|
||||
? slideEvent(getBox, box, details.globalPosition)
|
||||
: null,
|
||||
onPanEnd: (details) {
|
||||
if ((trackType == TrackType.hue &&
|
||||
hsvColor.saturation == 0) ||
|
||||
(trackType == TrackType.saturation &&
|
||||
hsvColor.value == 0)) {
|
||||
return;
|
||||
}
|
||||
onColorChangeEnd();
|
||||
},
|
||||
onPanUpdate: (DragUpdateDetails details) => getBox != null
|
||||
? slideEvent(getBox, box, details.globalPosition)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple round color indicator.
|
||||
class ColorIndicator extends StatelessWidget {
|
||||
const ColorIndicator(
|
||||
this.hsvColor, {
|
||||
super.key,
|
||||
this.currentHsvColor,
|
||||
this.icon,
|
||||
this.width = 50.0,
|
||||
this.height = 50.0,
|
||||
this.adaptive = false,
|
||||
});
|
||||
|
||||
final HSVColor hsvColor;
|
||||
final HSVColor? currentHsvColor;
|
||||
final double width;
|
||||
final double height;
|
||||
final IconData? icon;
|
||||
final bool adaptive;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color color = hsvColor.toColor();
|
||||
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: color,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: useWhiteForeground(color)
|
||||
? Colors.white.withOpacity(.5)
|
||||
: Colors.black.withOpacity(.5),
|
||||
offset: const Offset(0, 0),
|
||||
blurRadius: 5)
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 210),
|
||||
opacity: (icon != null || currentHsvColor == hsvColor) &&
|
||||
(adaptive ||
|
||||
Provider.of<SettingsProvider>(context, listen: false)
|
||||
.accentColor !=
|
||||
AccentColor.adaptive)
|
||||
? 1
|
||||
: 0,
|
||||
child: Icon(icon ?? Icons.done,
|
||||
color: useWhiteForeground(color) ? Colors.white : Colors.black),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Provide Rectangle & Circle 2 categories, 10 variations of palette widget.
|
||||
class ColorPickerArea extends StatelessWidget {
|
||||
const ColorPickerArea(
|
||||
this.hsvColor,
|
||||
this.onColorChanged,
|
||||
this.onChangeEnd,
|
||||
this.paletteType, {
|
||||
super.key,
|
||||
});
|
||||
|
||||
final HSVColor hsvColor;
|
||||
final ValueChanged<HSVColor> onColorChanged;
|
||||
final void Function() onChangeEnd;
|
||||
final PaletteType paletteType;
|
||||
|
||||
/*void _handleColorRectChange(double horizontal, double vertical) {
|
||||
onColorChanged(hsvColor.withSaturation(horizontal).withValue(vertical));
|
||||
}*/
|
||||
|
||||
void _handleGesture(
|
||||
Offset position, BuildContext context, double height, double width) {
|
||||
RenderBox? getBox = context.findRenderObject() as RenderBox?;
|
||||
if (getBox == null) return;
|
||||
|
||||
Offset localOffset = getBox.globalToLocal(position);
|
||||
double horizontal = localOffset.dx.clamp(0.0, width);
|
||||
double vertical = localOffset.dy.clamp(0.0, height);
|
||||
|
||||
//_handleColorRectChange(horizontal / width, 1 - vertical / height);
|
||||
|
||||
onColorChanged(hsvColor.withSaturation(horizontal).withValue(vertical));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
double width = constraints.maxWidth;
|
||||
double height = constraints.maxHeight;
|
||||
|
||||
return RawGestureDetector(
|
||||
gestures: {
|
||||
_AlwaysWinPanGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<
|
||||
_AlwaysWinPanGestureRecognizer>(
|
||||
() => _AlwaysWinPanGestureRecognizer(),
|
||||
(_AlwaysWinPanGestureRecognizer instance) {
|
||||
instance
|
||||
..onDown = ((details) => _handleGesture(
|
||||
details.globalPosition, context, height, width))
|
||||
..onEnd = ((d) => onChangeEnd())
|
||||
..onUpdate = ((details) => _handleGesture(
|
||||
details.globalPosition, context, height, width));
|
||||
},
|
||||
),
|
||||
},
|
||||
child: Builder(
|
||||
builder: (BuildContext _) {
|
||||
return CustomPaint(painter: HSVWithHueColorPainter(hsvColor));
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AlwaysWinPanGestureRecognizer extends PanGestureRecognizer {
|
||||
@override
|
||||
void addAllowedPointer(event) {
|
||||
super.addAllowedPointer(event);
|
||||
resolve(GestureDisposition.accepted);
|
||||
}
|
||||
|
||||
@override
|
||||
String get debugDescription => 'alwaysWin';
|
||||
}
|
||||
|
||||
/// Uppercase text formater
|
||||
class UpperCaseTextFormatter extends TextInputFormatter {
|
||||
@override
|
||||
TextEditingValue formatEditUpdate(oldValue, TextEditingValue newValue) =>
|
||||
TextEditingValue(
|
||||
text: newValue.text.toUpperCase(), selection: newValue.selection);
|
||||
}
|
||||
221
refilc/lib/ui/flutter_colorpicker/utils.dart
Normal file
221
refilc/lib/ui/flutter_colorpicker/utils.dart
Normal file
@@ -0,0 +1,221 @@
|
||||
// FROM: https://pub.dev/packages/flutter_colorpicker
|
||||
// FROM: https://pub.dev/packages/flutter_colorpicker
|
||||
// FROM: https://pub.dev/packages/flutter_colorpicker
|
||||
// FROM: https://pub.dev/packages/flutter_colorpicker
|
||||
|
||||
// ignore: dangling_library_doc_comments
|
||||
/// Common function lib
|
||||
|
||||
import 'dart:math';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'colors.dart';
|
||||
|
||||
/// Check if is good condition to use white foreground color by passing
|
||||
/// the background color, and optional bias.
|
||||
///
|
||||
/// Reference:
|
||||
///
|
||||
/// Old: https://www.w3.org/TR/WCAG20-TECHS/G18.html
|
||||
///
|
||||
/// New: https://github.com/mchome/flutter_statusbarcolor/issues/40
|
||||
bool useWhiteForeground(Color backgroundColor, {double bias = 0.0}) {
|
||||
// Old:
|
||||
// return 1.05 / (color.computeLuminance() + 0.05) > 4.5;
|
||||
|
||||
// New:
|
||||
int v = sqrt(pow(backgroundColor.red, 2) * 0.299 +
|
||||
pow(backgroundColor.green, 2) * 0.587 +
|
||||
pow(backgroundColor.blue, 2) * 0.114)
|
||||
.round();
|
||||
return v < 130 + bias ? true : false;
|
||||
}
|
||||
|
||||
/// Convert HSV to HSL
|
||||
///
|
||||
/// Reference: https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_HSL
|
||||
HSLColor hsvToHsl(HSVColor color) {
|
||||
double s = 0.0;
|
||||
double l = 0.0;
|
||||
l = (2 - color.saturation) * color.value / 2;
|
||||
if (l != 0) {
|
||||
if (l == 1) {
|
||||
s = 0.0;
|
||||
} else if (l < 0.5) {
|
||||
s = color.saturation * color.value / (l * 2);
|
||||
} else {
|
||||
s = color.saturation * color.value / (2 - l * 2);
|
||||
}
|
||||
}
|
||||
return HSLColor.fromAHSL(
|
||||
color.alpha,
|
||||
color.hue,
|
||||
s.clamp(0.0, 1.0),
|
||||
l.clamp(0.0, 1.0),
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert HSL to HSV
|
||||
///
|
||||
/// Reference: https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_HSV
|
||||
HSVColor hslToHsv(HSLColor color) {
|
||||
double s = 0.0;
|
||||
double v = 0.0;
|
||||
|
||||
v = color.lightness + color.saturation * (color.lightness < 0.5 ? color.lightness : 1 - color.lightness);
|
||||
if (v != 0) s = 2 - 2 * color.lightness / v;
|
||||
|
||||
return HSVColor.fromAHSV(
|
||||
color.alpha,
|
||||
color.hue,
|
||||
s.clamp(0.0, 1.0),
|
||||
v.clamp(0.0, 1.0),
|
||||
);
|
||||
}
|
||||
|
||||
/// [RegExp] pattern for validation HEX color [String] inputs, allows only:
|
||||
///
|
||||
/// * exactly 1 to 8 digits in HEX format,
|
||||
/// * only Latin A-F characters, case insensitive,
|
||||
/// * and integer numbers 0,1,2,3,4,5,6,7,8,9,
|
||||
/// * with optional hash (`#`) symbol at the beginning (not calculated in length).
|
||||
///
|
||||
/// ```dart
|
||||
/// final RegExp hexInputValidator = RegExp(kValidHexPattern);
|
||||
/// if (hexInputValidator.hasMatch(hex)) print('$hex might be a valid HEX color');
|
||||
/// ```
|
||||
/// Reference: https://en.wikipedia.org/wiki/Web_colors#Hex_triplet
|
||||
const String kValidHexPattern = r'^#?[0-9a-fA-F]{1,8}';
|
||||
|
||||
/// [RegExp] pattern for validation complete HEX color [String], allows only:
|
||||
///
|
||||
/// * exactly 6 or 8 digits in HEX format,
|
||||
/// * only Latin A-F characters, case insensitive,
|
||||
/// * and integer numbers 0,1,2,3,4,5,6,7,8,9,
|
||||
/// * with optional hash (`#`) symbol at the beginning (not calculated in length).
|
||||
///
|
||||
/// ```dart
|
||||
/// final RegExp hexCompleteValidator = RegExp(kCompleteValidHexPattern);
|
||||
/// if (hexCompleteValidator.hasMatch(hex)) print('$hex is valid HEX color');
|
||||
/// ```
|
||||
/// Reference: https://en.wikipedia.org/wiki/Web_colors#Hex_triplet
|
||||
const String kCompleteValidHexPattern = r'^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$';
|
||||
|
||||
/// Try to convert text input or any [String] to valid [Color].
|
||||
/// The [String] must be provided in one of those formats:
|
||||
///
|
||||
/// * RGB
|
||||
/// * #RGB
|
||||
/// * RRGGBB
|
||||
/// * #RRGGBB
|
||||
/// * AARRGGBB
|
||||
/// * #AARRGGBB
|
||||
///
|
||||
/// Where: A stands for Alpha, R for Red, G for Green, and B for blue color.
|
||||
/// It will only accept 3/6/8 long HEXs with an optional hash (`#`) at the beginning.
|
||||
/// Allowed characters are Latin A-F case insensitive and numbers 0-9.
|
||||
/// Optional [enableAlpha] can be provided (it's `true` by default). If it's set
|
||||
/// to `false` transparency information (alpha channel) will be removed.
|
||||
/// ```dart
|
||||
/// /// // Valid 3 digit HEXs:
|
||||
/// colorFromHex('abc') == Color(0xffaabbcc)
|
||||
/// colorFromHex('ABc') == Color(0xffaabbcc)
|
||||
/// colorFromHex('ABC') == Color(0xffaabbcc)
|
||||
/// colorFromHex('#Abc') == Color(0xffaabbcc)
|
||||
/// colorFromHex('#abc') == Color(0xffaabbcc)
|
||||
/// colorFromHex('#ABC') == Color(0xffaabbcc)
|
||||
/// // Valid 6 digit HEXs:
|
||||
/// colorFromHex('aabbcc') == Color(0xffaabbcc)
|
||||
/// colorFromHex('AABbcc') == Color(0xffaabbcc)
|
||||
/// colorFromHex('AABBCC') == Color(0xffaabbcc)
|
||||
/// colorFromHex('#AABbcc') == Color(0xffaabbcc)
|
||||
/// colorFromHex('#aabbcc') == Color(0xffaabbcc)
|
||||
/// colorFromHex('#AABBCC') == Color(0xffaabbcc)
|
||||
/// // Valid 8 digit HEXs:
|
||||
/// colorFromHex('ffaabbcc') == Color(0xffaabbcc)
|
||||
/// colorFromHex('ffAABbcc') == Color(0xffaabbcc)
|
||||
/// colorFromHex('ffAABBCC') == Color(0xffaabbcc)
|
||||
/// colorFromHex('ffaabbcc', enableAlpha: true) == Color(0xffaabbcc)
|
||||
/// colorFromHex('FFAAbbcc', enableAlpha: true) == Color(0xffaabbcc)
|
||||
/// colorFromHex('ffAABBCC', enableAlpha: true) == Color(0xffaabbcc)
|
||||
/// colorFromHex('FFaabbcc', enableAlpha: true) == Color(0xffaabbcc)
|
||||
/// colorFromHex('#ffaabbcc') == Color(0xffaabbcc)
|
||||
/// colorFromHex('#ffAABbcc') == Color(0xffaabbcc)
|
||||
/// colorFromHex('#FFAABBCC') == Color(0xffaabbcc)
|
||||
/// colorFromHex('#ffaabbcc', enableAlpha: true) == Color(0xffaabbcc)
|
||||
/// colorFromHex('#FFAAbbcc', enableAlpha: true) == Color(0xffaabbcc)
|
||||
/// colorFromHex('#ffAABBCC', enableAlpha: true) == Color(0xffaabbcc)
|
||||
/// colorFromHex('#FFaabbcc', enableAlpha: true) == Color(0xffaabbcc)
|
||||
/// // Invalid HEXs:
|
||||
/// colorFromHex('bc') == null // length 2
|
||||
/// colorFromHex('aabbc') == null // length 5
|
||||
/// colorFromHex('#ffaabbccd') == null // length 9 (+#)
|
||||
/// colorFromHex('aabbcx') == null // x character
|
||||
/// colorFromHex('#aabbвв') == null // в non-latin character
|
||||
/// colorFromHex('') == null // empty
|
||||
/// ```
|
||||
/// Reference: https://en.wikipedia.org/wiki/Web_colors#Hex_triplet
|
||||
Color? colorFromHex(String inputString, {bool enableAlpha = true}) {
|
||||
// Registers validator for exactly 6 or 8 digits long HEX (with optional #).
|
||||
final RegExp hexValidator = RegExp(kCompleteValidHexPattern);
|
||||
// Validating input, if it does not match — it's not proper HEX.
|
||||
if (!hexValidator.hasMatch(inputString)) return null;
|
||||
// Remove optional hash if exists and convert HEX to UPPER CASE.
|
||||
String hexToParse = inputString.replaceFirst('#', '').toUpperCase();
|
||||
// It may allow HEXs with transparency information even if alpha is disabled,
|
||||
if (!enableAlpha && hexToParse.length == 8) {
|
||||
// but it will replace this info with 100% non-transparent value (FF).
|
||||
hexToParse = 'FF${hexToParse.substring(2)}';
|
||||
}
|
||||
// HEX may be provided in 3-digits format, let's just duplicate each letter.
|
||||
if (hexToParse.length == 3) {
|
||||
hexToParse = hexToParse.split('').expand((i) => [i * 2]).join();
|
||||
}
|
||||
// We will need 8 digits to parse the color, let's add missing digits.
|
||||
if (hexToParse.length == 6) hexToParse = 'FF$hexToParse';
|
||||
// HEX must be valid now, but as a precaution, it will just "try" to parse it.
|
||||
final intColorValue = int.tryParse(hexToParse, radix: 16);
|
||||
// If for some reason HEX is not valid — abort the operation, return nothing.
|
||||
if (intColorValue == null) return null;
|
||||
// Register output color for the last step.
|
||||
final color = Color(intColorValue);
|
||||
// Decide to return color with transparency information or not.
|
||||
return enableAlpha ? color : color.withAlpha(255);
|
||||
}
|
||||
|
||||
/// Converts `dart:ui` [Color] to the 6/8 digits HEX [String].
|
||||
///
|
||||
/// Prefixes a hash (`#`) sign if [includeHashSign] is set to `true`.
|
||||
/// The result will be provided as UPPER CASE, it can be changed via [toUpperCase]
|
||||
/// flag set to `false` (default is `true`). Hex can be returned without alpha
|
||||
/// channel information (transparency), with the [enableAlpha] flag set to `false`.
|
||||
String colorToHex(
|
||||
Color color, {
|
||||
bool includeHashSign = false,
|
||||
bool enableAlpha = true,
|
||||
bool toUpperCase = true,
|
||||
}) {
|
||||
final String hex = (includeHashSign ? '#' : '') +
|
||||
(enableAlpha ? _padRadix(color.alpha) : '') +
|
||||
_padRadix(color.red) +
|
||||
_padRadix(color.green) +
|
||||
_padRadix(color.blue);
|
||||
return toUpperCase ? hex.toUpperCase() : hex;
|
||||
}
|
||||
|
||||
// Shorthand for padLeft of RadixString, DRY.
|
||||
String _padRadix(int value) => value.toRadixString(16).padLeft(2, '0');
|
||||
|
||||
// Extension for String
|
||||
extension ColorExtension1 on String {
|
||||
Color? toColor() {
|
||||
Color? color = colorFromName(this);
|
||||
if (color != null) return color;
|
||||
return colorFromHex(this);
|
||||
}
|
||||
}
|
||||
|
||||
// Extension from Color
|
||||
extension ColorExtension2 on Color {
|
||||
String toHexString({bool includeHashSign = false, bool enableAlpha = true, bool toUpperCase = true}) =>
|
||||
colorToHex(this, includeHashSign: false, enableAlpha: true, toUpperCase: true);
|
||||
}
|
||||
358
refilc/lib/ui/widgets/grade/grade_tile.dart
Normal file
358
refilc/lib/ui/widgets/grade/grade_tile.dart
Normal file
@@ -0,0 +1,358 @@
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:refilc_kreta_api/models/grade.dart';
|
||||
import 'package:refilc/helpers/subject.dart';
|
||||
import 'package:refilc/utils/format.dart';
|
||||
import 'package:refilc_mobile_ui/pages/grades/calculator/grade_calculator_provider.dart';
|
||||
import 'package:refilc_mobile_ui/pages/grades/subject_grades_container.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class GradeTile extends StatelessWidget {
|
||||
const GradeTile(
|
||||
this.grade, {
|
||||
super.key,
|
||||
this.onTap,
|
||||
this.padding,
|
||||
this.censored = false,
|
||||
this.viewOverride = false,
|
||||
});
|
||||
|
||||
final Grade grade;
|
||||
final void Function()? onTap;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final bool censored;
|
||||
final bool viewOverride;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String title;
|
||||
String subtitle;
|
||||
bool isTitleItalic = false;
|
||||
bool isSubtitleItalic = false;
|
||||
// EdgeInsets leadingPadding = EdgeInsets.zero;
|
||||
bool isSubjectView =
|
||||
SubjectGradesContainer.of(context) != null || viewOverride;
|
||||
String subjectName =
|
||||
grade.subject.renamedTo ?? grade.subject.name.escapeHtml().capital();
|
||||
String modeDescription = grade.mode.description.escapeHtml().capital();
|
||||
String description = grade.description.escapeHtml().capital();
|
||||
|
||||
GradeCalculatorProvider calculatorProvider =
|
||||
Provider.of<GradeCalculatorProvider>(context, listen: false);
|
||||
SettingsProvider settingsProvider = Provider.of<SettingsProvider>(context);
|
||||
// Test order:
|
||||
// description
|
||||
// mode
|
||||
// value name
|
||||
if (grade.type == GradeType.midYear || grade.type == GradeType.ghost) {
|
||||
if (grade.description != "") {
|
||||
title = description;
|
||||
} else {
|
||||
title = modeDescription != ""
|
||||
? modeDescription
|
||||
: grade.value.valueName.split("(")[0];
|
||||
}
|
||||
} else {
|
||||
title = subjectName;
|
||||
isTitleItalic =
|
||||
grade.subject.isRenamed && settingsProvider.renamedSubjectsItalics;
|
||||
}
|
||||
|
||||
// Test order:
|
||||
// subject name
|
||||
// mode + weight != 100
|
||||
if (grade.type == GradeType.midYear) {
|
||||
subtitle = isSubjectView
|
||||
? description != ""
|
||||
? modeDescription
|
||||
: ""
|
||||
: subjectName;
|
||||
isSubtitleItalic = isSubjectView
|
||||
? false
|
||||
: grade.subject.isRenamed && settingsProvider.renamedSubjectsItalics;
|
||||
} else {
|
||||
subtitle = grade.value.valueName.split("(")[0];
|
||||
}
|
||||
|
||||
// if (subtitle != "") leadingPadding = const EdgeInsets.only(top: 2.0);
|
||||
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Padding(
|
||||
padding: padding ?? const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: ListTile(
|
||||
visualDensity: VisualDensity.compact,
|
||||
contentPadding: isSubjectView
|
||||
? grade.type != GradeType.ghost
|
||||
? const EdgeInsets.symmetric(horizontal: 12.0)
|
||||
: const EdgeInsets.only(left: 12.0, right: 4.0)
|
||||
: const EdgeInsets.only(left: 8.0, right: 12.0),
|
||||
onTap: onTap,
|
||||
// onLongPress: kDebugMode ? () => log(jsonEncode(grade.json)) : null,
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(14.0)),
|
||||
leading: isSubjectView
|
||||
? GradeValueWidget(grade.value)
|
||||
: GradeValueWidget(
|
||||
grade.value,
|
||||
fill: true,
|
||||
size: 27.5,
|
||||
),
|
||||
// leading: isSubjectView
|
||||
// ? GradeValueWidget(grade.value)
|
||||
// : SizedBox(
|
||||
// width: 44,
|
||||
// height: 44,
|
||||
// child: censored
|
||||
// ? Container(
|
||||
// decoration: BoxDecoration(
|
||||
// color: AppColors.of(context).text,
|
||||
// borderRadius: BorderRadius.circular(60.0),
|
||||
// ),
|
||||
// )
|
||||
// : Center(
|
||||
// child: Padding(
|
||||
// padding: leadingPadding,
|
||||
// child: Icon(
|
||||
// SubjectIcon.resolveVariant(
|
||||
// subject: grade.subject, context: context),
|
||||
// size: 28.0,
|
||||
// color: AppColors.of(context).text,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
title: censored
|
||||
? Wrap(
|
||||
children: [
|
||||
Container(
|
||||
width: 110,
|
||||
height: 15,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.of(context).text,
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Text(
|
||||
title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontStyle: isTitleItalic ? FontStyle.italic : null),
|
||||
),
|
||||
subtitle: subtitle != ""
|
||||
? censored
|
||||
? Wrap(
|
||||
children: [
|
||||
Container(
|
||||
width: 50,
|
||||
height: 10,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.of(context).text,
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Text(
|
||||
subtitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontStyle:
|
||||
isSubtitleItalic ? FontStyle.italic : null),
|
||||
)
|
||||
: null,
|
||||
trailing: isSubjectView
|
||||
? grade.type != GradeType.ghost
|
||||
? Text(grade.date.format(context),
|
||||
style: const TextStyle(fontWeight: FontWeight.w500))
|
||||
: IconButton(
|
||||
splashRadius: 24.0,
|
||||
icon: Icon(FeatherIcons.trash2,
|
||||
color: AppColors.of(context).red),
|
||||
onPressed: () {
|
||||
calculatorProvider.removeGrade(grade);
|
||||
},
|
||||
)
|
||||
: censored
|
||||
? Container(
|
||||
width: 15,
|
||||
height: 15,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.of(context).text,
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
)
|
||||
// : GradeValueWidget(grade.value),
|
||||
: Icon(
|
||||
SubjectIcon.resolveVariant(
|
||||
context: context, subject: grade.subject),
|
||||
color: AppColors.of(context).text.withOpacity(.5),
|
||||
),
|
||||
minLeadingWidth: isSubjectView ? 32.0 : 0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GradeValueWidget extends StatelessWidget {
|
||||
const GradeValueWidget(
|
||||
this.value, {
|
||||
super.key,
|
||||
this.size = 38.0,
|
||||
this.fill = false,
|
||||
this.contrast = false,
|
||||
this.shadow = false,
|
||||
this.outline = false,
|
||||
this.complemented = false,
|
||||
this.nocolor = false,
|
||||
this.color,
|
||||
});
|
||||
|
||||
final GradeValue value;
|
||||
final double size;
|
||||
final bool fill;
|
||||
final bool contrast;
|
||||
final bool shadow;
|
||||
final bool outline;
|
||||
final bool complemented;
|
||||
final bool nocolor;
|
||||
final Color? color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
GradeValue value = this.value;
|
||||
bool isSubjectView = SubjectGradesContainer.of(context) != null;
|
||||
|
||||
Color color = this.color ??
|
||||
gradeColor(context: context, value: value.value, nocolor: nocolor);
|
||||
Widget valueText;
|
||||
final percentage = value.percentage;
|
||||
|
||||
if (percentage) {
|
||||
double multiplier = 1.0;
|
||||
|
||||
if (isSubjectView) multiplier = 0.75;
|
||||
|
||||
valueText = Text.rich(
|
||||
TextSpan(
|
||||
text: value.value.toString(),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "\n%",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: size / 2.5 * multiplier,
|
||||
height: 0.7),
|
||||
),
|
||||
],
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: size / (isSubjectView ? 1 : 1.5) * multiplier,
|
||||
height: 1),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
} else if (value.valueName.toLowerCase().specialChars() == 'nem irt') {
|
||||
valueText = const Icon(FeatherIcons.slash);
|
||||
} else {
|
||||
valueText = Stack(alignment: Alignment.topRight, children: [
|
||||
Transform.translate(
|
||||
offset: (value.weight >= 200) ? const Offset(1.0, 0.2) : Offset.zero,
|
||||
child: Text(
|
||||
value.value.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontWeight:
|
||||
value.weight == 50 ? FontWeight.w500 : FontWeight.bold,
|
||||
fontSize: size,
|
||||
color: contrast ? Colors.white : color,
|
||||
shadows: [
|
||||
if (value.weight >= 200)
|
||||
Shadow(
|
||||
color: (contrast ? Colors.white : color).withOpacity(.4),
|
||||
offset: const Offset(-4, -3),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (complemented)
|
||||
Transform.translate(
|
||||
offset: const Offset(9, 1),
|
||||
child: Text(
|
||||
"*",
|
||||
style:
|
||||
TextStyle(fontSize: size / 1.6, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
return fill
|
||||
? Container(
|
||||
width: size * 1.4,
|
||||
height: size * 1.4,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(contrast ? 1.0 : .25),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
if (shadow &&
|
||||
Provider.of<SettingsProvider>(context, listen: false)
|
||||
.shadowEffect)
|
||||
BoxShadow(
|
||||
color: color,
|
||||
blurRadius: 62.0,
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Center(child: valueText),
|
||||
)
|
||||
: valueText;
|
||||
}
|
||||
}
|
||||
|
||||
Color gradeColor(
|
||||
{required BuildContext context, required num value, bool nocolor = false}) {
|
||||
int valueInt = 0;
|
||||
|
||||
var settings = Provider.of<SettingsProvider>(context, listen: false);
|
||||
|
||||
try {
|
||||
if (value < 2.0) {
|
||||
valueInt = 1;
|
||||
} else {
|
||||
if (value >= value.floor() + settings.rounding / 10) {
|
||||
valueInt = value.ceil();
|
||||
} else {
|
||||
valueInt = value.floor();
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
if (nocolor) return AppColors.of(context).text;
|
||||
|
||||
switch (valueInt) {
|
||||
case 5:
|
||||
return settings.gradeColors[4];
|
||||
case 4:
|
||||
return settings.gradeColors[3];
|
||||
case 3:
|
||||
return settings.gradeColors[2];
|
||||
case 2:
|
||||
return settings.gradeColors[1];
|
||||
case 1:
|
||||
return settings.gradeColors[0];
|
||||
default:
|
||||
return AppColors.of(context).text;
|
||||
}
|
||||
}
|
||||
451
refilc/lib/ui/widgets/lesson/lesson_tile.dart
Normal file
451
refilc/lib/ui/widgets/lesson/lesson_tile.dart
Normal file
@@ -0,0 +1,451 @@
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc_kreta_api/providers/exam_provider.dart';
|
||||
import 'package:refilc_kreta_api/providers/homework_provider.dart';
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:refilc_kreta_api/models/exam.dart';
|
||||
import 'package:refilc_kreta_api/models/homework.dart';
|
||||
import 'package:refilc_kreta_api/models/lesson.dart';
|
||||
import 'package:refilc/utils/format.dart';
|
||||
import 'package:refilc_mobile_ui/common/panel/panel.dart';
|
||||
import 'package:refilc_mobile_ui/common/round_border_icon.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/exam/exam_view.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/homework/homework_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'lesson_tile.i18n.dart';
|
||||
|
||||
class LessonTile extends StatelessWidget {
|
||||
const LessonTile(this.lesson, {super.key, this.onTap, this.swapDesc = false});
|
||||
|
||||
final Lesson lesson;
|
||||
final bool swapDesc;
|
||||
final void Function()? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget> subtiles = [];
|
||||
|
||||
Color accent = Theme.of(context).colorScheme.secondary;
|
||||
bool fill = false;
|
||||
bool fillLeading = false;
|
||||
String lessonIndexTrailing = "";
|
||||
|
||||
SettingsProvider settingsProvider = Provider.of<SettingsProvider>(context);
|
||||
|
||||
// Only put a trailing . if its a digit
|
||||
if (RegExp(r'\d').hasMatch(lesson.lessonIndex)) lessonIndexTrailing = ".";
|
||||
|
||||
var now = DateTime.now();
|
||||
if (lesson.start.isBefore(now) &&
|
||||
lesson.end.isAfter(now) &&
|
||||
lesson.status?.name != "Elmaradt") {
|
||||
fillLeading = true;
|
||||
}
|
||||
|
||||
if (lesson.substituteTeacher != null &&
|
||||
lesson.substituteTeacher?.name != "") {
|
||||
fill = true;
|
||||
accent = AppColors.of(context).yellow;
|
||||
}
|
||||
|
||||
if (lesson.status?.name == "Elmaradt") {
|
||||
fill = true;
|
||||
accent = AppColors.of(context).red;
|
||||
}
|
||||
|
||||
if (lesson.isEmpty) {
|
||||
accent = AppColors.of(context).text.withOpacity(0.6);
|
||||
}
|
||||
|
||||
if (!lesson.studentPresence) {
|
||||
subtiles.add(LessonSubtile(
|
||||
type: LessonSubtileType.absence,
|
||||
title: "absence".i18n,
|
||||
));
|
||||
}
|
||||
|
||||
if (lesson.homeworkId != "") {
|
||||
Homework homework = Provider.of<HomeworkProvider>(context, listen: false)
|
||||
.homework
|
||||
.firstWhere((h) => h.id == lesson.homeworkId,
|
||||
orElse: () => Homework.fromJson({}));
|
||||
|
||||
if (homework.id != "") {
|
||||
subtiles.add(LessonSubtile(
|
||||
type: LessonSubtileType.homework,
|
||||
title: homework.content,
|
||||
onPressed: () => HomeworkView.show(homework, context: context),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (lesson.exam != "") {
|
||||
Exam exam = Provider.of<ExamProvider>(context, listen: false)
|
||||
.exams
|
||||
.firstWhere((t) => t.id == lesson.exam,
|
||||
orElse: () => Exam.fromJson({}));
|
||||
if (exam.id != "") {
|
||||
subtiles.add(LessonSubtile(
|
||||
type: LessonSubtileType.exam,
|
||||
title: exam.description != ""
|
||||
? exam.description
|
||||
: exam.mode?.description ?? "exam".i18n,
|
||||
onPressed: () => ExamView.show(exam, context: context),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// String description = '';
|
||||
// String room = '';
|
||||
|
||||
final cleanDesc = lesson.description
|
||||
.replaceAll(lesson.subject.name.specialChars().toLowerCase(), '');
|
||||
|
||||
// if (!swapDesc) {
|
||||
// if (cleanDesc != "") {
|
||||
// description = lesson.description;
|
||||
// }
|
||||
|
||||
// // Changed lesson Description
|
||||
// if (lesson.isChanged) {
|
||||
// if (lesson.status?.name == "Elmaradt") {
|
||||
// description = 'cancelled'.i18n;
|
||||
// } else if (lesson.substituteTeacher?.name != "") {
|
||||
// description = 'substitution'.i18n;
|
||||
// }
|
||||
// }
|
||||
|
||||
// room = lesson.room.replaceAll("_", " ");
|
||||
// } else {
|
||||
// description = lesson.room.replaceAll("_", " ");
|
||||
// }
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4.0, top: 7.0),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
child: Visibility(
|
||||
visible: lesson.subject.id != '' || lesson.isEmpty,
|
||||
replacement: Padding(
|
||||
padding: const EdgeInsets.only(top: 6.0),
|
||||
child: PanelTitle(title: Text(lesson.name)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: subtiles.isEmpty ? 0.0 : 12.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
minVerticalPadding: cleanDesc == '' ? 12.0 : 0.0,
|
||||
dense: true,
|
||||
onTap: onTap,
|
||||
// onLongPress: kDebugMode ? () => log(jsonEncode(lesson.json)) : null,
|
||||
visualDensity: VisualDensity.compact,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12.0)),
|
||||
title: Text(
|
||||
!lesson.isEmpty
|
||||
? lesson.subject.renamedTo ??
|
||||
lesson.subject.name.capital()
|
||||
: "empty".i18n,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16.5,
|
||||
color: fill
|
||||
? accent
|
||||
: AppColors.of(context)
|
||||
.text
|
||||
.withOpacity(!lesson.isEmpty ? 1.0 : 0.5),
|
||||
fontStyle: lesson.subject.isRenamed &&
|
||||
settingsProvider.renamedSubjectsItalics
|
||||
? FontStyle.italic
|
||||
: null),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Row(
|
||||
// children: [
|
||||
// Container(
|
||||
// padding: const EdgeInsets.symmetric(
|
||||
// horizontal: 6.0, vertical: 3.5),
|
||||
// decoration: BoxDecoration(
|
||||
// color: Theme.of(context)
|
||||
// .colorScheme
|
||||
// .secondary
|
||||
// .withOpacity(.15),
|
||||
// borderRadius: BorderRadius.circular(10.0),
|
||||
// ),
|
||||
// child: Text(
|
||||
// lesson.room,
|
||||
// style: TextStyle(
|
||||
// height: 1.1,
|
||||
// fontSize: 12.5,
|
||||
// fontWeight: FontWeight.w600,
|
||||
// color: Theme.of(context)
|
||||
// .colorScheme
|
||||
// .secondary
|
||||
// .withOpacity(.9),
|
||||
// ),
|
||||
// ),
|
||||
// )
|
||||
// ],
|
||||
// ),
|
||||
// if (cleanDesc != '')
|
||||
// const SizedBox(
|
||||
// height: 10.0,
|
||||
// ),
|
||||
if (cleanDesc != '')
|
||||
Text(
|
||||
cleanDesc,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.start,
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: fill ? accent.withOpacity(0.5) : null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// subtitle: description != ""
|
||||
// ? Text(
|
||||
// description,
|
||||
// style: const TextStyle(
|
||||
// fontWeight: FontWeight.w500,
|
||||
// fontSize: 14.0,
|
||||
// ),
|
||||
// maxLines: 1,
|
||||
// softWrap: false,
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
// )
|
||||
// : null,
|
||||
minLeadingWidth: 34.0,
|
||||
leading: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
RoundBorderIcon(
|
||||
color: fill ? accent : AppColors.of(context).text,
|
||||
width: 1.0,
|
||||
icon: SizedBox(
|
||||
width: 25,
|
||||
height: 25,
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 3.0),
|
||||
child: Text(
|
||||
lesson.lessonIndex + lessonIndexTrailing,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 17.5,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: fill ? accent : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Text(
|
||||
// lesson.lessonIndex + lessonIndexTrailing,
|
||||
// textAlign: TextAlign.center,
|
||||
// style: TextStyle(
|
||||
// fontSize: 30.0,
|
||||
// fontWeight: FontWeight.w600,
|
||||
// color: accent,
|
||||
// ),
|
||||
// ),
|
||||
|
||||
// Current lesson indicator
|
||||
Transform.translate(
|
||||
offset: const Offset(-22.0, -1.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: fillLeading
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.secondary
|
||||
.withOpacity(.3)
|
||||
: const Color(0x00000000),
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
boxShadow: [
|
||||
if (fillLeading)
|
||||
BoxShadow(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondary
|
||||
.withOpacity(.25),
|
||||
blurRadius: 6.0,
|
||||
)
|
||||
],
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
width: 4.0,
|
||||
height: double.infinity,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
trailing: !lesson.isEmpty
|
||||
? Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// if (!swapDesc)
|
||||
// SizedBox(
|
||||
// width: 52.0,
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.only(right: 6.0),
|
||||
// child: Text(
|
||||
// room,
|
||||
// textAlign: TextAlign.center,
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
// maxLines: 2,
|
||||
// style: TextStyle(
|
||||
// fontWeight: FontWeight.w500,
|
||||
// color: AppColors.of(context)
|
||||
// .text
|
||||
// .withOpacity(.75),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6.0, vertical: 3.5),
|
||||
decoration: BoxDecoration(
|
||||
color: fill
|
||||
? accent.withOpacity(.15)
|
||||
: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondary
|
||||
.withOpacity(.15),
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
child: Text(
|
||||
lesson.room,
|
||||
style: TextStyle(
|
||||
height: 1.1,
|
||||
fontSize: 12.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: fill
|
||||
? accent.withOpacity(0.9)
|
||||
: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondary
|
||||
.withOpacity(.9),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// xix alignment hack :p
|
||||
const Opacity(opacity: 0, child: Text("EE:EE")),
|
||||
Text(
|
||||
"${DateFormat("H:mm").format(lesson.start)}\n${DateFormat("H:mm").format(lesson.end)}",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: fill
|
||||
? accent.withOpacity(.9)
|
||||
: AppColors.of(context)
|
||||
.text
|
||||
.withOpacity(.9),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
),
|
||||
|
||||
// Homework & Exams
|
||||
...subtiles,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum LessonSubtileType { homework, exam, absence }
|
||||
|
||||
class LessonSubtile extends StatelessWidget {
|
||||
const LessonSubtile(
|
||||
{super.key, this.onPressed, required this.title, required this.type});
|
||||
|
||||
final Function()? onPressed;
|
||||
final String title;
|
||||
final LessonSubtileType type;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
IconData icon;
|
||||
Color iconColor = AppColors.of(context).text;
|
||||
|
||||
switch (type) {
|
||||
case LessonSubtileType.absence:
|
||||
icon = FeatherIcons.slash;
|
||||
iconColor = AppColors.of(context).red;
|
||||
break;
|
||||
case LessonSubtileType.exam:
|
||||
icon = FeatherIcons.file;
|
||||
break;
|
||||
case LessonSubtileType.homework:
|
||||
icon = FeatherIcons.home;
|
||||
break;
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: InkWell(
|
||||
onTap: onPressed,
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 4.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Center(
|
||||
child: SizedBox(
|
||||
width: 30.0,
|
||||
child:
|
||||
Icon(icon, color: iconColor.withOpacity(.75), size: 20.0),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 10.0, top: 2.0),
|
||||
child: Text(
|
||||
title.escapeHtml(),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.of(context).text.withOpacity(.65)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
33
refilc/lib/ui/widgets/lesson/lesson_tile.i18n.dart
Normal file
33
refilc/lib/ui/widgets/lesson/lesson_tile.i18n.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
import 'package:i18n_extension/i18n_extension.dart';
|
||||
|
||||
extension Localization on String {
|
||||
static final _t = Translations.byLocale("hu_hu") +
|
||||
{
|
||||
"en_en": {
|
||||
"empty": "Free period",
|
||||
"cancelled": "Cancelled",
|
||||
"substitution": "Substituted",
|
||||
"absence": "You were absent on this lesson",
|
||||
"exam": "Exam"
|
||||
},
|
||||
"hu_hu": {
|
||||
"empty": "Lyukasóra",
|
||||
"cancelled": "Elmarad",
|
||||
"substitution": "Helyettesítés",
|
||||
"absence": "Hiányoztál ezen az órán",
|
||||
"exam": "Dolgozat"
|
||||
},
|
||||
"de_de": {
|
||||
"empty": "Springstunde",
|
||||
"cancelled": "Abgesagte",
|
||||
"substitution": "Vertretene",
|
||||
"absence": "Sie waren in dieser Lektion nicht anwesend",
|
||||
"exam": "Prüfung"
|
||||
}
|
||||
};
|
||||
|
||||
String get i18n => localize(this, _t);
|
||||
String fill(List<Object> params) => localizeFill(this, params);
|
||||
String plural(int value) => localizePlural(value, this, _t);
|
||||
String version(Object modifier) => localizeVersion(modifier, this, _t);
|
||||
}
|
||||
130
refilc/lib/ui/widgets/message/message_tile.dart
Normal file
130
refilc/lib/ui/widgets/message/message_tile.dart
Normal file
@@ -0,0 +1,130 @@
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:refilc/utils/format.dart';
|
||||
import 'package:refilc_kreta_api/models/message.dart';
|
||||
import 'package:refilc_mobile_ui/common/profile_image/profile_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class MessageTile extends StatelessWidget {
|
||||
const MessageTile(
|
||||
this.message, {
|
||||
super.key,
|
||||
this.messages,
|
||||
this.padding,
|
||||
this.onTap,
|
||||
this.censored = false,
|
||||
});
|
||||
|
||||
final Message message;
|
||||
final List<Message>? messages;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final Function()? onTap;
|
||||
final bool censored;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Padding(
|
||||
padding: padding ?? const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: ListTile(
|
||||
onTap: onTap,
|
||||
visualDensity: VisualDensity.compact,
|
||||
contentPadding: const EdgeInsets.only(left: 8.0, right: 4.0),
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(14.0)),
|
||||
leading: !Provider.of<SettingsProvider>(context, listen: false)
|
||||
.presentationMode
|
||||
? ProfileImage(
|
||||
name: message.author,
|
||||
radius: 19.2,
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
censored: censored,
|
||||
isNotePfp: true,
|
||||
)
|
||||
: ProfileImage(
|
||||
name: "Béla",
|
||||
radius: 19.2,
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
censored: censored,
|
||||
isNotePfp: true,
|
||||
),
|
||||
title: censored
|
||||
? Wrap(
|
||||
children: [
|
||||
Container(
|
||||
width: 105,
|
||||
height: 15,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.of(context).text.withOpacity(.85),
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
!Provider.of<SettingsProvider>(context, listen: false)
|
||||
.presentationMode
|
||||
? message.author
|
||||
: "Béla",
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600, fontSize: 15.5),
|
||||
),
|
||||
),
|
||||
if (message.attachments.isNotEmpty)
|
||||
const Icon(FeatherIcons.paperclip, size: 16.0)
|
||||
],
|
||||
),
|
||||
subtitle: censored
|
||||
? Wrap(
|
||||
children: [
|
||||
Container(
|
||||
width: 150,
|
||||
height: 10,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.of(context).text.withOpacity(.45),
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Text(
|
||||
message.subject,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500, fontSize: 14.0),
|
||||
),
|
||||
trailing: censored
|
||||
? Wrap(
|
||||
children: [
|
||||
Container(
|
||||
width: 35,
|
||||
height: 15,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.of(context).text.withOpacity(.45),
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Text(
|
||||
message.date.format(context),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14.0,
|
||||
color: AppColors.of(context).text.withOpacity(.75),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
15
refilc/lib/utils/color.dart
Normal file
15
refilc/lib/utils/color.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
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;
|
||||
}
|
||||
80
refilc/lib/utils/format.dart
Normal file
80
refilc/lib/utils/format.dart
Normal file
@@ -0,0 +1,80 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:refilc_kreta_api/models/week.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:i18n_extension/i18n_widget.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'format.i18n.dart';
|
||||
|
||||
extension StringFormatUtils on String {
|
||||
String specialChars() => replaceAll("é", "e")
|
||||
.replaceAll("á", "a")
|
||||
.replaceAll("ó", "o")
|
||||
.replaceAll("ő", "o")
|
||||
.replaceAll("ö", "o")
|
||||
.replaceAll("ú", "u")
|
||||
.replaceAll("ű", "u")
|
||||
.replaceAll("ü", "u")
|
||||
.replaceAll("í", "i");
|
||||
|
||||
String capital() => isNotEmpty ? this[0].toUpperCase() + substring(1) : "";
|
||||
|
||||
String capitalize() => split(" ").map((w) => w.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() ?? htmlString;
|
||||
}
|
||||
|
||||
String limit(int max) {
|
||||
if (length <= max) return this;
|
||||
return '${substring(0, min(length, 14))}…';
|
||||
}
|
||||
}
|
||||
|
||||
extension DateFormatUtils on DateTime {
|
||||
String format(BuildContext context,
|
||||
{bool timeOnly = false, bool forceToday = false, bool weekday = false}) {
|
||||
// Time only
|
||||
if (timeOnly) return DateFormat("HH:mm").format(this);
|
||||
|
||||
DateTime now = DateTime.now();
|
||||
if (now.year == year && now.month == month && now.day == day) {
|
||||
if (hour == 0 && minute == 0 && second == 0 || forceToday)
|
||||
return "Today".i18n;
|
||||
return DateFormat("HH:mm").format(this);
|
||||
}
|
||||
if (now.year == year &&
|
||||
now.month == month &&
|
||||
now.subtract(const Duration(days: 1)).day == day)
|
||||
return "Yesterday".i18n;
|
||||
if (now.year == year &&
|
||||
now.month == month &&
|
||||
now.add(const Duration(days: 1)).day == day) return "Tomorrow".i18n;
|
||||
|
||||
String formatString;
|
||||
|
||||
// If date is current week, show only weekday
|
||||
if (Week.current().start.isBefore(this) &&
|
||||
Week.current().end.isAfter(this)) {
|
||||
formatString = "EEEE";
|
||||
} else {
|
||||
if (year == now.year) {
|
||||
formatString = "MMM dd.";
|
||||
} else {
|
||||
formatString = "yy/MM/dd";
|
||||
} // ex. 21/01/01
|
||||
|
||||
if (weekday) formatString += " (EEEE)"; // ex. (monday)
|
||||
}
|
||||
return DateFormat(formatString, I18n.of(context).locale.toString())
|
||||
.format(this)
|
||||
.capital();
|
||||
}
|
||||
}
|
||||
27
refilc/lib/utils/format.i18n.dart
Normal file
27
refilc/lib/utils/format.i18n.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
import 'package:i18n_extension/i18n_extension.dart';
|
||||
|
||||
extension Localization on String {
|
||||
static final _t = Translations.byLocale("hu_hu") +
|
||||
{
|
||||
"en_en": {
|
||||
"Today": "Today",
|
||||
"Yesterday": "Yesterday",
|
||||
"Tomorrow": "Tomorrow",
|
||||
},
|
||||
"hu_hu": {
|
||||
"Today": "Ma",
|
||||
"Yesterday": "Tegnap",
|
||||
"Tomorrow": "Holnap",
|
||||
},
|
||||
"de_de": {
|
||||
"Today": "Heute",
|
||||
"Yesterday": "Gestern",
|
||||
"Tomorrow": "Morgen",
|
||||
}
|
||||
};
|
||||
|
||||
String get i18n => localize(this, _t);
|
||||
String fill(List<Object> params) => localizeFill(this, params);
|
||||
String plural(int value) => localizePlural(value, this, _t);
|
||||
String version(Object modifier) => localizeVersion(modifier, this, _t);
|
||||
}
|
||||
42
refilc/lib/utils/jwt.dart
Normal file
42
refilc/lib/utils/jwt.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:refilc/models/user.dart';
|
||||
|
||||
class JwtUtils {
|
||||
static Map? decodeJwt(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] += "=";
|
||||
}
|
||||
|
||||
try {
|
||||
var payload = utf8.decode(base64Url.decode(parts[1]));
|
||||
return jsonDecode(payload);
|
||||
} catch (error) {
|
||||
// ignore: avoid_print
|
||||
print("ERROR: JwtUtils.decodeJwt: $error");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? getNameFromJWT(String jwt) {
|
||||
var jwtData = decodeJwt(jwt);
|
||||
return jwtData?["name"];
|
||||
}
|
||||
|
||||
static Role? getRoleFromJWT(String jwt) {
|
||||
var jwtData = decodeJwt(jwt);
|
||||
|
||||
switch (jwtData?["role"]) {
|
||||
case "Tanulo":
|
||||
return Role.student;
|
||||
case "Gondviselo":
|
||||
return Role.parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
6
refilc/lib/utils/platform.dart
Normal file
6
refilc/lib/utils/platform.dart
Normal file
@@ -0,0 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
class PlatformUtils {
|
||||
static bool get isDesktop => Platform.isWindows || Platform.isMacOS || Platform.isLinux;
|
||||
static bool get isMobile => !isDesktop;
|
||||
}
|
||||
44
refilc/lib/utils/reverse_search.dart
Normal file
44
refilc/lib/utils/reverse_search.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:refilc_kreta_api/models/absence.dart';
|
||||
import 'package:refilc_kreta_api/models/lesson.dart';
|
||||
import 'package:refilc_kreta_api/models/week.dart';
|
||||
import 'package:refilc_kreta_api/providers/timetable_provider.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ReverseSearch {
|
||||
static Future<Lesson?> getLessonByAbsence(
|
||||
Absence absence, BuildContext context) async {
|
||||
final timetableProvider =
|
||||
Provider.of<TimetableProvider>(context, listen: false);
|
||||
|
||||
List<Lesson> lessons = [];
|
||||
final week = Week.fromDate(absence.date);
|
||||
try {
|
||||
await timetableProvider.fetch(week: week);
|
||||
} catch (e) {
|
||||
log("[ERROR] getLessonByAbsence: $e");
|
||||
}
|
||||
lessons = timetableProvider.getWeek(week) ?? [];
|
||||
|
||||
// Find absence lesson in timetable
|
||||
Lesson lesson = lessons.firstWhere(
|
||||
(l) =>
|
||||
_sameDate(l.date, absence.date) &&
|
||||
l.subject.id == absence.subject.id &&
|
||||
l.lessonIndex == absence.lessonIndex.toString(),
|
||||
orElse: () => Lesson.fromJson({'isEmpty': true}),
|
||||
);
|
||||
|
||||
if (lesson.isEmpty) {
|
||||
return null;
|
||||
} else {
|
||||
return lesson;
|
||||
}
|
||||
}
|
||||
|
||||
// difference.inDays is not reliable
|
||||
static bool _sameDate(DateTime a, DateTime b) =>
|
||||
(a.year == b.year && a.month == b.month && a.day == b.day);
|
||||
}
|
||||
Reference in New Issue
Block a user