changed everything from filcnaplo to refilc finally

This commit is contained in:
Kima
2024-02-24 20:12:25 +01:00
parent 0d1c7b7143
commit 1171e3aaaf
655 changed files with 38728 additions and 44967 deletions

44
refilc/.gitignore vendored Normal file
View File

@@ -0,0 +1,44 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

36
refilc/.metadata Normal file
View File

@@ -0,0 +1,36 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "b0366e0a3f089e15fd89c97604ab402fe26b724c"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: b0366e0a3f089e15fd89c97604ab402fe26b724c
base_revision: b0366e0a3f089e15fd89c97604ab402fe26b724c
- platform: linux
create_revision: b0366e0a3f089e15fd89c97604ab402fe26b724c
base_revision: b0366e0a3f089e15fd89c97604ab402fe26b724c
- platform: macos
create_revision: b0366e0a3f089e15fd89c97604ab402fe26b724c
base_revision: b0366e0a3f089e15fd89c97604ab402fe26b724c
- platform: windows
create_revision: b0366e0a3f089e15fd89c97604ab402fe26b724c
base_revision: b0366e0a3f089e15fd89c97604ab402fe26b724c
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

3
refilc/README.md Normal file
View File

@@ -0,0 +1,3 @@
# refilc
Main lib

View File

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

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 735 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,3 @@
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5 0.138916C9.18678 0.138916 7.88642 0.397574 6.67317 0.900121C5.45991 1.40267 4.35752 2.13926 3.42893 3.06785C1.55357 4.94321 0.5 7.48675 0.5 10.1389C0.5 14.5589 3.37 18.3089 7.34 19.6389C7.84 19.7189 8 19.4089 8 19.1389V17.4489C5.23 18.0489 4.64 16.1089 4.64 16.1089C4.18 14.9489 3.53 14.6389 3.53 14.6389C2.62 14.0189 3.6 14.0389 3.6 14.0389C4.6 14.1089 5.13 15.0689 5.13 15.0689C6 16.5889 7.47 16.1389 8.04 15.8989C8.13 15.2489 8.39 14.8089 8.67 14.5589C6.45 14.3089 4.12 13.4489 4.12 9.63892C4.12 8.52892 4.5 7.63892 5.15 6.92892C5.05 6.67892 4.7 5.63892 5.25 4.28892C5.25 4.28892 6.09 4.01892 8 5.30892C8.79 5.08892 9.65 4.97892 10.5 4.97892C11.35 4.97892 12.21 5.08892 13 5.30892C14.91 4.01892 15.75 4.28892 15.75 4.28892C16.3 5.63892 15.95 6.67892 15.85 6.92892C16.5 7.63892 16.88 8.52892 16.88 9.63892C16.88 13.4589 14.54 14.2989 12.31 14.5489C12.67 14.8589 13 15.4689 13 16.3989V19.1389C13 19.4089 13.16 19.7289 13.67 19.6389C17.64 18.2989 20.5 14.5589 20.5 10.1389C20.5 8.8257 20.2413 7.52534 19.7388 6.31208C19.2362 5.09883 18.4997 3.99643 17.5711 3.06785C16.6425 2.13926 15.5401 1.40267 14.3268 0.900121C13.1136 0.397574 11.8132 0.138916 10.5 0.138916Z" fill="#243F76"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,3 @@
<svg width="19" height="13" viewBox="0 0 19 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.8333 11.375C16.7042 11.375 17.4088 10.6438 17.4088 9.75L17.4167 1.625C17.4167 0.73125 16.7042 0 15.8333 0H3.16667C2.29583 0 1.58333 0.73125 1.58333 1.625V9.75C1.58333 10.6438 2.29583 11.375 3.16667 11.375H0V13H19V11.375H15.8333ZM3.16667 1.625H15.8333V9.75H3.16667V1.625Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 403 B

View File

@@ -0,0 +1,3 @@
<svg width="23" height="22" viewBox="0 0 23 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.5 0C9.9898 0 8.49438 0.291587 7.09914 0.858113C5.7039 1.42464 4.43615 2.25501 3.36827 3.30181C1.2116 5.41593 0 8.28329 0 11.2731C0 16.2558 3.3005 20.4832 7.866 21.9825C8.441 22.0727 8.625 21.7233 8.625 21.4189V19.5137C5.4395 20.1901 4.761 18.0031 4.761 18.0031C4.232 16.6955 3.4845 16.346 3.4845 16.346C2.438 15.6471 3.565 15.6696 3.565 15.6696C4.715 15.7485 5.3245 16.8307 5.3245 16.8307C6.325 18.5442 8.0155 18.037 8.671 17.7664C8.7745 17.0336 9.0735 16.5376 9.3955 16.2558C6.8425 15.974 4.163 15.0045 4.163 10.7094C4.163 9.45813 4.6 8.45482 5.3475 7.65443C5.2325 7.37261 4.83 6.2002 5.4625 4.67834C5.4625 4.67834 6.4285 4.37396 8.625 5.82819C9.5335 5.58018 10.5225 5.45618 11.5 5.45618C12.4775 5.45618 13.4665 5.58018 14.375 5.82819C16.5715 4.37396 17.5375 4.67834 17.5375 4.67834C18.17 6.2002 17.7675 7.37261 17.6525 7.65443C18.4 8.45482 18.837 9.45813 18.837 10.7094C18.837 15.0158 16.146 15.9627 13.5815 16.2445C13.9955 16.594 14.375 17.2817 14.375 18.3301V21.4189C14.375 21.7233 14.559 22.084 15.1455 21.9825C19.711 20.4719 23 16.2558 23 11.2731C23 9.79269 22.7025 8.32678 22.1246 6.95907C21.5467 5.59135 20.6996 4.34862 19.6317 3.30181C18.5639 2.25501 17.2961 1.42464 15.9009 0.858113C14.5056 0.291587 13.0102 0 11.5 0Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,3 @@
<svg width="16" height="19" viewBox="0 0 16 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.33331 0C0.979692 0 0.64056 0.142984 0.390517 0.397498C0.140473 0.652012 0 0.997206 0 1.35714V19H2.66661V1.35714C2.66661 0.997206 2.52614 0.652012 2.2761 0.397498C2.02605 0.142984 1.68692 0 1.33331 0ZM15.7504 7.50229L3.33327 12.6187V1.63129L15.7504 6.74704C15.8241 6.77749 15.8873 6.82967 15.9318 6.89688C15.9762 6.96409 16 7.04329 16 7.12432C16 7.20536 15.9762 7.28455 15.9318 7.35176C15.8873 7.41897 15.8241 7.47115 15.7504 7.50161V7.50229Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 573 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.61217 0.411889C9.48161 0.281306 9.32661 0.17772 9.15602 0.107047C8.98543 0.0363749 8.80259 0 8.61794 0C8.43329 0 8.25045 0.0363749 8.07986 0.107047C7.90927 0.17772 7.75427 0.281306 7.62372 0.411889L5.96753 2.06861C5.6922 2.34397 5.50939 2.69821 5.44447 3.08217C5.37956 3.46613 5.43576 3.86077 5.60527 4.21134L3.03383 5.49501C2.68443 5.66957 2.37523 5.91499 2.12591 6.21564C1.87658 6.51629 1.69262 6.86558 1.58572 7.24125L0.0392041 12.6857C-0.0109899 12.8626 -0.0130219 13.0497 0.0333182 13.2277C0.0796583 13.4057 0.172689 13.568 0.302791 13.698C0.432894 13.828 0.595347 13.9208 0.773354 13.967C0.951362 14.0131 1.13846 14.0109 1.31532 13.9605L6.75673 12.4115C7.1321 12.3047 7.48112 12.1209 7.78159 11.8718C8.08205 11.6228 8.32738 11.3139 8.50195 10.9648L9.78603 8.39323C10.1367 8.56347 10.5317 8.62033 10.9161 8.55592C11.3006 8.4915 11.6554 8.30901 11.9315 8.03376L13.5881 6.37704C13.7187 6.24648 13.8223 6.09147 13.893 5.92087C13.9636 5.75028 14 5.56743 14 5.38277C14 5.19811 13.9636 5.01526 13.893 4.84466C13.8223 4.67407 13.7187 4.51906 13.5881 4.3885L9.6117 0.411889H9.61217ZM8.28638 1.07458C8.3299 1.031 8.38159 0.996437 8.43848 0.972851C8.49537 0.949266 8.55635 0.937127 8.61794 0.937127C8.67953 0.937127 8.74051 0.949266 8.7974 0.972851C8.85429 0.996437 8.90598 1.031 8.9495 1.07458L12.9259 5.05119C13.0138 5.13908 13.0631 5.25826 13.0631 5.38253C13.0631 5.50681 13.0138 5.62599 12.9259 5.71388L11.2688 7.37107L6.62926 2.73131L8.28638 1.07458ZM6.62972 4.71984L9.27756 7.36779C9.24199 7.40344 9.2124 7.44458 9.18992 7.48965L7.66402 10.5453C7.54772 10.7781 7.38424 10.9841 7.18398 11.1502C6.98373 11.3164 6.75108 11.439 6.50085 11.5103L1.82145 12.8418L4.69798 9.96513C4.99727 10.1077 5.3373 10.14 5.65807 10.0562C5.97883 9.97251 6.25971 9.77815 6.45112 9.50747C6.64253 9.23679 6.73218 8.90719 6.70424 8.57684C6.67629 8.24649 6.53256 7.93663 6.29839 7.70195C6.064 7.46672 5.75392 7.32205 5.42307 7.29357C5.09223 7.26509 4.76198 7.35464 4.49084 7.54635C4.2197 7.73807 4.02516 8.01958 3.9417 8.341C3.85824 8.66242 3.89124 9.00301 4.03485 9.30244L1.15739 12.18L2.48833 7.49714C2.55947 7.24677 2.68196 7.01396 2.84801 6.81353C3.01406 6.6131 3.22003 6.44944 3.4528 6.33298L6.50788 4.80748C6.55297 4.78487 6.59412 4.75512 6.62972 4.71938V4.71984Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -0,0 +1,3 @@
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.97258 0H8.02016C7.50351 0.000653543 6.99296 0.11297 6.52264 0.329438C6.05233 0.545906 5.63309 0.861535 5.29297 1.25522C4.95286 1.6489 4.6997 2.11155 4.55043 2.61225C4.40116 3.11294 4.35922 3.64013 4.42742 4.15855L4.48403 5.04316C4.12638 5.10009 3.80265 5.29026 3.57636 5.57636C3.35008 5.86247 3.23758 6.22384 3.26094 6.58958C3.2843 6.95531 3.44183 7.29901 3.70263 7.55324C3.96342 7.80746 4.30864 7.95385 4.67056 7.96369L4.75403 9.25754C4.84634 9.95556 5.1451 10.609 5.61106 11.1319C6.07701 11.6549 6.68836 12.0229 7.36476 12.1876C5.58243 12.5072 3.92957 13.3421 2.60565 14.5916V14.5843C1.42818 15.6953 0.553297 17.0946 0.0653227 18.6473V18.7282C0.0392656 18.8052 0.0174577 18.8837 0 18.9633L2.60347 18.9927L2.60492 19H15.3936L15.3958 18.9927L18 18.9633L17.984 18.9199C17.9565 18.8595 17.9398 18.7945 17.9347 18.7282V18.6473C17.444 17.0959 16.5696 15.6972 15.3944 14.5843V14.5916C14.0712 13.3434 12.4197 12.509 10.6389 12.1891C11.3162 12.025 11.9285 11.6571 12.3952 11.1338C12.8619 10.6105 13.161 9.95634 13.2532 9.25754L13.3323 7.96148C13.6852 7.93871 14.0176 7.78639 14.2674 7.53309C14.5171 7.27979 14.667 6.94293 14.6888 6.58573C14.7107 6.22853 14.603 5.87554 14.3859 5.59302C14.1689 5.31049 13.8575 5.11785 13.5102 5.05124L13.5653 4.15855C13.6336 3.64011 13.5918 3.11287 13.4426 2.61213C13.2933 2.11138 13.0402 1.64867 12.7001 1.25497C12.3599 0.861257 11.9406 0.545628 11.4703 0.3292C10.9999 0.112771 10.4893 0.00053424 9.97258 0Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

View File

@@ -0,0 +1,3 @@
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.16667 0C2.75081 0 2.33903 0.0819083 1.95484 0.241048C1.57064 0.400188 1.22155 0.633443 0.927495 0.927495C0.33363 1.52136 0 2.32681 0 3.16667V15.8333C0 16.6732 0.33363 17.4786 0.927495 18.0725C1.22155 18.3666 1.57064 18.5998 1.95484 18.759C2.33903 18.9181 2.75081 19 3.16667 19H15.8333C16.6732 19 17.4786 18.6664 18.0725 18.0725C18.6664 17.4786 19 16.6732 19 15.8333V3.16667C19 2.75081 18.9181 2.33903 18.759 1.95484C18.5998 1.57064 18.3666 1.22155 18.0725 0.927495C17.7785 0.633443 17.4294 0.400188 17.0452 0.241048C16.661 0.0819083 16.2492 0 15.8333 0H3.16667ZM1.26667 3.16667C1.26667 2.66276 1.46684 2.17948 1.82316 1.82316C2.17948 1.46684 2.66276 1.26667 3.16667 1.26667H15.8333C16.7371 1.26667 17.4927 1.89747 17.6858 2.74233L8.84767 11.6071C8.78933 11.6658 8.71034 11.6993 8.62761 11.7005C8.54487 11.7017 8.46496 11.6704 8.40497 11.6134L7.04457 10.3214C6.78224 10.0725 6.43338 9.93524 6.07172 9.93878C5.71007 9.94233 5.36396 10.0864 5.10657 10.3404L1.26667 14.1297V3.16667ZM1.26793 15.8979L5.84947 11.3582C5.90766 11.3005 5.98598 11.2675 6.06796 11.2663C6.14993 11.2652 6.22917 11.2958 6.289 11.3519L7.65067 12.6299C7.91356 12.8766 8.2618 13.0117 8.62224 13.007C8.98268 13.0023 9.32726 12.858 9.5836 12.6046L17.7333 4.5448V15.8333C17.7333 16.3372 17.5332 16.8205 17.1768 17.1768C16.8205 17.5332 16.3372 17.7333 15.8333 17.7333H3.16667C2.67397 17.7333 2.20056 17.5419 1.84631 17.1994C1.49206 16.857 1.28469 16.3903 1.26793 15.8979Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -0,0 +1,3 @@
<svg width="20" height="19" viewBox="0 0 20 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.2777 0.748963C18.8178 0.280979 18.1938 0.0123079 17.5392 0.000412436C16.8846 -0.011483 16.2513 0.234342 15.7749 0.68531L6.27841 9.67238L6.26855 9.67097L4.76926 11.1562C4.65745 11.2668 4.56485 11.3954 4.49532 11.5367C3.90325 11.5167 3.32013 11.6859 2.82983 12.0198C2.30986 12.3226 1.87751 12.7564 1.57544 13.2783C1.27337 13.8002 1.11201 14.3922 1.1073 14.9959C1.11849 15.2442 1.0807 15.4924 0.996102 15.726C0.911504 15.9597 0.781767 16.1742 0.614346 16.3573L0.0587138 16.88L0.0171647 17.1092C-0.0290688 17.3612 0.0188538 17.6216 0.151766 17.8404C0.284678 18.0592 0.49325 18.2212 0.737585 18.2952L2.55378 18.849L2.90589 18.9197C3.76665 19.0771 4.6536 19.0022 5.47605 18.7024C6.29849 18.4027 7.02692 17.889 7.58686 17.2138C7.99108 16.7301 8.22066 16.1254 8.24179 15.498C8.41784 15.4308 8.58404 15.3255 8.7277 15.184L9.28967 14.6267L9.30023 14.6479L19.2953 4.26755C19.7503 3.795 20.0033 3.1624 20 2.50501C19.9967 1.84762 19.7374 1.2169 19.2777 0.748963ZM8.99037 12.9349L7.01151 10.9221L16.7411 1.71436C16.9499 1.51759 17.227 1.41053 17.5132 1.41602C17.7995 1.42151 18.0723 1.53911 18.2735 1.74374C18.4747 1.94836 18.5883 2.22383 18.5901 2.5114C18.592 2.79896 18.4819 3.07586 18.2833 3.28305L8.99037 12.9349ZM3.15941 17.5293L2.92701 17.4861L1.81152 17.1466C2.24317 16.5719 2.48631 15.8767 2.5073 15.1571C2.48871 14.7622 2.58091 14.37 2.77339 14.0252C2.96587 13.6803 3.2509 13.3967 3.59603 13.2065C3.86776 13.0169 4.19727 12.9295 4.52679 12.9595C4.8563 12.9895 5.16478 13.135 5.39813 13.3706L6.42207 14.4025C6.66996 14.6525 6.81583 14.9866 6.83108 15.3391C6.84632 15.6916 6.72983 16.0371 6.50447 16.3078C6.10445 16.7906 5.58401 17.158 4.99631 17.3726C4.40861 17.5872 3.77472 17.6413 3.15941 17.5293Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,5 @@
<svg width="20" height="19" viewBox="0 0 20 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.58619 1.96552V0.854345C4.58619 0.38131 4.95178 0 5.40516 0C5.85854 0 6.22412 0.38131 6.22412 0.854345V1.96552H4.58619ZM12.7758 11.269C12.7758 11.1473 12.8242 11.0307 12.9102 10.9447C12.9962 10.8587 13.1128 10.8103 13.2345 10.8103H14.9379C15.0595 10.8103 15.1762 10.8587 15.2622 10.9447C15.3482 11.0307 15.3965 11.1473 15.3965 11.269V12.9724C15.3965 13.094 15.3482 13.2107 15.2622 13.2967C15.1762 13.3827 15.0595 13.431 14.9379 13.431H13.2345C13.1128 13.431 12.9962 13.3827 12.9102 13.2967C12.8242 13.2107 12.7758 13.094 12.7758 12.9724V11.269ZM4.25861 7.9931C4.25861 7.87147 4.30692 7.75482 4.39293 7.66881C4.47894 7.5828 4.59559 7.53448 4.71723 7.53448H6.42068C6.54231 7.53448 6.65896 7.5828 6.74497 7.66881C6.83098 7.75482 6.8793 7.87147 6.8793 7.9931V9.69655C6.8793 9.81818 6.83098 9.93484 6.74497 10.0208C6.65896 10.1069 6.54231 10.1552 6.42068 10.1552H4.71723C4.59559 10.1552 4.47894 10.1069 4.39293 10.0208C4.30692 9.93484 4.25861 9.81818 4.25861 9.69655V7.9931ZM4.91378 8.18966V9.5H6.22412V8.18966H4.91378ZM8.51723 7.9931C8.51723 7.87147 8.56555 7.75482 8.65155 7.66881C8.73756 7.5828 8.85421 7.53448 8.97585 7.53448H10.6793C10.8009 7.53448 10.9176 7.5828 11.0036 7.66881C11.0896 7.75482 11.1379 7.87147 11.1379 7.9931V9.69655C11.1379 9.81818 11.0896 9.93484 11.0036 10.0208C10.9176 10.1069 10.8009 10.1552 10.6793 10.1552H8.97585C8.85421 10.1552 8.73756 10.1069 8.65155 10.0208C8.56555 9.93484 8.51723 9.81818 8.51723 9.69655V7.9931ZM9.1724 8.18966V9.5H10.4827V8.18966H9.1724ZM12.7758 7.9931C12.7758 7.87147 12.8242 7.75482 12.9102 7.66881C12.9962 7.5828 13.1128 7.53448 13.2345 7.53448H14.9379C15.0595 7.53448 15.1762 7.5828 15.2622 7.66881C15.3482 7.75482 15.3965 7.87147 15.3965 7.9931V9.69655C15.3965 9.81818 15.3482 9.93484 15.2622 10.0208C15.1762 10.1069 15.0595 10.1552 14.9379 10.1552H13.2345C13.1128 10.1552 12.9962 10.1069 12.9102 10.0208C12.8242 9.93484 12.7758 9.81818 12.7758 9.69655V7.9931ZM13.431 8.18966V9.5H14.7414V8.18966H13.431ZM4.25861 11.269C4.25861 11.1473 4.30692 11.0307 4.39293 10.9447C4.47894 10.8587 4.59559 10.8103 4.71723 10.8103H6.42068C6.54231 10.8103 6.65896 10.8587 6.74497 10.9447C6.83098 11.0307 6.8793 11.1473 6.8793 11.269V12.9724C6.8793 13.094 6.83098 13.2107 6.74497 13.2967C6.65896 13.3827 6.54231 13.431 6.42068 13.431H4.71723C4.59559 13.431 4.47894 13.3827 4.39293 13.2967C4.30692 13.2107 4.25861 13.094 4.25861 12.9724V11.269ZM4.91378 11.4655V12.7759H6.22412V11.4655H4.91378ZM8.51723 11.269C8.51723 11.1473 8.56555 11.0307 8.65155 10.9447C8.73756 10.8587 8.85421 10.8103 8.97585 10.8103H10.6793C10.8009 10.8103 10.9176 10.8587 11.0036 10.9447C11.0896 11.0307 11.1379 11.1473 11.1379 11.269V12.9724C11.1379 13.094 11.0896 13.2107 11.0036 13.2967C10.9176 13.3827 10.8009 13.431 10.6793 13.431H8.97585C8.85421 13.431 8.73756 13.3827 8.65155 13.2967C8.56555 13.2107 8.51723 13.094 8.51723 12.9724V11.269ZM9.1724 11.4655V12.7759H10.4827V11.4655H9.1724ZM4.25861 14.5448C4.25861 14.4232 4.30692 14.3065 4.39293 14.2205C4.47894 14.1345 4.59559 14.0862 4.71723 14.0862H6.42068C6.54231 14.0862 6.65896 14.1345 6.74497 14.2205C6.83098 14.3065 6.8793 14.4232 6.8793 14.5448V16.2483C6.8793 16.3699 6.83098 16.4866 6.74497 16.5726C6.65896 16.6586 6.54231 16.7069 6.42068 16.7069H4.71723C4.59559 16.7069 4.47894 16.6586 4.39293 16.5726C4.30692 16.4866 4.25861 16.3699 4.25861 16.2483V14.5448ZM4.91378 14.7414V16.0517H6.22412V14.7414H4.91378ZM8.51723 14.5448C8.51723 14.4232 8.56555 14.3065 8.65155 14.2205C8.73756 14.1345 8.85421 14.0862 8.97585 14.0862H10.6793C10.8009 14.0862 10.9176 14.1345 11.0036 14.2205C11.0896 14.3065 11.1379 14.4232 11.1379 14.5448V16.2483C11.1379 16.3699 11.0896 16.4866 11.0036 16.5726C10.9176 16.6586 10.8009 16.7069 10.6793 16.7069H8.97585C8.85421 16.7069 8.73756 16.6586 8.65155 16.5726C8.56555 16.4866 8.51723 16.3699 8.51723 16.2483V14.5448ZM9.1724 14.7414V16.0517H10.4827V14.7414H9.1724Z" fill="white"/>
<path d="M4.58621 3.07672C4.58621 3.54975 4.95179 3.93106 5.40517 3.93106C5.85855 3.93106 6.22414 3.54975 6.22414 3.07672V1.96554H13.431V3.07672C13.431 3.54975 13.7966 3.93106 14.25 3.93106C14.7034 3.93106 15.069 3.54975 15.069 3.07672V1.96554H17.1924C18.5486 1.96554 19.6552 3.07344 19.6552 4.44537V15.8814C19.6552 17.598 18.2629 19 16.5372 19H3.11862C2.29194 18.9986 1.49951 18.6696 0.914954 18.0851C0.330399 17.5005 0.00138589 16.7081 0 15.8814V4.44537C0 3.07344 1.10724 1.95899 2.46279 1.96554H4.58621V3.07672ZM1.31034 5.89658V15.8814C1.31173 16.3605 1.5026 16.8195 1.84127 17.1583C2.17995 17.4971 2.63892 17.6881 3.11797 17.6897H14.0928V16.7069H13.2345C13.1128 16.7069 12.9962 16.6586 12.9102 16.5726C12.8242 16.4866 12.7759 16.3699 12.7759 16.2483V14.5449C12.7759 14.4232 12.8242 14.3066 12.9102 14.2206C12.9962 14.1346 13.1128 14.0862 13.2345 14.0862H14.9379C14.9823 14.0862 15.0264 14.0926 15.069 14.1052C15.5735 13.6702 16.2176 13.4309 16.8838 13.4311H18.3448V5.89658H1.31034ZM14.1 16.017C14.1331 15.5639 14.2771 15.1258 14.5193 14.7414H13.431V16.0518H14.098L14.1 16.017ZM14.7479 17.5541L18.2158 14.0862H16.8838C16.3179 14.088 15.7756 14.3135 15.3754 14.7137C14.9752 15.1139 14.7497 15.6562 14.7479 16.2221V17.5541Z" fill="white"/>
<path d="M13.431 0.854345V1.96552H15.069V0.854345C15.069 0.38131 14.7034 0 14.25 0C13.7966 0 13.431 0.38131 13.431 0.854345Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.63347 1.46773e-08C4.17718 1.46773e-08 2.15934 1.9315 1.9387 4.3981C1.66876 4.46542 1.43013 4.6275 1.26399 4.85636C1.09784 5.08522 1.01461 5.36652 1.02863 5.65173C1.04265 5.93694 1.15306 6.20817 1.34081 6.41864C1.52855 6.62911 1.78186 6.76561 2.05704 6.8046C2.099 6.97812 2.14991 7.14776 2.20932 7.31353L2.18622 7.30965C2.06688 7.28651 1.94628 7.27097 1.82508 7.26312H1.80057C1.49035 7.27282 1.31025 7.5234 1.25273 7.72988C1.19144 7.94993 1.22916 8.18792 1.32062 8.41621L1.35834 8.51024L0.640782 8.70217L0.637482 8.70314C0.400811 8.76809 0.0575906 8.92126 0.0057304 9.32016C-0.0140708 9.47187 0.0222314 9.60128 0.0420326 9.66381C0.0514617 9.69483 0.0627766 9.72536 0.0703199 9.7462L0.0722057 9.75153L0.0882353 9.79855C0.166497 10.0603 0.331035 10.3206 0.519618 10.5489C0.897617 11.0007 1.36672 11.3626 1.89439 11.6094L1.64357 11.8372C1.5589 11.9114 1.4921 12.0048 1.44839 12.11C1.39862 12.2346 1.38969 12.3724 1.42293 12.5027C1.44839 12.6054 1.49506 12.6868 1.52335 12.7338C1.54456 12.7687 1.57191 12.8099 1.59265 12.841L1.60963 12.8662C1.78642 13.1347 2.09805 13.3901 2.46155 13.5855C2.83447 13.7866 3.30074 13.9451 3.81934 13.9882C4.816 14.0716 5.96164 13.7241 6.96066 12.5419C7.96014 13.7246 9.10578 14.0716 10.102 13.9882C10.5761 13.9484 11.0377 13.8116 11.4598 13.5859C11.8233 13.3901 12.1349 13.1347 12.3117 12.8662L12.3287 12.841C12.3494 12.8099 12.3772 12.7683 12.398 12.7338C12.4428 12.6625 12.4766 12.5845 12.4984 12.5027C12.5316 12.3724 12.5227 12.2346 12.4729 12.11C12.4293 12.0051 12.3627 11.9118 12.2782 11.8377L12.0529 11.6326C12.6017 11.386 13.0896 11.0156 13.4804 10.5489C13.669 10.3206 13.8335 10.0603 13.9118 9.79855L13.9278 9.75153L13.9297 9.7462C13.9373 9.72536 13.9481 9.69483 13.958 9.66381C13.9773 9.60079 14.0141 9.47187 13.9943 9.32016C13.9425 8.92126 13.5992 8.76809 13.3626 8.70314L13.3593 8.70217L12.6412 8.51024L12.679 8.41621C12.7704 8.1884 12.8086 7.94993 12.7473 7.72988C12.6898 7.5234 12.5092 7.27282 12.199 7.26312H12.1754C12.0584 7.27055 11.9418 7.28544 11.8266 7.30771C11.8893 7.13128 11.9425 6.95001 11.9859 6.76486C12.2221 6.69215 12.4306 6.54602 12.5828 6.34656C12.735 6.14709 12.8233 5.90408 12.8357 5.6509C12.8481 5.39773 12.784 5.14681 12.6521 4.93264C12.5202 4.71847 12.327 4.55155 12.0991 4.45481C12.0032 3.24113 11.4666 2.10899 10.5958 1.28357C9.72506 0.45814 8.584 -9.46918e-05 7.39958 1.46773e-08H6.63347ZM11.1538 4.46984C10.9539 4.56225 10.7825 4.70948 10.6586 4.89538C10.5347 5.08128 10.4629 5.29868 10.4513 5.52373C10.4396 5.74877 10.4884 5.97279 10.5923 6.1712C10.6963 6.36961 10.8514 6.53476 11.0406 6.64853C10.8238 7.47062 10.3497 8.19645 9.69163 8.71399C9.03357 9.23153 8.22811 9.512 7.39958 9.5121H6.63347C5.78749 9.51203 4.96607 9.21968 4.30113 8.68199C3.63619 8.1443 3.16631 7.39247 2.96695 6.54723C3.11456 6.42609 3.23205 6.27062 3.30974 6.09363C3.38744 5.91665 3.4231 5.72321 3.4138 5.52927C3.4045 5.33533 3.3505 5.14644 3.25625 4.97817C3.162 4.8099 3.0302 4.66707 2.87171 4.56144C2.94183 3.58507 3.3686 2.67211 4.06635 2.00583C4.7641 1.33956 5.68117 0.969298 6.63347 0.969386H7.39958C8.33646 0.969275 9.23984 1.32765 9.93382 1.97471C10.6278 2.62178 11.0627 3.51122 11.1538 4.46984ZM10.7799 9.01335L12.9755 9.60128C12.935 9.68513 12.8661 9.79516 12.7615 9.92118C12.62 10.0928 12.4343 10.2702 12.224 10.4253C11.8718 10.6851 11.5107 10.8421 11.2113 10.8697C10.7876 10.485 10.3623 10.1021 9.93555 9.721C10.2425 9.51928 10.5259 9.28183 10.7799 9.01335ZM9.06665 10.1698C9.09494 10.2357 9.13784 10.2963 9.19536 10.3458C9.45466 10.5692 10.7201 11.716 11.4706 12.3975C11.3857 12.4896 11.2377 12.6107 11.0227 12.7271C10.7127 12.8925 10.3737 12.9928 10.0256 13.0222C9.28729 13.0838 8.36324 12.8293 7.51273 11.7058L7.5099 11.7024C7.4802 11.6636 7.41467 11.5764 7.32509 11.5105C7.21945 11.4319 7.09092 11.3925 6.96066 11.399C6.83039 11.3925 6.70186 11.4319 6.59622 11.5105C6.50664 11.5764 6.44064 11.6636 6.41141 11.7024L6.40905 11.7058C5.55807 12.8293 4.63402 13.0838 3.89619 13.0222C3.54787 12.9927 3.20875 12.8922 2.89859 12.7266C2.6836 12.6112 2.53557 12.4891 2.4507 12.3975L3.07303 11.8328C3.09377 11.8304 3.11499 11.828 3.13573 11.8246L3.1296 11.7809C3.65861 11.2991 4.19074 10.8208 4.72595 10.3463C4.7922 10.2895 4.84195 10.215 4.86975 10.131C5.43028 10.3633 6.02912 10.4823 6.63347 10.4815H7.39958C7.96904 10.4822 8.53389 10.3766 9.06665 10.1698ZM4.03338 9.67835C3.78775 9.895 3.25265 10.3778 2.71896 10.861C2.43373 10.8179 2.10041 10.6657 1.77511 10.4248C1.57712 10.2796 1.39669 10.1108 1.23765 9.92166C1.15436 9.82394 1.08267 9.71639 1.02408 9.60128L3.24605 9.00559C3.48319 9.25763 3.74721 9.4835 4.03338 9.67786V9.67835ZM2.31822 12.2123L2.31917 12.2147C2.31883 12.2139 2.31852 12.2131 2.31822 12.2123ZM11.6021 12.2123L11.6012 12.2147C11.6015 12.2139 11.6019 12.2131 11.6021 12.2123Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.65 11.3L8 5.65L13.65 0L19.3 5.65L13.65 11.3ZM0 9.3V1.3H8V9.3H0ZM10 19.3V11.3H18V19.3H10ZM0 19.3V11.3H8V19.3H0Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 243 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

7
refilc/build-ipa.sh Normal file
View File

@@ -0,0 +1,7 @@
#!/bin/sh
flutter clean
dart pub get
flutter doctor -v
flutter build ipa --release --dart-define=APPVER=$(cat pubspec.yaml | grep version: | cut -d' ' -f2 | cut -d+ -f1) --no-tree-shake-icons

3
refilc/build.sh Normal file
View File

@@ -0,0 +1,3 @@
#!/bin/sh
flutter build apk --release --dart-define=APPVER=$(cat pubspec.yaml | grep version: | cut -d' ' -f2 | cut -d+ -f1) --no-tree-shake-icons

357
refilc/lib/api/client.dart Normal file
View 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
View 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
View File

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

View 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();
}
}
}

View 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);
}
}

View 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();
}

View 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();
}
}

View 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();
}
}

View 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();
}
}
}

View 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();
});
}

View 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;
}
}

View 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
View 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);
}
}

View 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");
}
}

View 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()));
}
}

View 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]);
}
}

View 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(",");
}
}

View 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;
}
}

View 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;
}
}

View 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!);
}
}
}

View 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);
}

View 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;
}
});
}

View 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);
}
}

View 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;
}
}

View 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() ?? "?";
}
}

View 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;
}
}

View 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
View 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
View 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'),
);
}
}

View 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();
}

View File

@@ -0,0 +1 @@
enum IconPack { material, cupertino }

View 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,
);
}
}

View 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
}

View 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;
}

View 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,
};
}

View 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();
}
}

View 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']),
);
}
}

View 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)),
};
}
}

View 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(),
);
}
}

Some files were not shown because too many files have changed in this diff Show More