├── .gitignore ├── LICENSE ├── README.md ├── boostan.pro ├── header ├── base │ ├── network.h │ └── settings.h ├── controls │ └── scheduletable.h ├── handlers │ ├── abstractxmldatahandler.h │ ├── accounthandler.h │ ├── briefinfohandler.h │ ├── captchahandler.h │ ├── courseschedulehandler.h │ ├── handler.h │ ├── inithandler.h │ ├── loginhandler.h │ ├── offeredcoursehandler.h │ └── scoreshandler.h ├── helpers │ ├── commonmacros.h │ ├── constants.h │ ├── errors.h │ └── logger.h └── models │ └── offeredcoursemodel.h ├── qml ├── Controls │ ├── ClickableText.qml │ ├── Icon.qml │ ├── LoadingAnimationColor.qml │ ├── LoadingAnimationPulse.qml │ ├── MyButton.qml │ ├── MyComboBox.qml │ ├── MySwitch.qml │ ├── MyTableView.qml │ ├── MyTextInput.qml │ ├── Notifier.qml │ ├── PageBase.qml │ ├── Plot.qml │ ├── ScheduleTable.qml │ ├── ScreenShot.qml │ └── ViewManager.qml ├── Helpers │ ├── ErrorHandler.qml │ ├── ErrorRectangle.qml │ ├── SettingsPage │ │ ├── About.qml │ │ ├── BoostanSettings.qml │ │ └── GolestanSettings.qml │ ├── SideBar.qml │ ├── SideBarDelegate.qml │ └── SideBarItem.qml ├── Pages │ ├── DashboardPage.qml │ ├── ErrorPage.qml │ ├── LoginPage.qml │ ├── OfferedCoursePage.qml │ ├── ScoresPage.qml │ └── SettingsPage.qml ├── fonts │ ├── Mj_Afsoon.ttf │ ├── Tanha.ttf │ ├── Vazir-Regular.ttf │ └── icons.ttf ├── main.qml ├── pics │ ├── boostan-logo.svg │ ├── boostan.ico │ ├── error-logo.svg │ ├── icon-boy.svg │ └── icon-girl.svg ├── qml.qrc └── qtquickcontrols2.conf └── source ├── base ├── network.cpp └── settings.cpp ├── controls └── scheduletable.cpp ├── handlers ├── abstractxmldatahandler.cpp ├── accounthandler.cpp ├── briefinfohandler.cpp ├── captchahandler.cpp ├── courseschedulehandler.cpp ├── handler.cpp ├── inithandler.cpp ├── loginhandler.cpp ├── offeredcoursehandler.cpp └── scoreshandler.cpp ├── helpers ├── errors.cpp └── logger.cpp ├── main.cpp └── models └── offeredcoursemodel.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | *.pro.user 2 | *.html 3 | test/ 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Boostan-Desktop 2 | 3 | There is a university affairs automation software in Iran called Golestan, which is highly legacy, outdated, and unusable. Until a few years ago, they couldn't even support browsers other than Internet Explorer. 4 | To solve this problem, I developed free software called Boostan (Boostan and Golestan have the same meaning, "garden") that is more beautiful, easier to use, and less likely to cause headaches. 5 | 6 | Boostan is actually a desktop UI for Golestan. It "simulates" weird browser requests to the Golestan server, processes its weird response, and renders them in a reasonable way for the user(I use the word "simulate" because we are actually doing this. There is no API or something like that. I somehow reverse-engineered the requests and responses to understand how to send/receive proper data). 7 | 8 | Aside from Golestan's basic functionalities, Boostan provides much more features. 9 | 10 | Boostan is written in C++, Qt, and QML. 11 | 12 | Demo: 13 | 14 | 15 | https://user-images.githubusercontent.com/24620298/190890888-47ed93a7-f91c-4ed4-9c6b-7b357e38b021.mp4 16 | 17 | 18 | 19 | # Acknowledgement 20 | 21 | - [Vida Azadi](mailto:azadivida@gmail.com): UI design 22 | - [Amir TBO](https://t.me/TheBurningOne): Assisting with the design of UI for the login page and Boostan icon. 23 | -------------------------------------------------------------------------------- /boostan.pro: -------------------------------------------------------------------------------- 1 | QT += quick network core charts 2 | 3 | CONFIG += c++17 4 | #CONFIG -= app_bundle 5 | 6 | # The following define makes your compiler emit warnings if you use 7 | # any Qt feature that has been marked deprecated (the exact warnings 8 | # depend on your compiler). Please consult the documentation of the 9 | # deprecated API in order to know how to port your code away from it. 10 | DEFINES += QT_DEPRECATED_WARNINGS 11 | 12 | # You can also make your code fail to compile if it uses deprecated APIs. 13 | # In order to do so, uncomment the following line. 14 | # You can also select to disable deprecated APIs only up to a certain version of Qt. 15 | #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 16 | 17 | SOURCES += \ 18 | source/handlers/accounthandler.cpp \ 19 | source/helpers/logger.cpp \ 20 | source/models/offeredcoursemodel.cpp \ 21 | source/handlers/scoreshandler.cpp \ 22 | source/handlers/abstractxmldatahandler.cpp \ 23 | source/handlers/courseschedulehandler.cpp \ 24 | source/handlers/offeredcoursehandler.cpp \ 25 | source/handlers/briefinfohandler.cpp \ 26 | source/handlers/captchahandler.cpp \ 27 | source/handlers/handler.cpp \ 28 | source/handlers/inithandler.cpp \ 29 | source/handlers/loginhandler.cpp \ 30 | source/base/settings.cpp \ 31 | source/base/network.cpp \ 32 | source/helpers/errors.cpp \ 33 | source/controls/scheduletable.cpp \ 34 | source/main.cpp 35 | 36 | HEADERS += \ 37 | header/handlers/abstractxmldatahandler.h \ 38 | header/handlers/accounthandler.h \ 39 | header/handlers/courseschedulehandler.h \ 40 | header/handlers/offeredcoursehandler.h \ 41 | header/handlers/briefinfohandler.h \ 42 | header/handlers/captchahandler.h \ 43 | header/handlers/handler.h \ 44 | header/handlers/inithandler.h \ 45 | header/handlers/loginhandler.h \ 46 | header/handlers/scoreshandler.h \ 47 | header/helpers/commonmacros.h \ 48 | header/helpers/errors.h \ 49 | header/helpers/constants.h \ 50 | header/base/network.h \ 51 | header/base/settings.h \ 52 | header/models/offeredcoursemodel.h \ 53 | header/controls/scheduletable.h \ 54 | header/helpers/logger.h 55 | 56 | RESOURCES += qml/qml.qrc 57 | 58 | # Additional import path used to resolve QML modules in Qt Creator's code model 59 | QML_IMPORT_PATH = 60 | 61 | # Additional import path used to resolve QML modules just for Qt Quick Designer 62 | QML_DESIGNER_IMPORT_PATH = 63 | 64 | # Default rules for deployment. 65 | qnx: target.path = /tmp/$${TARGET}/bin 66 | else: unix:!android: target.path = /opt/$${TARGET}/bin 67 | !isEmpty(target.path): INSTALLS += target 68 | 69 | DISTFILES += \ 70 | test/* 71 | -------------------------------------------------------------------------------- /header/base/network.h: -------------------------------------------------------------------------------- 1 | #ifndef NETWORK_H 2 | #define NETWORK_H 3 | 4 | /* 5 | * Class: Network 6 | * Files: network.h and network.cpp 7 | * This class is a wrapper on ther QNetwork classes. 8 | */ 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include 17 | 18 | #include "settings.h" 19 | #include "../helpers/logger.h" 20 | 21 | class Network : public QObject 22 | { 23 | Q_OBJECT 24 | 25 | private: 26 | QUrl url; 27 | QHash headers; 28 | QNetworkAccessManager netaccman; 29 | 30 | // actually sets header for a request 'req' from 'headers' 31 | void setRequestHeader(QNetworkRequest& req); 32 | 33 | public: 34 | explicit Network(QObject *parent = nullptr); 35 | explicit Network(QUrl url, QObject* parent = nullptr); 36 | 37 | // return a QHash which keys is the header titles and values are header values 38 | QHash getHeaders() const; 39 | // replace 'value' as new headers 40 | void setHeaders(const QHash &value); 41 | 42 | // return url 43 | QUrl getUrl() const; 44 | // set url 45 | void setUrl(const QUrl &value); 46 | 47 | // add a single header to headers with title 'header' and value 'value' 48 | void addHeader(const QByteArray& header, const QByteArray& value); 49 | // send a POST request to 'url' with data 'data' 50 | bool post(const QByteArray& data); 51 | // send a GET request to 'url' 52 | bool get(); 53 | 54 | signals: 55 | void complete(QNetworkReply& data); 56 | 57 | private slots: 58 | // slot that connects to QNetworkReply finished 59 | void finished(QNetworkReply* reply); 60 | }; 61 | 62 | #endif // NETWORK_H 63 | -------------------------------------------------------------------------------- /header/base/settings.h: -------------------------------------------------------------------------------- 1 | #ifndef SETTINGS_H 2 | #define SETTINGS_H 3 | 4 | /* 5 | * Class: Settings 6 | * Files: settings.h and settings.cpp 7 | * This class is a wrapper on QSettings and suppose to check if settings are available 8 | * and set settings value and retrieve them 9 | */ 10 | 11 | #include 12 | #include 13 | #include 14 | #include "../helpers/constants.h" 15 | 16 | class Settings : public QObject 17 | { 18 | Q_OBJECT 19 | private: 20 | inline static QSettings settings{Constants::settings_path, QSettings::IniFormat}; 21 | inline static QString prefix_uid; 22 | inline static QString prefix_url; 23 | 24 | public: 25 | Settings(); 26 | // check if settings are available 27 | static bool checkSettings(); 28 | 29 | public slots: 30 | // set value 'value' to key 'key' 31 | static void setValue(const QString key, const QString value, const bool raw_key = false); 32 | // get value of key 'key'. raw_key defines if we should include prefixes in key or not. 33 | static QVariant getValue(const QString key, const bool raw_key = false); 34 | // set uid to prefix_uid 35 | static void setPrefixUid(const QString uid); 36 | // set url to prefix_url 37 | static void setPrefixUrl(const QString url); 38 | }; 39 | 40 | #endif // SETTINGS_H 41 | -------------------------------------------------------------------------------- /header/controls/scheduletable.h: -------------------------------------------------------------------------------- 1 | #ifndef SCHEDULETABLE_H 2 | #define SCHEDULETABLE_H 3 | 4 | /* 5 | * Class: ScheduleTable 6 | * Files: scheduletable.h and scheduletable.cpp 7 | * This is a Back-end class for QML component named ScheduleTable 8 | * The main job of this class is to: 9 | * 1- Serialize/deSerialize the model. 2- Find collisions in model 10 | */ 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | class ScheduleTable : public QObject 18 | { 19 | Q_OBJECT 20 | 21 | private: 22 | 23 | // Container that we store our model in 24 | QHash model_data; 25 | 26 | public: 27 | 28 | // Type of collisions 29 | enum collision_errors 30 | { 31 | NoCollision = 0, 32 | ExamCollision, 33 | TimeCollision, 34 | ExamWarning 35 | }; 36 | 37 | explicit ScheduleTable(QObject *parent = nullptr); 38 | 39 | // calculate the corresponding row(in scheduleTable component) for the given day 40 | static int calculateScheduleRow (const QString& day); 41 | // calculate the corresponding column(in scheduleTable component) for the given hour 42 | static float calculateScheduleColumn (const QString& hour); 43 | // calculate the the duration of a course in hour format. 44 | static float calculateScheduleLen (const QString& hour, const float start_column); 45 | 46 | public slots: 47 | // add element to model_data 48 | void addEelement (const QString uid, QVariantMap element); 49 | // remove element with key 'uid' from model_data 50 | void removeEelement (const QString& uid); 51 | // check for collisions between element and other members of model_data 52 | // return a list of members with collision and the collision type. 53 | QVariantList checkCollision (const QVariantMap element) const; 54 | // clear model_data 55 | void clearAll(); 56 | // serialize the model_data into a string(Base64 format) 57 | QString serialize() const; 58 | 59 | // parse 'data'(Base64 format) and return a container in type of model_data 60 | static QHash deserialize(const QString& data); 61 | 62 | /** specialized function member for storing courses **/ 63 | 64 | // Generate a Unique id for a course 65 | static QString getUid (const int course_number, const int course_group); 66 | static QString getUid (const QString& course_number, const QString& course_group); 67 | 68 | // return a string which in have list of course names separated with '
' 69 | QString getCourseNames (const QVariantList uids) const; 70 | // set 'warning_list' as a list of warnings for course with uid of 'uid' 71 | void setCourseWarnings (const QString uid, const QVariantList warning_list); 72 | }; 73 | 74 | #endif // SCHEDULETABLE_H 75 | -------------------------------------------------------------------------------- /header/handlers/abstractxmldatahandler.h: -------------------------------------------------------------------------------- 1 | #ifndef ABSTRACTXMLDATAHANDLER_H 2 | #define ABSTRACTXMLDATAHANDLER_H 3 | 4 | /* 5 | * Class: AbstractXmlDataHandler 6 | * Files: abstractxmldatahandler.h and abstractxmldatahandler.cpp 7 | * This is a ABSTRACT class for being drived by classes that tend to parse XML data 8 | * which recieved from Golestan system. 9 | 10 | * Note: You MUST implement 'getIsEmpty' which determines the empty-ness of a DS. 11 | */ 12 | 13 | #include "handler.h" 14 | #include 15 | 16 | class AbstractXmlDataHandler : public Handler 17 | { 18 | Q_OBJECT 19 | 20 | private: 21 | // this property indicate that if our model is totally empty or not 22 | Q_PROPERTY(bool is_empty READ getIsEmpty NOTIFY isEmptyChanged) 23 | 24 | protected: 25 | // REGEX pattern for extracting the XML data withing another data's 26 | const QString _xmldata_pattern {QStringLiteral("[\\W\\w]+<\\/Root>")}; 27 | bool _is_empty; 28 | 29 | public: 30 | AbstractXmlDataHandler(); 31 | // set state to is_empty 32 | void setIsEmpty(bool state); 33 | 34 | virtual bool getIsEmpty() const = 0; 35 | 36 | signals: 37 | void isEmptyChanged(); 38 | }; 39 | 40 | #endif // ABSTRACTXMLDATAHANDLER_H 41 | -------------------------------------------------------------------------------- /header/handlers/accounthandler.h: -------------------------------------------------------------------------------- 1 | #ifndef ACCOUNTHANDLER_H 2 | #define ACCOUNTHANDLER_H 3 | 4 | /* 5 | * Class: AccountHandler 6 | * Files: accounthandler.h, accounthandler.cpp 7 | * The task of this class is to change the Golestan account credentials 8 | */ 9 | 10 | #include "handler.h" 11 | #include "../helpers/commonmacros.h" 12 | 13 | class AccountHandler : public Handler 14 | { 15 | Q_OBJECT 16 | 17 | private: 18 | const QString _account_url {QStringLiteral("/Forms/F0217_PROCESS_SCROLDPASS/F0217_01_PROCESS_SCROLDPASS_Dat.aspx?r=0.8555344031558193&fid=0%3b11160&b=0&l=0&&lastm=20190220153948&tck=")}; 19 | QString _username, _password, _new_password, _new_username; 20 | 21 | // request golestan to get the tokens 22 | void requestTokens (); 23 | // request the golestan to change the credentials 24 | void requestChangeCreds (); 25 | 26 | // check if credential changing proccess is successfull 27 | bool isChangeSuccess (const QString& data); 28 | 29 | private slots: 30 | // parse the response to requestTokens() 31 | void parseTokens (QNetworkReply& reply); 32 | // parse the response to requestChangeCreds() 33 | void parseChangeCreds (QNetworkReply& reply); 34 | 35 | public: 36 | AccountHandler(); 37 | 38 | public slots: 39 | // change the credentials of account with username {{username}} and password {{password}} 40 | // and change them to {{new_username}} and {{new_password}} 41 | void changeCreds (const QString username, const QString password, 42 | const QString new_password, const QString new_username = QString()); 43 | 44 | }; 45 | 46 | #endif // ACCOUNTHANDLER_H 47 | -------------------------------------------------------------------------------- /header/handlers/briefinfohandler.h: -------------------------------------------------------------------------------- 1 | #ifndef DASHBOARDHANDLER_H 2 | #define DASHBOARDHANDLER_H 3 | 4 | /* 5 | * Class: BriefInfoHandler 6 | * Files: briefinfohandler.h, briefinfohandler.cpp 7 | * The task of this class is to request to the "general information" section of Golestan, 8 | * parse that information which includes semesters averages and identity informations 9 | * then create a neat structure for exposing those informations to QML 10 | */ 11 | 12 | #include "handler.h" 13 | #include 14 | #include 15 | 16 | class BriefInfoHandler : public Handler 17 | { 18 | Q_OBJECT 19 | private: 20 | /** Properties **/ 21 | Q_PROPERTY(QVariantMap briefInfo READ getStudentInfo NOTIFY studentInfoChanged) 22 | Q_PROPERTY(int currentYear READ getCurrentYear NOTIFY currentYearChanged) 23 | 24 | // url of section we gonna send our requests. 25 | static inline QString _user_info_url {QStringLiteral("/Forms/F1802_PROCESS_MNG_STDJAMEHMON/F1802_01_PROCESS_MNG_STDJAMEHMON_Dat.aspx?r=0.9638806400489983&fid=0;12310&b=0&l=0&&lastm=20180201081222&tck=")}; 26 | 27 | // this is the keys of a QVariantMap that we wanna expose to qml 28 | /// WHY I CHOOSED STD::VECTOR ??? :/ 29 | const std::vector _info_title {QStringLiteral("id"), QStringLiteral("field"), QStringLiteral("studyType"), QStringLiteral("average"), QStringLiteral("passedUnits")}; 30 | 31 | // Container which our information would stored in. 32 | QVariantMap _student_info; 33 | QList _passed_semesters; // passed semesters 34 | QStringList _passed_semesters_avg; // semesters averages 35 | QLocale _locale; 36 | int _current_year; // current semester 37 | 38 | /** Functions **/ 39 | 40 | // request validators 41 | bool requestTokens(); 42 | // request student id 43 | bool requestStuId(); 44 | // request student information brief 45 | bool requestBriefInfo(); 46 | 47 | /* 48 | * return student general information as a QVariantMap 49 | * with the keys 'info_title' and related values for using in QML 50 | */ 51 | QVariantMap getStudentInfo() const; 52 | // extract general information from 'response' and fill 'student_info' 53 | bool extractStudentInfo (const QString& response); 54 | // extract semester averages and fill 'passed_semesters' and 'passed_semester_avg' 55 | bool extractStudentAvgs (const QString& response); 56 | // extract student id from 'response' and fill related index in student_info 57 | QString extractStuId(const QString& response); 58 | 59 | public: 60 | // index of each key in info_title 61 | enum info_index { 62 | Index_START = -1, // used to determine the start of enum in loops 63 | Index_Id, 64 | Index_Field, 65 | Index_StudyType, 66 | Index_TotalAvg, 67 | Index_Passed, 68 | Index_END // used to determine the end of enum in loops 69 | }; 70 | 71 | 72 | BriefInfoHandler(); 73 | // return current_year 74 | int getCurrentYear() const; 75 | 76 | private slots: 77 | // parse the response to requestBriefInfo() 78 | void parseUserInfo(QNetworkReply& reply); 79 | // parse the response to requestStuId() 80 | void parseStuId(QNetworkReply& reply); 81 | // parse the response to requestTokens() 82 | void parseTokens(QNetworkReply& reply); 83 | 84 | public slots: 85 | // call requestTokens() 86 | void start(); 87 | // return passed_semesters_avg 88 | QStringList getSemesterAvgs() const; 89 | // return passed_semesesters in readable format 90 | QStringList getSemesterYears() const; 91 | // return passed_semesters 92 | QList getRawSemesters() const; 93 | 94 | signals: 95 | void studentInfoChanged(); 96 | void currentYearChanged(); 97 | }; 98 | 99 | #endif // DASHBOARDHANDLER_H 100 | -------------------------------------------------------------------------------- /header/handlers/captchahandler.h: -------------------------------------------------------------------------------- 1 | #ifndef CAPTCHAHANDLER_H 2 | #define CAPTCHAHANDLER_H 3 | 4 | /* 5 | * Class: CapthcaHandler 6 | * Files: captchahandler.h, captchahandler.cpp 7 | * This class handles requesting captcha picture 8 | */ 9 | 10 | #include "handler.h" 11 | 12 | class CaptchaHandler : public Handler 13 | { 14 | Q_OBJECT 15 | private: 16 | const QString _captcha_url {QStringLiteral("/Forms/AuthenticateUser/captcha.aspx?0.03212876247262375")}; 17 | const QByteArray _image_path {"captcha.png"}; 18 | 19 | private slots: 20 | // save captcha image to 'image_path' 21 | bool parseGetCaptcha(QNetworkReply& reply); 22 | 23 | public: 24 | CaptchaHandler(); 25 | // request a new captcah image 26 | Q_INVOKABLE bool getCaptcha(); 27 | }; 28 | 29 | #endif // CAPTCHAHANDLER_H 30 | -------------------------------------------------------------------------------- /header/handlers/courseschedulehandler.h: -------------------------------------------------------------------------------- 1 | #ifndef COURSESCHEDULEHANDLER_H 2 | #define COURSESCHEDULEHANDLER_H 3 | 4 | /* 5 | * Class: CourseScheduleHandler 6 | * Files: courseschedulehandler.h, courseschedulehandler.cpp 7 | * The task of this class is to get the courses weekly schedule (and related informations) 8 | * from Golestan and send them to QML in proper structure. 9 | 10 | * The data structure we are using is based on ScheduleTable (refer to the scheduletable.cpp comments) 11 | */ 12 | 13 | #include 14 | 15 | #include "abstractxmldatahandler.h" 16 | #include "../controls/scheduletable.h" 17 | #include "../helpers/commonmacros.h" 18 | 19 | 20 | class CourseScheduleHandler : public AbstractXmlDataHandler 21 | { 22 | Q_OBJECT 23 | 24 | private: 25 | /** Properties **/ 26 | 27 | const QString _schedule_url {QStringLiteral("/Forms/F0202_PROCESS_REP_FILTER/F0202_01_PROCESS_REP_FILTER_DAT.ASPX?r=0.10057227848084405&fid=1;423&b=0&l=0&&lastm=20190829142532&tck=")}; 28 | // container for storing the ScheduleTable material 29 | QVariantList _weekly_schedule; 30 | QString _semester; 31 | 32 | /** Functions **/ 33 | 34 | // set semester to sem 35 | void setSemester (const QString& sem); 36 | 37 | // extract weekly course schedule from 'response' and fill 'weekly_schedule' 38 | bool extractWeeklySchedule (QString& response); 39 | 40 | // request validators for being able to make further requests 41 | bool requestTokens(); 42 | // request the weekly schedule 43 | bool requestSchedule(); 44 | 45 | // forced getter implementation (because of abstract parent class) for is_empty 46 | bool getIsEmpty() const override; 47 | 48 | private slots: 49 | // parse the validators from request requestTokens() 50 | void parseTokens (QNetworkReply& reply); 51 | // parse the weekly schedule 52 | void parseSchedule (QNetworkReply& reply); 53 | 54 | public slots: 55 | // start the process for recieving data from Golestan for semester 'current_semester' 56 | void start (const QString current_semester); 57 | // return the ScheduleTable materials 58 | QVariantList getSchedule () const; 59 | 60 | public: 61 | CourseScheduleHandler(); 62 | }; 63 | 64 | #endif // COURSESCHEDULEHANDLER_H 65 | -------------------------------------------------------------------------------- /header/handlers/handler.h: -------------------------------------------------------------------------------- 1 | #ifndef HANDLER_H 2 | #define HANDLER_H 3 | 4 | /* 5 | * Class: Handler 6 | * Files: handler.h, handler.cpp 7 | * 8 | * This is the primary class that handles every request and every response 9 | * sent to and recieve from Golestan system. 10 | * Extract validation values from every response, use validations correctly for every requests 11 | * and check if a response has any error(and extract possible error code) are the main responsibilities 12 | * of this class. 13 | * All the other *Handler classes are derived from this class. 14 | * 15 | */ 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | #include "../base/network.h" 25 | #include "../base/settings.h" 26 | #include "../helpers/errors.h" 27 | #include "../helpers/logger.h" 28 | 29 | class Handler : public QObject 30 | { 31 | Q_OBJECT 32 | private: 33 | /** Properties **/ 34 | 35 | /* Patterns for extracting necessary data from a response */ 36 | const QString _tokens_pattern {QStringLiteral("SavAut\\([,a-z0-9'A-Z-]+")}; // regex pattern for lt,ctck,u,ft,seq and ... 37 | const QString _error_withcode_pattern {QStringLiteral("ErrorArr = new Array\\('[\\w :]+")}; // regex pattern for finding error which has a code 38 | 39 | const QString _viewstate_keyword {QStringLiteral("__VIEWSTATE\" value=\"")}; // keyword for finding view state 40 | const QString _viewstate_gen_keyword {QStringLiteral("__VIEWSTATEGENERATOR\" value=\"")}; // keyword for finding viewstate generator 41 | const QString _event_val_keyword {QStringLiteral("__EVENTVALIDATION\" value=\"")}; // keyword for finding event validation 42 | const QString _tck_keyword {QStringLiteral("SetOpenerTck('")}; // keyword for finding tck 43 | 44 | /** Functions **/ 45 | 46 | /* 47 | * Extract toknes(not validators) from a response and return QHashString 48 | * which includes token titles as key and token values as value of QHash 49 | */ 50 | QHashString extractTokens(const QString& response); 51 | 52 | /* 53 | * Extract error code from response which returned by Golestan 54 | * return Errors::NoCodeFound if no code can be found in response. 55 | */ 56 | int extractDataErrorCode(const QString& response); 57 | 58 | /* 59 | * Extract error from response. 60 | * return Errors::NoError if there is no error 61 | * return Errors::UnknownError if there is error 62 | * but can't find error code in custom errors 63 | */ 64 | int extractDataError(const QString& response); 65 | 66 | protected: 67 | /** Properties **/ 68 | 69 | Q_PROPERTY(bool finished READ getFinished NOTIFY finished) 70 | Q_PROPERTY(bool success READ getSuccess NOTIFY successChanged) 71 | // returns Golestan errors or Errors:error_code 72 | Q_PROPERTY(uint errorCode READ getErrorCode NOTIFY errorCodeChanged) 73 | Q_PROPERTY(int errorType READ getErrorType NOTIFY errorCodeChanged) 74 | Q_PROPERTY(bool working READ getWorking NOTIFY workingChanged) 75 | 76 | static inline QHashString _cookies, _request_validators; 77 | static inline QString _root_url; 78 | 79 | Network _request; 80 | Errors _error_handler; 81 | bool _is_finished, _success; 82 | 83 | /** Functions **/ 84 | 85 | // Add to cookies a cookie with name of 'key' and value of 'value' 86 | void setCookie(const QString& key, const QString& value); 87 | /* 88 | * Add to cookies a cookie in a single string format like this: 89 | * SomeCookieName=SomeValue 90 | */ 91 | void setCookie(const QString& keyvalue); 92 | // remove all cookies 93 | void clearCookies(); 94 | 95 | // return cookies in string format that capable of being used as a request header 96 | QString getCookies() const; 97 | 98 | // return 'is_finished' 99 | bool getFinished() const; 100 | // set 'is_finished' to 'value' 101 | void setFinished(bool value); 102 | // return true if there is any request working 103 | bool getWorking() const; 104 | 105 | /* 106 | * simple setter and getter for error_code 107 | */ 108 | uint getErrorCode() const; 109 | void setErrorCode(int ecode); 110 | 111 | // Get error type(Critical-ness of a error) from Errors 112 | int getErrorType() const; 113 | 114 | /* 115 | * simple setter and getter for success 116 | */ 117 | void setSuccess(bool state); 118 | bool getSuccess() const; 119 | 120 | // set 'error_code' to 'ecode' and return true if QNetworkReply::NetworkError == QNetworkReply::NoError 121 | bool hasError(QNetworkReply::NetworkError ecode); 122 | 123 | /* 124 | * extract tokens and update 'cookies' and 'request_validators' 125 | * return true if succeed otherwise return false 126 | */ 127 | bool updateTokens(const QString& data); 128 | void clearTokens(); 129 | 130 | /* 131 | * This is important function and will be used in almost all of the 132 | * functions that parse Golestan response. 133 | * 'data' can have response or be empty. if data == QString(), the function 134 | * will fill it in proper moment. 135 | 136 | * This function Verify a response by: 137 | * 1- check if there is any error in 'reply' and 'data' 138 | * 2- update tokens by parsing 'data' 139 | * return ture if succeed and error_code will be Errors::NoError 140 | * otherwise return false and error_code will be a raw error code. 141 | */ 142 | bool verifyResponse(QNetworkReply& reply, QString& data); 143 | 144 | // extract form validators from a 'response' and return an empty QHashString if nothing found 145 | QHashString extractFormValidators(const QString& response); 146 | // return the tck or ctck token for authentication in Golestan 147 | QString getTckToken() const; 148 | 149 | public: 150 | explicit Handler(QObject *parent = nullptr); 151 | 152 | public slots: 153 | // return a error title for 'error_code' in error_handler 154 | QString getErrorString() const; 155 | // return a error description for 'error_code' in error_handler 156 | QString getErrorSolution() const; 157 | 158 | signals: 159 | // determines that a jobs has finished 160 | void finished(); 161 | void successChanged(); 162 | void errorCodeChanged(); 163 | void workingChanged(); 164 | }; 165 | 166 | #endif // HANDLER_H 167 | -------------------------------------------------------------------------------- /header/handlers/inithandler.h: -------------------------------------------------------------------------------- 1 | #ifndef INITHANDLER_H 2 | #define INITHANDLER_H 3 | 4 | /* 5 | * Class: InitHandler 6 | * Files: inithandler.h, inithandler.cpp 7 | * This class is supposed to initialize an connection between client 8 | * and Golestan system. 9 | */ 10 | 11 | #include "handler.h" 12 | #include 13 | 14 | class InitHandler : public Handler 15 | { 16 | Q_OBJECT 17 | private: 18 | const QString _loginurl{QStringLiteral("/Forms/AuthenticateUser/AuthUser.aspx?fid=0;1&tck=&&&lastm=20190219160242")}; 19 | 20 | private slots: 21 | // extract session id and parse informations needed to make subsequent requests. 22 | bool parseInit(QNetworkReply& reply); 23 | 24 | public: 25 | // send application's first request 26 | InitHandler(); 27 | Q_INVOKABLE bool start(); 28 | }; 29 | 30 | #endif // INITHANDLER_H 31 | -------------------------------------------------------------------------------- /header/handlers/loginhandler.h: -------------------------------------------------------------------------------- 1 | #ifndef LOGINHANDLER_H 2 | #define LOGINHANDLER_H 3 | 4 | /* 5 | * Class: LoginHandler 6 | * Files: loginhandler.h, loginhandler.cpp 7 | * This class handle login things and extract student name after successful login. 8 | */ 9 | 10 | #include 11 | #include "handler.h" 12 | 13 | class LoginHandler : public Handler 14 | { 15 | Q_OBJECT 16 | private: 17 | const QString _login_url{QStringLiteral("/Forms/AuthenticateUser/AuthUser.aspx?fid=0;1&tck=&&&lastm=20190219160242")}; 18 | QString _user_name; 19 | 20 | // extract student name and store it into 'user_name' by parsing 'response' 21 | bool extractName (QString& response); 22 | 23 | private slots: 24 | // return true if login was successful by parsing 'reply' 25 | bool parseLogin (QNetworkReply& reply); 26 | 27 | public: 28 | LoginHandler(); 29 | // send a request for login 30 | Q_INVOKABLE bool tryLogin(const QString username, const QString password, const QString captcha); 31 | // return 'user_name' 32 | Q_INVOKABLE QString getName() const; 33 | }; 34 | 35 | #endif // LOGINHANDLER_H 36 | -------------------------------------------------------------------------------- /header/handlers/offeredcoursehandler.h: -------------------------------------------------------------------------------- 1 | #ifndef OFFEREDCOURSEHANDLER_H 2 | #define OFFEREDCOURSEHANDLER_H 3 | 4 | /* 5 | * Class: OfferedCourseHandler 6 | * Files: offeredcoursehandler.h, offeredcoursehandler.cpp 7 | * The task of this class is: 8 | * 1- Retrieve offered course from Golestan 9 | * 2- Parse and extract necessary information 10 | * 3- Move the extracted data to OfferedCourseModel 11 | * 4- Restore the ScheduleTable(Weekly schedule created by user) from disk and integrate 12 | * the schedule with the data 13 | */ 14 | 15 | #include 16 | 17 | #include "abstractxmldatahandler.h" 18 | #include "../models/offeredcoursemodel.h" 19 | #include "../controls/scheduletable.h" 20 | #include "../helpers/commonmacros.h" 21 | 22 | class OfferedCourseHandler : public AbstractXmlDataHandler 23 | { 24 | Q_OBJECT 25 | 26 | private: 27 | const QString _offered_course_url {QStringLiteral("/Forms/F0202_PROCESS_REP_FILTER/F0202_01_PROCESS_REP_FILTER_DAT.ASPX?r=0.6914703770312649&fid=1;%1&b=0&l=0&isb=4&lastm=20190829142532&tck=")}; 28 | // we have 2 pages with information of offered course. their 'fid' parameter is below: 29 | const QStringList _url_fids {QStringLiteral("212"), QStringLiteral("211")}; 30 | 31 | // our container for storing offered course data 32 | QList _container; 33 | // our container for restoring schedule table from disk 34 | const QHash _schedule; 35 | // indicate how many we tried for retrieving data from Golestan. 36 | int _request_number; 37 | 38 | // Forced implementation of AbstractXmlDataHandler pure function 39 | bool getIsEmpty () const override; 40 | // clear the containers and free up the memory 41 | void cleanUp(); 42 | // Replace wrong characters in 'time' and simplify() the 'time' 43 | void normalizeTime (QString& time); 44 | // Check if a course with key 'key' is already choosed(and present in 'schedule') or not. 45 | bool CheckIsChoosed (const QString& key, const QHash& schedule) const; 46 | // request for offerd course information 47 | void requestCourses (); 48 | // Extract needed information from 'response' 49 | bool extractOfferedCourses (const QString& response); 50 | 51 | public: 52 | OfferedCourseHandler (); 53 | virtual ~OfferedCourseHandler (); 54 | 55 | private slots: 56 | // parse the response from requestCourse() 57 | void parseCourses (QNetworkReply& reply); 58 | 59 | public slots: 60 | void start(); 61 | // move 'container' to the 'model' which in this case is OfferedCourseModel 62 | void sendDataTo (QObject* model); 63 | // Restore the ScheduleTable data that stored in disk. 64 | QVariantList restoreSchedule () const; 65 | 66 | }; 67 | 68 | #endif // OFFEREDCOURSEHANDLER_H 69 | -------------------------------------------------------------------------------- /header/handlers/scoreshandler.h: -------------------------------------------------------------------------------- 1 | #ifndef SCORESHANDLER_H 2 | #define SCORESHANDLER_H 3 | 4 | /* 5 | * Class: ScoresHandler 6 | * Files: scoreshandler.h, scoreshandler.cpp 7 | * The task of this class is: 8 | * 1- Retrieve scores for a specific semester from Golestan 9 | * 2- re-calculate scores average if we have some temporary scores (because Golestan would not count them) 10 | */ 11 | 12 | #include 13 | #include "abstractxmldatahandler.h" 14 | #include "../helpers/commonmacros.h" 15 | 16 | class ScoresHandler : public AbstractXmlDataHandler 17 | { 18 | Q_OBJECT 19 | 20 | private: 21 | // containers 22 | QVariantList _scores; 23 | QVariantMap _score_brief; 24 | 25 | // the semester number we wanna get information of 26 | QString _semester; 27 | QString _student_id; 28 | // re-calculated average stores here 29 | float _custom_average; 30 | // flag which determine the need of re-calculating of averages 31 | bool _need_custom_avg; 32 | 33 | // url of section we gonna send our requests. 34 | const inline static QString _scores_url {QStringLiteral("/Forms/F1802_PROCESS_MNG_STDJAMEHMON/F1802_01_PROCESS_MNG_STDJAMEHMON_Dat.aspx?r=0.9638806400489983&fid=0;12310&b=0&l=0&&lastm=20180201081222&tck=")}; 35 | 36 | 37 | // Forced implementation of AbstractXmlDataHandler pure function 38 | bool getIsEmpty () const override; 39 | 40 | // requests to Golestan 41 | void requestTokens(); 42 | void requestScores(); 43 | 44 | // normalize the name and remove ugly chars 45 | void normalizeName(QString& name); 46 | bool extractScores(const QString& data); 47 | bool extractBirefScores(const QString& data); 48 | // extract the value of 'key' from Golestan-provided XML data which have stored in 'data' 49 | QString extractXmlAttr(const QString& data, const QString& key, const bool start_from_first = true) const; 50 | 51 | private slots: 52 | // parse the response to requestTokens() 53 | void parseTokens(QNetworkReply& reply); 54 | // parse the response to requestScores() 55 | void parseScores(QNetworkReply& reply); 56 | 57 | public: 58 | // Course status 59 | enum status 60 | { 61 | Passed, 62 | Failed, 63 | Deleted, 64 | Temporary, 65 | Undefined 66 | }; 67 | Q_ENUM(status) 68 | 69 | ScoresHandler(); 70 | 71 | public slots: 72 | // Initialize the request-sequence 73 | void start(const QString semester, const QString student_id); 74 | // get scores of semester 'semester' 75 | void getScoresOf(const QString semester); 76 | // return containers 77 | QVariantList getScores() const; 78 | QVariantList getBriefScores() const; 79 | }; 80 | 81 | #endif // SCORESHANDLER_H 82 | -------------------------------------------------------------------------------- /header/helpers/commonmacros.h: -------------------------------------------------------------------------------- 1 | #ifndef COMMONMACROS_H 2 | #define COMMONMACROS_H 3 | 4 | /* 5 | * file name: commonmacros.h 6 | * This file suppose to store all of the Macro's we might use in our code. 7 | */ 8 | 9 | #include 10 | #include 11 | 12 | // Just use QStringLiteral when we are not in windows 13 | #if defined (Q_OS_WINDOWS) 14 | #define MyStringLiteral(str) \ 15 | str 16 | #elif defined (Q_OS_LINUX) 17 | #define MyStringLiteral(str) \ 18 | QStringLiteral(str) 19 | #elif defined (Q_OS_MACOS) 20 | #define MyStringLiteral(str) \ 21 | QStringLiteral(str) 22 | #endif 23 | 24 | #endif // COMMONMACROS_H 25 | -------------------------------------------------------------------------------- /header/helpers/constants.h: -------------------------------------------------------------------------------- 1 | #ifndef CONSTANTS_H 2 | #define CONSTANTS_H 3 | 4 | /* 5 | * file name: Constatns.h 6 | * This file suppose to store all of the constant values which other files might use. 7 | */ 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | using QHashString = QHash ; 14 | namespace Constants { 15 | 16 | static inline QString generateTodayDate () 17 | { 18 | QLocale locale {QLocale::Persian, QLocale::Iran}; 19 | locale.setNumberOptions(QLocale::OmitGroupSeparator); 20 | 21 | QCalendar calendar(QCalendar::System::Jalali); 22 | QDate today = QDate::currentDate(); 23 | QCalendar::YearMonthDay ymd = calendar.partsFromDate(today); 24 | 25 | return QString(QStringLiteral("%1 %2 %3")) 26 | .arg(locale.toString(ymd.day), calendar.monthName(locale, ymd.month), locale.toString(ymd.year)); 27 | } 28 | 29 | static inline const QString application_name{QStringLiteral("Boostan")}, 30 | organization_name{QStringLiteral("AVID")}, 31 | domain_name{QStringLiteral("SeedPuller.ir")}, 32 | application_path {QDir::currentPath() + "/"}, 33 | // default golestan url for inserting to settings if no configuration is available 34 | root_url {QStringLiteral("https://golestan.umz.ac.ir")}, 35 | settings_path {application_path + QStringLiteral("settings.ini")}, 36 | today_date {generateTodayDate()}, 37 | version {QStringLiteral("0.1.2")}; 38 | 39 | }; // namespace Constants 40 | 41 | #endif // CONSTANTS_H 42 | -------------------------------------------------------------------------------- /header/helpers/errors.h: -------------------------------------------------------------------------------- 1 | #ifndef ERRORS_H 2 | #define ERRORS_H 3 | 4 | /* 5 | * Class: Errors 6 | * Files: errors.h, errors.cpp 7 | * This class act as an interface for showing errors. 8 | * Convert raw error codes to showing-capable errors, retrieving error titles and description- 9 | * and storing error states are the tasks of this class. 10 | */ 11 | 12 | #include 13 | #include 14 | #include "commonmacros.h" 15 | 16 | class Errors : public QObject 17 | { 18 | Q_OBJECT 19 | public: 20 | // our custom error codes. 21 | enum error_codes 22 | { 23 | NoError = 0, 24 | CustomCode = 800, 25 | UnknownError = 1000, 26 | ServerConnenctionError = CustomCode, 27 | WrongCaptcha, 28 | CaptchaStoreError, 29 | SettingsError, 30 | ExtractError 31 | }; 32 | 33 | // error types that identify if a error code is critical or not 34 | enum error_type: uint 35 | { 36 | Critical = 0, 37 | SemiCritical, 38 | Normal 39 | }; 40 | Q_ENUM(error_type) 41 | 42 | private: 43 | int error_code; 44 | 45 | // type of each error code 46 | QHash critical_status 47 | { 48 | {ServerConnenctionError, SemiCritical}, 49 | {UnknownError, Critical}, 50 | {WrongCaptcha, Normal}, 51 | {CaptchaStoreError, Critical}, 52 | {SettingsError, Critical}, 53 | {ExtractError, Normal}, 54 | // this is built-in Golestan error codes that we might see. 55 | {1, Normal}, 56 | 57 | // start of account settings errors 58 | {3, Normal}, 59 | {4, Normal}, 60 | {6, Normal}, 61 | {8, Normal}, 62 | // end of account settings errors 63 | 64 | {24, Critical}, // authorization destroyed. 65 | {27, SemiCritical}, 66 | {18, Normal} // Access is limited(access denied actually) 67 | }; 68 | 69 | // Error title for each error code that identifyed so far 70 | inline static const QHash error_strings 71 | { 72 | {ServerConnenctionError, MyStringLiteral("خطا در اتصال به سرور")}, 73 | {UnknownError, MyStringLiteral("اوه! یک خطای ناشناخته رخ داده!")}, 74 | {WrongCaptcha, MyStringLiteral("کد امنیتی اشتباه وارد شده!")}, 75 | {CaptchaStoreError, MyStringLiteral("مشکلی در ذخیره تصویر امنیتی بوجود اومده!")}, 76 | {SettingsError, MyStringLiteral("مشکلی در تنظیمات بوستان بوجود اومده!")}, 77 | {ExtractError, MyStringLiteral("مشکلی در استخراج اطلاعات از گلستان بوجود اومده!")}, 78 | // this is built-in Golestan error codes that we might see. 79 | {1, MyStringLiteral("نام کاربری یا رمز عبوری که وارد شده اشتباهه!")}, 80 | {3, MyStringLiteral("نام کاربری فعلی‌ت رو اشتباه وارد کردی!")}, 81 | {4, MyStringLiteral("رمز فعلی‌ت رو اشتباه وارد کردی!")}, 82 | {6, MyStringLiteral("رمز های جدید باهمدیگه مطابقت ندارن!")}, 83 | {8, MyStringLiteral("نام کاربری ای که وارد کردی قبلا انتخاب شده!")}, 84 | {24, MyStringLiteral("گلستان میگه دوباره باید وارد بشی!")}, 85 | {27, MyStringLiteral("تعداد تلاش ناموفق برای ورود بیش از حد مجاز شده!")}, 86 | {18, MyStringLiteral("امکان دسترسی به محتوای مورد نظر محدود شده!")} 87 | }; 88 | 89 | // Error description for each error code that identifyed so far 90 | inline static const QHash error_solutions 91 | { 92 | {ServerConnenctionError, MyStringLiteral("لطفا وضعیت اتصال به اینترنت و وبسایت گلستان رو بررسی کنید")}, 93 | {UnknownError, MyStringLiteral("از اونجایی که منم نمیدونم چه خطاییه، بهتره لاگ هارو بررسی کنی و در صفحه گیتهاب این مشکل رو گزارش بدی")}, 94 | {WrongCaptcha, MyStringLiteral("دوباره با دقت تلاش کن :)")}, 95 | {CaptchaStoreError, MyStringLiteral("دسترسی های پوشه بوستان رو بررسی کنید و برنامه رو دوباره اجرا کنید")}, 96 | {SettingsError, MyStringLiteral("دسترسی های فایل تنظیمات که در پوشه برنامه قرار داره رو بررسی کنید")}, 97 | {ExtractError, MyStringLiteral("یکبار دیگه تلاش کن تا بوستان این مشکل رو برات حل کنه")}, 98 | // this is built-in Golestan error codes that we might see. 99 | {1, MyStringLiteral("دوباره با دقت تلاش کن :)")}, 100 | {3, MyStringLiteral("دوباره با دقت تلاش کن :)")}, 101 | {4, MyStringLiteral("دوباره با دقت تلاش کن :)")}, 102 | {6, MyStringLiteral("دوباره با دقت تلاش کن :)")}, 103 | {8, MyStringLiteral("یک نام کاربری دیگه رو انتخاب کن :)")}, 104 | {24, MyStringLiteral("دوباره برای ورود تلاش کن تا این مشکل رفع بشه. اگر نشد لطفا در گیتهاب خبر بده")}, 105 | {27, MyStringLiteral("یکبار دیگه تلاش کن تا بوستان این مشکل رو برات حل کنه")}, 106 | {18, MyStringLiteral("از خودِ سایت گلستان چک کن ببین آیا همه چیز درسته؟ اگر درست بود، این مشکل از بوستانه پس لطفا در گیتهاب به ما گزارش بده.")} 107 | }; 108 | 109 | public: 110 | /* 111 | * 'qt_offset' is offset for QNetworkReply::NetworkError codes 112 | * the reason i defined this is Golestan error codes are in conflict with Qt network error codes. 113 | */ 114 | inline static constexpr int qt_offset{100}; 115 | inline static constexpr int NoCodeFound{-1}; 116 | 117 | /* 118 | * Key words for identifying an error code for strings that returned by Golestan 119 | * and has no built-in error codes in it. 120 | */ 121 | inline static const QHash error_keywords 122 | { 123 | {WrongCaptcha, MyStringLiteral("كد امنيتي")} // error strings that has this keyword are about WrongCaptcha 124 | }; 125 | 126 | explicit Errors(QObject *parent = nullptr); 127 | 128 | // return error code 129 | int getErrorCode() const; 130 | 131 | // Assign an error type to an error code(specifying the critical-ness of an error) 132 | void setCriticalStatus(const int ecode, const Errors::error_type type); 133 | // reset error_code to NoError 134 | void reset(); 135 | 136 | public slots: 137 | /* 138 | * We need these functions to accessed directly via QML 139 | */ 140 | 141 | // Set 'ecode' as error code 142 | bool setErrorCode(int ecode); 143 | 144 | // return type of a 'error_code' which is a 'error_type' member 145 | int getErrorType() const; 146 | 147 | // return a error title for 'error_code' 148 | QString getErrorString() const; 149 | // return a error description for 'error_code' 150 | QString getErrorSolution() const; 151 | 152 | }; 153 | 154 | #endif // ERRORS_H 155 | -------------------------------------------------------------------------------- /header/helpers/logger.h: -------------------------------------------------------------------------------- 1 | #ifndef LOGGER_H 2 | #define LOGGER_H 3 | /* 4 | * Class: Logger 5 | * Files: logger.h, logger.cpp 6 | * This class will write logs into the disk in proper format 7 | */ 8 | 9 | #include 10 | 11 | class Logger 12 | { 13 | 14 | private: 15 | static inline QString _file_name {"boostan.log"}; 16 | static inline QFile _file {_file_name}; 17 | 18 | public: 19 | Logger() = default; 20 | // initialize the logger. 21 | static bool init (); 22 | // write logs into the disk. {{more}} defines if we should close the file or not. 23 | static void log (const QByteArray& data, const bool more = false); 24 | 25 | }; 26 | 27 | #endif // LOGGER_H 28 | -------------------------------------------------------------------------------- /header/models/offeredcoursemodel.h: -------------------------------------------------------------------------------- 1 | #ifndef OFFEREDCOURSEMODEL_H 2 | #define OFFEREDCOURSEMODEL_H 3 | 4 | /* 5 | * Class: OfferedCourseModel 6 | * Files: offeredcoursemodel.h, offeredcoursemodel.cpp 7 | * This class have to prepare data model to be used in a table in OfferedCourse page 8 | * Also this class convert a data to ScheduleTable-compatible format to get used there. 9 | */ 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | #include "../controls/scheduletable.h" 16 | 17 | class OfferedCourseModel : public QAbstractListModel 18 | { 19 | Q_OBJECT 20 | 21 | private: 22 | // the container which stores the data 23 | QList _data_container; 24 | 25 | public: 26 | explicit OfferedCourseModel (QObject *parent = nullptr); 27 | virtual ~OfferedCourseModel (); 28 | 29 | // role names used in model 30 | inline static const QStringList columns 31 | { 32 | QStringLiteral("group"), QStringLiteral("courseNumber"), QStringLiteral("courseName"), 33 | QStringLiteral("weight"), QStringLiteral("capacity"), QStringLiteral("sex"), 34 | QStringLiteral("teacher"), QStringLiteral("time"), QStringLiteral("place"), 35 | QStringLiteral("exam"), QStringLiteral("isChoosed") 36 | }; 37 | 38 | // roles used in model 39 | enum roles 40 | { 41 | ROLE_START = Qt::UserRole + 1, 42 | groupRole, 43 | courseNumberRole, 44 | courseNameRole, 45 | weightRole, 46 | capacityRole, 47 | sexRole, 48 | teacherRole, 49 | timeRole, 50 | placeRole, 51 | examRole, 52 | isChoosedRole, 53 | ROLE_END 54 | }; 55 | 56 | enum gender 57 | { 58 | Male = 0, 59 | Female, 60 | None 61 | }; 62 | 63 | Q_ENUMS(gender) 64 | 65 | /** Basic functionality: **/ 66 | 67 | int rowCount (const QModelIndex &parent = QModelIndex()) const override; 68 | QVariant data (const QModelIndex &index, int role = Qt::DisplayRole) const override; 69 | bool setData (const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; 70 | Qt::ItemFlags flags (const QModelIndex& index) const override; 71 | QHash roleNames () const override; 72 | 73 | /** Custom functions: **/ 74 | 75 | // convert {{role}} to integer index 76 | static int roleToIndex (roles role); 77 | // free up memory 78 | void cleanUp (); 79 | // move {{container}} data's to {{_data_container}} 80 | void setDataContainer (QList& container); 81 | 82 | public slots: 83 | // convert and return a 'data_container' element to ScheduleTable compatible format 84 | QVariantMap toScheduleFormat (const int index) const; 85 | // return a weight of a course at index {{index}} in {{_data_container}} 86 | int getCourseWeight (const int index) const; 87 | // set isChoosed property of {{index_list}} indexes to false 88 | void clearAllChoosed (const QList index_list); 89 | }; 90 | 91 | #endif // OFFEREDCOURSEMODEL_H 92 | -------------------------------------------------------------------------------- /qml/Controls/ClickableText.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * This Control is a Label with support of MouseArea 3 | */ 4 | 5 | import QtQuick 2.15 6 | import QtQuick.Controls 2.15 7 | 8 | Label { 9 | signal clicked() 10 | property alias area: mouse_area 11 | MouseArea { 12 | id: mouse_area 13 | anchors.fill: parent 14 | cursorShape: Qt.PointingHandCursor 15 | onClicked: parent.clicked() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /qml/Controls/Icon.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * This control is responsible for showing an icon by using "fontello" font. 3 | */ 4 | 5 | import QtQuick 2.15 6 | import QtQuick.Controls 2.15 7 | ClickableText { 8 | property bool clickAble: false 9 | property string description 10 | area.enabled: clickAble 11 | area.cursorShape: clickAble ? Qt.PointingHandCursor : Qt.ArrowCursor 12 | area.hoverEnabled: true 13 | font.family: "fontello" 14 | font.pixelSize: 15 15 | ToolTip.visible: area.containsMouse && description !== "" 16 | ToolTip.delay: 500 17 | ToolTip.text: description 18 | } 19 | -------------------------------------------------------------------------------- /qml/Controls/LoadingAnimationColor.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * This Component is a color changing animation which is being used for loadings 3 | */ 4 | 5 | import QtQuick 2.15 6 | import QtQuick.Controls 2.15 7 | 8 | Rectangle { 9 | id: root 10 | color: "#1D2025" 11 | 12 | property color from: "#1D2025" 13 | property color to: "#424242" 14 | 15 | SequentialAnimation on color { 16 | id: animation 17 | running: root.visible 18 | loops: Animation.Infinite 19 | 20 | ColorAnimation { 21 | from: root.from 22 | to: root.to 23 | duration: 2000 24 | } 25 | 26 | ColorAnimation { 27 | from: root.to 28 | to: root.from 29 | duration: 2000 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /qml/Controls/LoadingAnimationPulse.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * This component is a animation consists of some rectangles that being short and tall 3 | */ 4 | 5 | import QtQuick 2.15 6 | 7 | Item { 8 | id: root 9 | 10 | property alias barCount: repeater.model 11 | property color color: "white" 12 | property int spacing: 5 13 | property bool running: true 14 | 15 | Repeater { 16 | id: repeater 17 | delegate: Component { 18 | Rectangle { 19 | width: (root.width / root.barCount) - root.spacing 20 | height: root.height 21 | x: index * width + root.spacing * index 22 | transform: Scale { 23 | id: rectScale 24 | origin { 25 | x: width / 2 26 | y: height / 2 27 | } 28 | } 29 | transformOrigin: Item.Center 30 | color: root.color 31 | 32 | SequentialAnimation { 33 | id: anim 34 | loops: Animation.Infinite 35 | running: root.running 36 | NumberAnimation { target: rectScale; property: "yScale"; from: 1; to: 1.5; duration: 300 } 37 | NumberAnimation { target: rectScale; property: "yScale"; from: 1.5; to: 1; duration: 300 } 38 | PauseAnimation { duration: root.barCount * 150 } 39 | } 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /qml/Controls/MyButton.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * Custom button component 3 | */ 4 | 5 | //! TODO: implement an implicit width calculation 6 | 7 | import QtQuick 2.15 8 | import QtQuick.Controls 2.15 9 | import QtQuick.Layouts 1.15 10 | 11 | Button { 12 | id: root 13 | font.capitalization: Font.MixedCase 14 | Keys.onReturnPressed: clicked() 15 | Keys.onEnterPressed: clicked() 16 | Keys.onSpacePressed: {} 17 | 18 | property var bgColor: "#3fc1c9" 19 | property alias border: __background.border 20 | property alias color: __text.color 21 | property alias radius: __background.radius 22 | property alias iconText: __icon.text 23 | property alias iconSize: __icon.font.pixelSize 24 | property alias area: __mousearea 25 | property alias direction: __content_layout.layoutDirection 26 | 27 | signal clicked() 28 | signal pressAndHold() 29 | 30 | contentItem: Item { 31 | RowLayout { 32 | id: __content_layout 33 | anchors.centerIn: parent 34 | opacity: __background.opacity 35 | 36 | Text { 37 | id: __icon 38 | visible: text == "" ? false : true 39 | Layout.bottomMargin: 3 40 | font.family: "fontello" 41 | color: __text.color 42 | } 43 | 44 | Text { 45 | id: __text 46 | text: root.text 47 | font: root.font 48 | color: "black" 49 | } 50 | } 51 | } 52 | 53 | background: Rectangle { 54 | id: __background 55 | property real mouse_opacity: 1.0 56 | implicitHeight: 30 57 | implicitWidth: 50 58 | radius: 3 59 | color: root.bgColor 60 | opacity: { 61 | if (!root.enabled) return 0.4; 62 | if (__mousearea.containsPress) return 0.65; 63 | return 1.0; 64 | } 65 | 66 | 67 | MouseArea { 68 | id: __mousearea 69 | anchors.fill: parent 70 | hoverEnabled: true 71 | onPressed: { 72 | __background_presshold.x = __mousearea.mouseX 73 | __background.state = "PressedAndHold" 74 | } 75 | onReleased: { 76 | __background.state = "" 77 | root.clicked() 78 | } 79 | } 80 | 81 | Rectangle { 82 | /* 83 | * this rectangle will fill the whole button with animation when a click occur 84 | */ 85 | id: __background_presshold 86 | height: __background.height 87 | width: 0 88 | // height: 0 89 | color: root.bgColor 90 | opacity: 0.5 91 | radius: __background.radius 92 | 93 | } 94 | 95 | Rectangle { 96 | /* shadow will be visible when mouse hovered on button or a click occured */ 97 | id: __background_shadow 98 | anchors.fill: __background 99 | radius: __background.radius 100 | color: "#616161" 101 | opacity: (__mousearea.containsMouse || root.focus) && !__mousearea.containsPress ? 0.2 : 0.0 102 | Behavior on opacity { 103 | NumberAnimation { 104 | duration: 300 105 | easing.type: Easing.InOutQuad 106 | } 107 | } 108 | } 109 | 110 | states: [ 111 | State { 112 | name: "PressedAndHold" 113 | AnchorChanges { 114 | target: __background_presshold 115 | anchors.right: __background.right 116 | anchors.left: __background.left 117 | } 118 | } 119 | ] 120 | 121 | transitions: [ 122 | Transition { 123 | to: "PressedAndHold" 124 | SequentialAnimation { 125 | NumberAnimation { 126 | duration: 150 127 | easing.type: Easing.InOutQuad 128 | } 129 | AnchorAnimation { 130 | duration: 500 131 | easing.type: Easing.InOutQuad 132 | } 133 | } 134 | } 135 | ] 136 | 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /qml/Controls/MyComboBox.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * Custom RTL ComboBox component 3 | * This component made from an customized ComboBox inside an Item. 4 | * The reason of this choice is the ComboBox reaction(animation) in response to activation 5 | * or deactivation. The ComboBox height will change by 10 in those situtaion and Would 6 | * make problem if We only have ComboBox(Without wrapping in Item) and any other component has 7 | * binding with this ComboBox. 8 | */ 9 | 10 | import QtQuick 2.15 11 | import QtQuick.Controls 2.15 12 | Item { 13 | id: root 14 | implicitWidth: control.implicitWidth 15 | implicitHeight: control.implicitHeight 16 | property alias comboItem: control 17 | property alias radius: control.radius 18 | property alias font: control.font 19 | property alias model: control.model 20 | property alias popupMaxHeight: control.popupMaxHeight 21 | property alias currentValue: control.currentValue 22 | property alias currentIndex: control.currentIndex 23 | 24 | signal activated(var index); 25 | 26 | ComboBox { 27 | id: control 28 | width: parent.width 29 | // we need 10 unit for the animation 30 | height: parent.height - 10 31 | model: ["سلام", "سلام دو", "سلام سه"] 32 | font.family: regular_font.name 33 | font.weight: Font.DemiBold 34 | font.pixelSize: 17 35 | property real radius: 12 36 | // specify the maximum height of popup. 37 | property real popupMaxHeight: 150 38 | 39 | Behavior on height { 40 | NumberAnimation { duration: 150 } 41 | } 42 | 43 | onActivated: root.activated(index) 44 | 45 | delegate: Item { 46 | width: control.width 47 | height: item_delg.height 48 | ItemDelegate { 49 | id: item_delg 50 | width: parent.width 51 | highlighted: control.highlightedIndex === index 52 | 53 | onClicked: { 54 | control.currentIndex = index 55 | control.activated(index) 56 | pop.close() 57 | } 58 | 59 | contentItem: Text { 60 | text: modelData[control.textRole] === undefined ? modelData : modelData[control.textRole] 61 | color: "#FFFFFF" 62 | font: control.font 63 | elide: Text.ElideRight 64 | verticalAlignment: Text.AlignVCenter 65 | } 66 | } 67 | 68 | // separator between items 69 | Rectangle { 70 | visible: index !== control.count - 1 71 | width: parent.width 72 | height: 1 73 | y: parent.height 74 | color: "#159E84" 75 | opacity: 0.5 76 | } 77 | } 78 | 79 | indicator: Canvas { 80 | id: canvas 81 | x: width + control.leftPadding 82 | y: control.topPadding + (control.availableHeight - height) / 2 83 | width: 12 84 | height: 8 85 | contextType: "2d" 86 | 87 | // rotate the 'indicator' by enabling/disabling the popup 88 | Connections { 89 | target: pop 90 | function onAboutToShow() { rotate.from= 0; rotate.to = 180; rotate.start() } 91 | function onAboutToHide() { rotate.from= 180; rotate.to = 360; rotate.start() } 92 | } 93 | 94 | Component.onCompleted: requestPaint() 95 | onPaint: { 96 | context.reset(); 97 | context.moveTo(0, 0); 98 | context.lineTo(width, 0); 99 | context.lineTo(width / 2, height); 100 | context.closePath(); 101 | context.fillStyle = "#121212"; 102 | context.fill(); 103 | } 104 | RotationAnimation { 105 | id: rotate 106 | target: canvas 107 | duration: 150 108 | } 109 | } 110 | 111 | contentItem: Text { 112 | rightPadding: 0 113 | leftPadding: control.indicator.width + control.spacing 114 | 115 | text: control.displayText 116 | font: control.font 117 | color: "#262125" 118 | verticalAlignment: Text.AlignVCenter 119 | elide: Text.ElideRight 120 | } 121 | 122 | background: Rectangle { 123 | implicitWidth: 120 124 | implicitHeight: 40 125 | color: "#19B99A" 126 | radius: control.radius 127 | 128 | } 129 | 130 | popup: Popup { 131 | id: pop 132 | y: control.height - 15 133 | width: control.width 134 | height: contentItem.implicitHeight 135 | padding: 1 136 | 137 | Behavior on height { 138 | NumberAnimation { duration: 130 } 139 | } 140 | 141 | contentItem: ListView { 142 | clip: true 143 | implicitHeight: contentHeight <= control.popupMaxHeight ? contentHeight + 5 : control.popupMaxHeight 144 | 145 | model: control.popup.visible ? control.delegateModel : null 146 | currentIndex: control.highlightedIndex 147 | boundsBehavior: ListView.StopAtBounds 148 | 149 | ScrollIndicator.vertical: ScrollIndicator { } 150 | } 151 | 152 | background: Rectangle { 153 | color: "#116A59" 154 | radius: control.radius 155 | } 156 | 157 | // change the component height whenever popup being visible/invisible 158 | onAboutToShow: { 159 | control.height += 10 160 | control.topPadding -= 10 161 | } 162 | onAboutToHide: { 163 | control.height -= 10 164 | control.topPadding += 10 165 | } 166 | 167 | } 168 | } 169 | 170 | } 171 | 172 | -------------------------------------------------------------------------------- /qml/Controls/MySwitch.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | import QtQuick.Controls 2.15 3 | 4 | Item { 5 | id: root 6 | 7 | property alias checked: control.checked 8 | width: 160 9 | height: 55 10 | 11 | Switch { 12 | id: control 13 | 14 | indicator: Rectangle { 15 | id: control_bg 16 | implicitWidth: 130 17 | implicitHeight: 64 18 | width: root.width - (check_indicator.width * 2) 19 | height: root.height 20 | radius: 48 21 | color: "#f6f6f6" 22 | border.color: "#ececec" 23 | border.width: 3 24 | 25 | Rectangle { 26 | id: switch_circle 27 | x: control.checked ? parent.width - width - 10 : 10 28 | width: root.height * 0.8 29 | height: width 30 | radius: width 31 | anchors.verticalCenter: parent.verticalCenter 32 | color: control.checked ? "#5ef499" : "#cccccc" 33 | border.color: control.checked ? "#86e2ac" : "#d1d1d1" 34 | 35 | Behavior on x { 36 | enabled: true 37 | NumberAnimation { 38 | duration: 200 / 3 39 | easing.type: Easing.OutInBounce 40 | } 41 | } 42 | 43 | Behavior on color { ColorAnimation { duration: 200} } 44 | } 45 | } 46 | 47 | } 48 | 49 | Rectangle { 50 | id: check_indicator 51 | width: 16 52 | height: 16 53 | radius: height 54 | color: control.checked ? "#5ef499" : "#d96763" 55 | anchors.verticalCenter: parent.verticalCenter 56 | anchors.left: control.right 57 | anchors.leftMargin: -15 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /qml/Controls/MyTextInput.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a custom text input component 3 | */ 4 | 5 | import QtQuick 2.15 6 | import QtQuick.Controls 2.15 7 | import QtQuick.Layouts 1.15 8 | 9 | Item { 10 | id: root 11 | property alias direction: row_layout.layoutDirection 12 | property alias icon: icon.text 13 | property alias iconSize: icon.font.pixelSize 14 | property alias text: tfield_root.text 15 | property alias placeHolder: tfield_root.placeholderText 16 | property alias mode: tfield_root.echoMode 17 | property alias horizontalAlignment: tfield_root.horizontalAlignment 18 | property bool isEmpty: tfield_root.text.trim() == "" 19 | 20 | signal focusChanged 21 | 22 | Rectangle { 23 | id: input_container 24 | width: root.width 25 | height: root.height 26 | color: "#FFFFFF" 27 | radius: 8 28 | border.width: 1 29 | border.color: "transparent" 30 | 31 | 32 | RowLayout { 33 | id: row_layout 34 | width: parent.width - 10 35 | height: parent.height 36 | anchors.horizontalCenter: parent.horizontalCenter 37 | spacing: 2 38 | 39 | Label { 40 | id: icon 41 | visible: text == "" ? false : true 42 | Layout.rightMargin: 2 43 | font.family: "fontello" 44 | font.pixelSize: 30 45 | } 46 | 47 | Rectangle { 48 | // separator between icon and input 49 | visible: icon.visible 50 | Layout.leftMargin: 6 51 | Layout.rightMargin: 6 52 | width: 1 53 | height: parent.height 54 | color: "#616161" 55 | } 56 | 57 | TextField { 58 | id: tfield_root 59 | placeholderText: "Here text" 60 | Layout.fillWidth: true 61 | Layout.fillHeight: true 62 | bottomPadding: tfield_bg.implicitHeight / 5 63 | selectByMouse: true 64 | onFocusChanged: { 65 | root.focusChanged() 66 | if (tfield_root.focus) { 67 | input_container.border.color = "#2196F3" 68 | return; 69 | } 70 | 71 | // if focus released and the input is empty, make the border orange 72 | if (!tfield_root.focus && tfield_root.text.trim() == "") { 73 | input_container.border.color = "#E65100" 74 | return; 75 | } 76 | input_container.border.color = "transparent" 77 | } 78 | 79 | background: Rectangle { 80 | id: tfield_bg 81 | implicitHeight: 40 82 | color: "transparent" 83 | } 84 | 85 | 86 | // cursor component that blink when user is typing 87 | cursorDelegate: Rectangle { 88 | id: tfield_cursor 89 | visible: tfield_root.focus 90 | height: tfield_root.height - 20 91 | width: 1 92 | color: "#212121" 93 | anchors.verticalCenter: tfield_root.verticalCenter 94 | 95 | SequentialAnimation { 96 | running: visible 97 | loops: Animation.Infinite 98 | NumberAnimation { 99 | target: tfield_cursor 100 | property: "opacity" 101 | duration: 100 102 | to: 1 103 | easing.type: Easing.InOutQuad 104 | } 105 | 106 | PauseAnimation { duration: 500 } 107 | 108 | NumberAnimation { 109 | target: tfield_cursor 110 | property: "opacity" 111 | duration: 100 112 | to: 0 113 | easing.type: Easing.InOutQuad 114 | } 115 | 116 | PauseAnimation { duration: 500 } 117 | } 118 | } 119 | } 120 | } 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /qml/Controls/Notifier.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * A notifier component which used for send temporary messages to user 3 | */ 4 | 5 | import QtQuick 2.0 6 | import QtQuick.Controls 2.15 7 | 8 | Item { 9 | id: root 10 | opacity: 0.0 11 | visible: false 12 | width: Math.max(notifier_text.contentWidth, notifier_solution.contentWidth) + 20 13 | height: notifier_text.contentHeight + notifier_solution.contentHeight + 20 14 | 15 | // identify the animation direction of notifier 16 | // Do not use anchors on the same direction with showType 17 | property int showType: Notifier.ShowType.DownToUp 18 | property alias bgColor: notifier_background.color 19 | property alias textColor: notifier_text.color 20 | property alias text: notifier_text.text 21 | property alias solution: notifier_solution.text 22 | property alias font: notifier_text.font 23 | property real distance: showType === Notifier.ShowType.DownToUp ? root.parent.height + root.height + 20 : -root.width 24 | enum ShowType { 25 | DownToUp, 26 | LeftToRight 27 | } 28 | 29 | function show() { 30 | root.state = "not-visible" 31 | root.state = "visible" 32 | } 33 | 34 | Rectangle { 35 | id: notifier_background 36 | anchors.fill: parent 37 | color: "#19B99A" 38 | radius: 5 39 | Column { 40 | anchors.centerIn: notifier_background 41 | Label { 42 | id: notifier_text 43 | anchors.horizontalCenter: parent.horizontalCenter 44 | text: "این یک متن ارور است که باید ببینید" 45 | color: "#FFFFFF" 46 | font.pixelSize: 15 47 | font.weight: Font.DemiBold 48 | } 49 | Label { 50 | id: notifier_solution 51 | visible: text !== "" 52 | anchors.horizontalCenter: parent.horizontalCenter 53 | // text: "این یک متن راهنماست" 54 | text: "" 55 | color: notifier_text.color 56 | font.family: notifier_text.font.family 57 | font.pixelSize: 14 58 | font.weight: Font.DemiBold 59 | } 60 | } 61 | 62 | 63 | } 64 | 65 | states: [ 66 | State { 67 | name: "visible" 68 | PropertyChanges { 69 | target: root 70 | visible: true 71 | opacity: 1.0 72 | y: showType === Notifier.ShowType.DownToUp ? root.parent.height - root.height - 20 : root.y 73 | x: showType === Notifier.ShowType.LeftToRight ? 20 : root.x 74 | } 75 | // start fading out slowly 76 | PropertyChanges { 77 | target: fade_animation 78 | running: true 79 | } 80 | }, 81 | State { 82 | name: "not-visible" 83 | PropertyChanges { 84 | target: root 85 | opacity: 0.0 86 | visible: false 87 | } 88 | } 89 | ] 90 | 91 | transitions: [ 92 | Transition { 93 | from: "not-visible" 94 | NumberAnimation { 95 | target: root 96 | property: "opacity" 97 | duration: 200 98 | from: 0.0 99 | easing.type: Easing.InOutQuad 100 | } 101 | 102 | NumberAnimation { 103 | target: root 104 | property: showType === Notifier.ShowType.DownToUp ? "y" : "x" 105 | from: distance 106 | duration: 200 107 | easing.type: Easing.OutElastic 108 | } 109 | } 110 | ] 111 | 112 | 113 | SequentialAnimation { 114 | id: fade_animation 115 | NumberAnimation { 116 | target: root 117 | property: "opacity" 118 | duration: 3200 119 | from: 1.0 120 | to: 0.0 121 | easing.type: Easing.InOutQuad 122 | } 123 | PropertyAnimation { 124 | target: root 125 | property: "visible" 126 | to: false 127 | duration: 100 // no effect 128 | } 129 | } 130 | 131 | MouseArea { 132 | id: mouse_area 133 | anchors.fill: parent 134 | cursorShape: Qt.PointingHandCursor 135 | hoverEnabled: true 136 | // stop fading when mouse got into area 137 | onEntered: { 138 | fade_animation.stop() 139 | root.opacity = 1.0 140 | } 141 | onExited: fade_animation.start() 142 | onClicked: root.state = "not-visible" 143 | } 144 | } 145 | 146 | -------------------------------------------------------------------------------- /qml/Controls/PageBase.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a Base for creating pages components which has integrated 3 | * with ViewManager component. 4 | * This component check the activeness of the page and will tell the ViewManager to destroy the page 5 | * after certain time of inactivity. 6 | */ 7 | 8 | import QtQuick 2.15 9 | import QtQuick.Controls 2.15 10 | 11 | Page { 12 | id: root 13 | 14 | // indicate if the Page should never destroy. 15 | property bool noDestruct: false 16 | property bool __isActive: true 17 | // unique id of the page in ViewManager 18 | property var __viewManUid 19 | 20 | // Notify the timer ending. 21 | signal __timedOut(var uid) 22 | 23 | on__IsActiveChanged: { 24 | if (__isActive === false && noDestruct === false) { 25 | __destruction_timer.restart() 26 | } else { 27 | __destruction_timer.stop() 28 | } 29 | } 30 | 31 | // Timer for measuring inactivity of page. 32 | Timer { 33 | id: __destruction_timer 34 | interval: 15000 35 | onTriggered: root.__timedOut(root.__viewManUid) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /qml/Controls/Plot.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * Plot component for drawing a Bar plot 3 | * this component will be rewritten. 4 | */ 5 | 6 | // TODO: rewrite this component 7 | import QtQuick 2.15 8 | import QtCharts 2.15 9 | 10 | ChartView { 11 | id: root 12 | title: "معدل ترمی" 13 | titleColor: "#FFFFFF" 14 | titleFont.family: fontFamily 15 | titleFont.weight: Font.Black 16 | antialiasing: true 17 | legend.visible: false 18 | locale: Qt.locale("fa_IR") 19 | localizeNumbers: true 20 | animationDuration: 500 21 | animationOptions: ChartView.SeriesAnimations 22 | backgroundColor: "transparent" 23 | 24 | property string fontFamily: "Arial" 25 | property alias xAxis: axis_x.categories 26 | property alias yAxis: bar_set.values 27 | 28 | BarCategoryAxis { 29 | id: axis_x 30 | color: "#FFFFFF" 31 | gridVisible: false 32 | labelsColor: "#FFFFFF" 33 | labelsFont.family: root.fontFamily 34 | labelsFont.weight: Font.Black 35 | lineVisible: false 36 | } 37 | 38 | ValueAxis { 39 | id: axis_y 40 | color: "#FFFFFF" 41 | gridLineColor: "#FFFFFF" 42 | labelsColor: "#FFFFFF" 43 | labelsFont.family: root.fontFamily 44 | labelsFont.weight: Font.Black 45 | max: 20 46 | min: 10 47 | // gridVisible: false 48 | } 49 | 50 | BarSeries { 51 | id: bar_serie 52 | axisX: axis_x 53 | axisY: axis_y 54 | barWidth: 0.35 55 | labelsVisible: true 56 | labelsPosition: AbstractBarSeries.LabelsInsideEnd 57 | BarSet { 58 | id: bar_set 59 | borderColor: "transparent" 60 | borderWidth: 0 61 | color: "#19B99A" 62 | labelColor: "#FFFFFF" 63 | labelFont.family: root.fontFamily 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /qml/Controls/ScreenShot.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * A component for do screenshot and save it as a image. 3 | */ 4 | 5 | import QtQuick 2.15 6 | import QtQuick.Dialogs 1.3 7 | 8 | FileDialog { 9 | id: file_dialog 10 | selectExisting: false 11 | selectMultiple: false 12 | folder: shortcuts.pictures 13 | // callback should be a function to run after a successful image saving 14 | property var callback 15 | property var exclude_item 16 | property var item_to_save 17 | 18 | onAccepted: { 19 | exclude_item.visible = false 20 | item_to_save.grabToImage(function(result) { 21 | let path; 22 | if (Qt.platform.os === "windows") 23 | path = String(file_dialog.fileUrl).replace("file://", "").substring(1) 24 | else 25 | path = String(file_dialog.fileUrl).replace("file://", "") 26 | 27 | result.saveToFile(path) 28 | callback() 29 | }); 30 | } 31 | 32 | function saveItem(item, exclude) { 33 | item_to_save = item 34 | exclude_item = exclude 35 | file_dialog.open() 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /qml/Controls/ViewManager.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * A component that is responsible to manage the view system(StackView in here). 3 | * This component is able to remove a object(PageBase) from the StackView but keep 4 | * the page instead of destroying it. This enables us to restore the already-created page. 5 | * So no need to re-create the object. 6 | * Also, the objects would destroy only if they time gets up. 7 | */ 8 | 9 | import QtQuick 2.15 10 | import QtQuick.Controls 2.15 11 | 12 | StackView { 13 | id: root 14 | 15 | property var __objects: ({}) 16 | 17 | function showPage(comp, comp_name, enable_cache = true, is_url = true) 18 | { 19 | if (root.__objects.hasOwnProperty(comp_name)) { 20 | root.currentItem.__isActive = false 21 | root.__objects[comp_name].__isActive = true 22 | root.replace(root.__objects[comp_name], StackView.PushTransition) 23 | return; 24 | } 25 | 26 | if (is_url === true) 27 | comp = Qt.createComponent(comp) 28 | 29 | var obj = comp.createObject(root.parent, {__viewManUid: comp_name}) 30 | obj.__timedOut.connect(root.removePage) 31 | 32 | __objects[comp_name] = obj 33 | 34 | var current_obj = root.currentItem 35 | root.replace(obj, StackView.PushTransition) 36 | 37 | // if the page noted that it should not be cached, remove it. 38 | if (enable_cache) 39 | current_obj.__isActive = false 40 | else 41 | removePage(current_obj.__viewManUid) 42 | } 43 | 44 | // just push the component 45 | function rawPush(comp, comp_name, is_url = true) 46 | { 47 | if (is_url === true) 48 | comp = Qt.createComponent(comp) 49 | 50 | var obj = comp.createObject(root.parent, {__viewManUid: comp_name}) 51 | __objects[comp_name] = obj 52 | 53 | root.push(obj) 54 | } 55 | 56 | // replace current item with {{comp}} and remove every 57 | // other existing component in {{__objects}} 58 | function rawPushReset(comp, comp_name, is_url = true) 59 | { 60 | if (is_url === true) 61 | comp = Qt.createComponent(comp) 62 | 63 | var obj = comp.createObject(root.parent, {__viewManUid: comp_name}) 64 | root.replace(obj, StackView.PushTransition) 65 | 66 | for (var uid in __objects) { 67 | removePage(uid); 68 | } 69 | __objects[comp_name] = obj 70 | 71 | } 72 | 73 | function removePage(uid) 74 | { 75 | if (__objects.hasOwnProperty(uid)) { 76 | __objects[uid].destroy() 77 | delete __objects[uid] 78 | } 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /qml/Helpers/ErrorHandler.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * This component are responsible to run error showing mechanism 3 | */ 4 | 5 | import QtQuick 2.15 6 | import API.Errors 1.0 7 | 8 | Error { 9 | id: root 10 | // the view system which manage pages (like stackview) 11 | required property var viewItem 12 | // the SideBar component 13 | property var sideBar 14 | // error type for using in universal errors 15 | property var errorType: ErrorHandler.Critical 16 | 17 | function raiseError(caller_object, callback_function = {}, notifier = undefined, force_callback = false) 18 | { 19 | var critical_status = caller_object.errorType 20 | // check if error is normal then just notify to the user 21 | if (critical_status === ErrorHandler.Normal && notifier !== undefined) { 22 | notifier.text = caller_object.getErrorString() 23 | notifier.solution = caller_object.getErrorSolution() 24 | notifier.show() 25 | // call callback_function() if we are forced to 26 | if (force_callback) 27 | callback_function(); 28 | return; 29 | } 30 | 31 | viewItem.push("../Pages/ErrorPage.qml", 32 | { 33 | "error_msg" : caller_object.getErrorString(), 34 | "error_solution" : caller_object.getErrorSolution(), 35 | "criticalStatus" : critical_status, 36 | "sideBarItem" : sideBar, 37 | "callback_function": function() { 38 | callback_function() 39 | viewItem.pop() 40 | } 41 | }) 42 | } 43 | 44 | // set error_code to the internal Error object data-member 45 | // and call raiseError with 'this' as a caller_object 46 | function raiseUniversalError(error_code) 47 | { 48 | root.setErrorCode(error_code) 49 | raiseError(this); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /qml/Helpers/ErrorRectangle.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * Just a rectangle to show message about the unavailablity of data. 3 | */ 4 | 5 | import QtQuick 2.15 6 | import QtQuick.Controls 2.15 7 | 8 | Rectangle { 9 | id: root 10 | 11 | property string name: "" 12 | 13 | color: "#1D2025" 14 | z: 1 15 | Label { 16 | width: parent.width 17 | height: parent.height 18 | wrapMode: Label.WordWrap 19 | font.family: regular_font.name 20 | horizontalAlignment: Label.AlignHCenter 21 | verticalAlignment: Label.AlignVCenter 22 | text: qsTr("متاسفانه اطلاعات %1 موجود نیست! :(").arg(root.name) 23 | color: "white" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /qml/Helpers/SettingsPage/About.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a Helper for SettingsPage.qml 3 | * In this page we write some information about Boostan project. 4 | */ 5 | 6 | import QtQuick 2.15 7 | import QtQuick.Controls 2.15 8 | import "../../Controls" 9 | 10 | Item { 11 | 12 | Column { 13 | width: parent.width - 40 14 | height: parent.height - 40 15 | anchors.centerIn: parent 16 | spacing: 10 17 | Label { 18 | id: about_boostan 19 | width: parent.width 20 | wrapMode: Label.WordWrap 21 | color: "#FCFCFC" 22 | font.family: regular_font.name 23 | font.pixelSize: 16 24 | text: "بوستان یک کارخواه(کلاینت) آزاد برای سامانهٔ دانشگاهی گلستان است." 25 | } 26 | 27 | // separator 28 | Rectangle { 29 | width: parent.width 30 | height: 1 31 | color: "#262A2F" 32 | } 33 | 34 | Label { 35 | width: parent.width 36 | wrapMode: Label.WordWrap 37 | color: "#FCFCFC" 38 | font.family: regular_font.name 39 | font.pixelSize: 16 40 | text: "نسخه: " + Version 41 | } 42 | 43 | // separator 44 | Rectangle { 45 | width: parent.width 46 | height: 1 47 | color: "#262A2F" 48 | } 49 | 50 | Label { 51 | width: parent.width 52 | wrapMode: Label.WordWrap 53 | color: "#FCFCFC" 54 | font.family: regular_font.name 55 | font.pixelSize: 16 56 | text: "برای اطلاعات بیشتر روی لینک کلیک کنید" 57 | } 58 | 59 | ClickableText { 60 | width: parent.width 61 | wrapMode: Label.WordWrap 62 | font.family: regular_font.name 63 | font.pixelSize: 16 64 | text: "https://seedpuller.github.io/Boostan-Desktop/" 65 | onClicked: { 66 | Qt.openUrlExternally("https://seedpuller.github.io/Boostan-Desktop/") 67 | } 68 | } 69 | 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /qml/Helpers/SettingsPage/BoostanSettings.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a Helper for SettingsPage.qml 3 | * In this Section we handle changing Boostan client configurations 4 | */ 5 | 6 | import QtQuick 2.15 7 | import QtQuick.Controls 2.15 8 | import API.Settings 1.0 9 | import "../../Controls" 10 | 11 | Item { 12 | Row { 13 | width: parent.width 14 | height: parent.height 15 | spacing: 0 16 | Column { 17 | width: parent.width / 2 18 | height: parent.height - 50 19 | anchors.verticalCenter: parent.verticalCenter 20 | spacing: 20 21 | MySwitch { 22 | anchors.horizontalCenter: parent.horizontalCenter 23 | width: 110 24 | height: 35 25 | checked: Settings.getValue("logging", true) === "true" ? true : false 26 | 27 | onCheckedChanged: { 28 | Settings.setValue("logging", checked, true) 29 | } 30 | } 31 | } 32 | 33 | Column { 34 | width: parent.width / 2 35 | height: parent.height - 50 36 | anchors.verticalCenter: parent.verticalCenter 37 | spacing: 20 38 | 39 | Label { 40 | anchors.horizontalCenter: parent.horizontalCenter 41 | verticalAlignment: Qt.AlignVCenter 42 | text: "ذخیره رویدادها" 43 | color: "#FCFCFC" 44 | font.family: regular_font.name 45 | font.weight: Font.Bold 46 | height: 35 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /qml/Helpers/SettingsPage/GolestanSettings.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a Helper for SettingsPage.qml 3 | * In this Section we handle changing Golestan account credentials 4 | */ 5 | 6 | import QtQuick 2.15 7 | import QtQuick.Layouts 1.15 8 | import QtQuick.Controls 2.15 9 | import API.Settings 1.0 10 | import API.AccountHandler 1.0 11 | 12 | import "../../Controls" 13 | 14 | Item { 15 | 16 | AccountHandler { 17 | id: account_handler 18 | property bool unameChanged: false 19 | property string newUsername 20 | property string newPassword 21 | 22 | onFinished: { 23 | right_pane.enableNavigator() 24 | submit_loading.visible = false 25 | 26 | if (!success) { 27 | error_handler.raiseError(this, function(){}, notifier) 28 | return; 29 | } 30 | notifier.text = "اطلاعات با موفقیت تغییر پیدا کردند!" 31 | notifier.solution = "" 32 | notifier.show() 33 | if (Settings.getValue("username", true) === universal_storage.username) { 34 | Settings.setValue("password", newPassword, true); 35 | if (unameChanged) 36 | Settings.setValue("username", newUsername, true); 37 | } 38 | 39 | if (unameChanged) { 40 | universal_storage.username = newUsername 41 | } 42 | } 43 | } 44 | 45 | ColumnLayout { 46 | width: parent.width - 10 47 | height: parent.height - 20 48 | anchors.centerIn: parent 49 | spacing: 0 50 | MyTextInput { 51 | id: currpass_inp 52 | Layout.alignment: Qt.AlignHCenter | Qt.AlignTop 53 | Layout.topMargin: 20 54 | width: 240 55 | height: 40 56 | Layout.preferredHeight: 40 57 | direction: Qt.RightToLeft 58 | placeHolder: "رمز عبور فعلی" 59 | mode: TextInput.Password 60 | icon: "\ue800" // profile icon 61 | iconSize: 24 62 | } 63 | 64 | MyTextInput { 65 | id: newpass_inp 66 | Layout.alignment: Qt.AlignHCenter | Qt.AlignTop 67 | width: 240 68 | height: 40 69 | Layout.preferredHeight: 40 70 | direction: Qt.RightToLeft 71 | placeHolder: "رمز عبور جدید" 72 | mode: TextInput.Password 73 | icon: "\ue80d" // lock icon 74 | iconSize: 24 75 | } 76 | 77 | MyTextInput { 78 | id: newpass_re_inp 79 | Layout.alignment: Qt.AlignHCenter 80 | width: 240 81 | height: 40 82 | Layout.preferredHeight: 40 83 | direction: Qt.RightToLeft 84 | placeHolder: "تکرار رمز عبور جدید" 85 | mode: TextInput.Password 86 | icon: "\ue80d" // lock icon 87 | iconSize: 24 88 | } 89 | 90 | Row { 91 | Layout.alignment: Qt.AlignRight 92 | Layout.rightMargin: 50 93 | Layout.preferredHeight: 30 94 | MySwitch { 95 | id: change_un 96 | anchors.verticalCenter: parent.verticalCenter 97 | width: 110 98 | height: 35 99 | } 100 | 101 | Label { 102 | anchors.verticalCenter: parent.verticalCenter 103 | text: "تغییر نام کاربری" 104 | color: "#FCFCFC" 105 | } 106 | } 107 | 108 | MyTextInput { 109 | id: new_username_inp 110 | visible: change_un.checked 111 | Layout.alignment: Qt.AlignHCenter 112 | width: 240 113 | height: 40 114 | Layout.preferredHeight: 40 115 | direction: Qt.RightToLeft 116 | placeHolder: "نام کاربری جدید" 117 | icon: "\ue805" // person icon 118 | iconSize: 24 119 | } 120 | 121 | MyButton { 122 | id: submit_button 123 | Layout.alignment: Qt.AlignHCenter 124 | Layout.preferredWidth: 240 125 | Layout.preferredHeight: 50 126 | text: "ثبت" 127 | color: "#FCFCFC" 128 | bgColor: "#19B99A" 129 | radius: 5 130 | font.pixelSize: 15 131 | font.family: regular_font.name 132 | font.weight: Font.Bold 133 | onClicked: { 134 | 135 | if (currpass_inp.isEmpty || newpass_inp.isEmpty || newpass_re_inp.isEmpty || (new_username_inp.isEmpty && change_un.checked)) { 136 | notifier.text = "ورودی ها نباید خالی باشن!" 137 | notifier.solution = "یک بار دیگه فرم رو بررسی کن و همه ورودی هارو پر کن" 138 | notifier.show() 139 | return; 140 | } 141 | 142 | if (newpass_inp.text !== newpass_re_inp.text) { 143 | notifier.text = "رمز های جدید باهم مطابقت ندارن!" 144 | notifier.solution = "رمز جدید و تکرارش رو بررسی کن" 145 | notifier.show() 146 | return; 147 | } 148 | 149 | if (currpass_inp.text === newpass_inp.text) { 150 | notifier.text = "رمز فعلیتو نباید بجای رمز جدید وارد کنی!" 151 | notifier.solution = "نمیشه رمز فعلی‌ت رو بجای رمز فعلی‌ت بذاری که آخه :))" 152 | notifier.show() 153 | return; 154 | } 155 | 156 | submit_loading.visible = true 157 | right_pane.disableNavigator() 158 | 159 | account_handler.newPassword = newpass_inp.text 160 | if (!change_un.checked) { 161 | account_handler.unameChanged = false 162 | account_handler.changeCreds(universal_storage.username, currpass_inp.text, newpass_inp.text) 163 | } else { 164 | account_handler.unameChanged = true; 165 | account_handler.newUsername = new_username_inp.text 166 | account_handler.changeCreds(universal_storage.username, currpass_inp.text, newpass_inp.text, new_username_inp.text) 167 | } 168 | } 169 | 170 | Rectangle { 171 | // the visiblity of this component is managed by 172 | // account_handler.onFinished and submit_button.onCLicked 173 | id: submit_loading 174 | visible: false 175 | anchors.fill: submit_button 176 | color: "#19B99A" 177 | LoadingAnimationPulse { 178 | anchors.centerIn: parent 179 | running: parent.visible 180 | barCount: 3 181 | color: "#FAFAFA" 182 | width: 40 183 | height: 25 184 | } 185 | 186 | MouseArea { 187 | anchors.fill: parent 188 | } 189 | } 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /qml/Helpers/SideBar.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * Side bar component 3 | * Show list of options (objects in 'options') and show them by click. 4 | */ 5 | 6 | import QtQuick 2.15 7 | import QtQuick.Controls 2.15 8 | import QtQuick.Layouts 1.15 9 | import "../Controls" 10 | 11 | Rectangle { 12 | id: side_bar 13 | color: "#232429" 14 | anchors.left: stackview.right 15 | width: enabled ? 280 : 0 16 | height: mainwindow.height 17 | 18 | // List of options which are going to be shown. 19 | default property list options 20 | 21 | property string studentName: universal_storage.studentName 22 | property real itemSize: width * 0.06 23 | // Index of current option in 'options' 24 | property int currentOption: -1 25 | property bool disableNav: false 26 | 27 | /** Functions **/ 28 | 29 | function toOption(option, cache_current = true) { 30 | if (currentOption < 0) { 31 | currentOption = option 32 | } else { 33 | repeater.itemAt(currentOption).isEnable = false 34 | currentOption = option 35 | } 36 | repeater.itemAt(option).isEnable = true 37 | 38 | stackview.showPage(side_bar.options[option].componentPath, side_bar.options[option].componentPath, cache_current) 39 | } 40 | 41 | function enableNavigator() 42 | { 43 | side_bar.disableNav = false 44 | } 45 | 46 | function disableNavigator() 47 | { 48 | side_bar.disableNav = true 49 | } 50 | 51 | ColumnLayout { 52 | visible: side_bar.enabled 53 | anchors.left: parent.left 54 | anchors.top: parent.top 55 | anchors.topMargin: 20 56 | width: parent.width - side_bar_right.width + 5 57 | height: parent.height 58 | spacing: 0 59 | Label { 60 | Layout.alignment: Qt.AlignHCenter 61 | Layout.leftMargin: 10 62 | font.family: "Mj_Afsoon" 63 | font.pixelSize: 55 64 | color: "#19B99A" 65 | text: "بوستان" 66 | } 67 | 68 | Item { 69 | height: 18 70 | } 71 | 72 | Label { 73 | id: student_name 74 | Layout.alignment: Qt.AlignHCenter 75 | font.family: regular_font.name 76 | font.pixelSize: 12 77 | color: "#F8F7F2" 78 | text: side_bar.studentName 79 | } 80 | 81 | Label { 82 | id: today_date 83 | Layout.alignment: Qt.AlignHCenter 84 | Layout.topMargin: 10 85 | text: TodayDate 86 | font.pixelSize: 12 87 | font.family: regular_font.name 88 | color: "#F8F7F2" 89 | } 90 | 91 | Item { 92 | height: 20 93 | } 94 | 95 | // Options shown here 96 | Item { 97 | Layout.fillWidth: true 98 | Layout.fillHeight: true 99 | Column { 100 | width: parent.width 101 | height: parent.height 102 | Repeater { 103 | id: repeater 104 | model: options 105 | delegate: SideBarDelegate { 106 | width: parent.width 107 | height: 40 108 | onClicked: side_bar.toOption(index) 109 | Icon { 110 | id: sidebar_elem_icon 111 | anchors.right: parent.right 112 | anchors.rightMargin: 20 113 | anchors.verticalCenter: parent.verticalCenter 114 | text: model.iconText 115 | color: parent.isEnable ? "#19B99A" : "#F8F7F2" 116 | width: 20 117 | horizontalAlignment: Text.AlignHCenter 118 | 119 | } 120 | 121 | Label { 122 | anchors.right: sidebar_elem_icon.left 123 | anchors.rightMargin: 20 124 | anchors.verticalCenter: parent.verticalCenter 125 | text: model.title 126 | color: parent.isEnable ? "#19B99A" : "#F8F7F2" 127 | font.family: regular_font.name 128 | font.pixelSize: itemSize 129 | font.bold: true 130 | } 131 | } 132 | } 133 | } 134 | 135 | MouseArea { 136 | id: navigator_disabler 137 | enabled: side_bar.disableNav 138 | anchors.fill: parent 139 | cursorShape: enabled ? Qt.BusyCursor : Qt.ArrowCursor 140 | hoverEnabled: enabled 141 | } 142 | } 143 | } 144 | 145 | Rectangle { 146 | id: side_bar_right 147 | anchors.right: side_bar.right 148 | anchors.rightMargin: -7 149 | width: enabled ? 70 : 0 150 | height: side_bar.height 151 | radius: 10 152 | color: "#19B99A" 153 | ColumnLayout { 154 | anchors.right: parent.right 155 | anchors.rightMargin: 13 156 | anchors.top: parent.top 157 | anchors.topMargin: 15 158 | width: parent.width - 20 159 | 160 | Icon { 161 | id: logout_icon 162 | Layout.alignment: Qt.AlignRight 163 | font.pixelSize: 15 164 | font.weight: Font.Thin 165 | text: "\ue803" // logout icon 166 | description: "خروج" 167 | color: "#262125" 168 | clickAble: true 169 | onClicked: { 170 | // close the sidebar and push login pages 171 | right_pane.enabled = false; 172 | stackview.rawPushReset("qrc:/Pages/LoginPage.qml", "qrc:/Pages/LoginPage.qml") 173 | } 174 | } 175 | 176 | Item { 177 | height: 10 178 | } 179 | 180 | Item { 181 | Layout.alignment: Qt.AlignHCenter 182 | width: 35 183 | height: 35 184 | Rectangle { 185 | id: attention_bg 186 | width: 35 187 | height: 35 188 | radius: 17 189 | color: "#ECEDFF" 190 | opacity: 0.63 191 | } 192 | Icon { 193 | text: "\ue804" // attention icon 194 | anchors.centerIn: attention_bg 195 | font.weight: Font.Thin 196 | font.pixelSize: 17 197 | } 198 | } 199 | 200 | Item { 201 | Layout.alignment: Qt.AlignHCenter 202 | width: 35 203 | height: 35 204 | Rectangle { 205 | id: attention_circle_bg 206 | width: 35 207 | height: 35 208 | radius: 17 209 | color: "#ECEDFF" 210 | opacity: 0.63 211 | } 212 | Icon { 213 | text: "\ue807" // attention circle icon 214 | anchors.centerIn: attention_circle_bg 215 | font.weight: Font.Thin 216 | font.pixelSize: 17 217 | } 218 | } 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /qml/Helpers/SideBarDelegate.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * A SideBar options delegation implementation 3 | */ 4 | 5 | import QtQuick 2.15 6 | 7 | Item { 8 | id: root 9 | property alias isEnable: control.enabled 10 | signal clicked 11 | Rectangle { 12 | id: control 13 | anchors.fill: parent 14 | enabled: false 15 | color: "transparent" 16 | 17 | } 18 | 19 | // show a shadow when a item is in selected mode or has a mouse hovered on 20 | Rectangle { 21 | id: shadow 22 | width: root.width 23 | height: root.height 24 | anchors.right: root.right 25 | color: "#F8F7F2" 26 | opacity: control.enabled || mouse_area.containsMouse ? 0.08 : 0.0 27 | Behavior on opacity { 28 | NumberAnimation { 29 | duration: 200 30 | easing.type: Easing.InOutQuad 31 | } 32 | } 33 | 34 | NumberAnimation { 35 | id: click_anim 36 | target: shadow 37 | property: "width" 38 | from: 0 39 | to: root.width 40 | duration: 200 41 | easing.type: Easing.InOutQuad 42 | } 43 | } 44 | 45 | // indicator that determine a item is in selected mode 46 | Rectangle { 47 | id: enable_indicator 48 | anchors.left: control.left 49 | visible: control.enabled 50 | width: 3 51 | height: control.height 52 | color: "#19B99A" 53 | } 54 | 55 | MouseArea { 56 | id: mouse_area 57 | anchors.fill: parent 58 | hoverEnabled: true 59 | cursorShape: Qt.PointingHandCursor 60 | onClicked: { 61 | if (!control.enabled) { 62 | click_anim.start() 63 | root.clicked() 64 | } 65 | } 66 | } 67 | } 68 | 69 | -------------------------------------------------------------------------------- /qml/Helpers/SideBarItem.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * A SideBar options Item. 3 | * store necessary information about an option in SideBar. 4 | */ 5 | 6 | import QtQuick 2.15 7 | 8 | QtObject { 9 | required property string title 10 | required property string componentPath 11 | required property string iconText 12 | property var type: SideBarItem.Page 13 | enum Type 14 | { 15 | Page, 16 | PopUp 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /qml/Pages/ErrorPage.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * The page that show a error and run the callback_function 3 | */ 4 | 5 | import QtQuick 2.15 6 | import QtQuick.Controls 2.15 7 | import "../Controls" 8 | import "../Helpers" 9 | 10 | Page { 11 | id: error_page 12 | property alias error_msg: error_text.text 13 | property alias error_solution: error_solution.text 14 | property var callback_function: function(){} 15 | property int criticalStatus: ErrorHandler.SemiCritical 16 | property var sideBarItem 17 | 18 | /** Private property **/ 19 | property bool _sideBarItemVisiblity 20 | Component.onCompleted: { 21 | _sideBarItemVisiblity = sideBarItem.enabled 22 | sideBarItem.enabled = false 23 | } 24 | 25 | Rectangle { 26 | id: page_background 27 | anchors.fill: parent 28 | color: "#262125" 29 | Image { 30 | id: error_logo 31 | sourceSize.width: parent.width / 1.5 32 | sourceSize.height: parent.height / 1.2 33 | source: "qrc:/pics/error-logo.svg" 34 | anchors.centerIn: parent 35 | Rectangle { 36 | anchors.fill: parent 37 | color: "#262125" 38 | opacity: 0.5 39 | } 40 | } 41 | Label { 42 | id: eror_icon 43 | anchors.horizontalCenter: parent.horizontalCenter 44 | anchors.top: error_logo.top 45 | anchors.topMargin: 50 46 | text: "\ue802" // sad icon 47 | font.family: "fontello" 48 | font.pixelSize: error_logo.width / 4 49 | color: "#F8F7F2" 50 | } 51 | 52 | Label { 53 | id: error_text 54 | width: parent.width - 10 55 | horizontalAlignment: Label.AlignHCenter 56 | anchors.top: eror_icon.bottom 57 | anchors.topMargin: 40 58 | font.family: "Tanha" 59 | font.pixelSize: eror_icon.font.pixelSize / 4 60 | font.weight: Font.DemiBold 61 | text: "این یک متن ارور است" 62 | color: "#F8F7F2" 63 | wrapMode: Label.WordWrap 64 | } 65 | 66 | Label { 67 | id: error_solution 68 | width: parent.width - 10 69 | horizontalAlignment: Label.AlignHCenter 70 | anchors.top: error_text.bottom 71 | font.family: "Tanha" 72 | font.pixelSize: error_text.font.pixelSize / 2 73 | font.weight: Font.DemiBold 74 | text: "این یک توضیحات اضافه است" 75 | color: "#F8F7F2" 76 | wrapMode: Label.WordWrap 77 | } 78 | 79 | MyButton { 80 | id: retry_button 81 | anchors.horizontalCenter: parent.horizontalCenter 82 | anchors.top: error_solution.bottom 83 | anchors.topMargin: 10 84 | width: 200 85 | height: 60 86 | // this text seems reverse. correct is: if status == Critical, close page. else try again 87 | text: error_page.criticalStatus == ErrorHandler.Critical ? "بستن برنامه!" : "دوباره تلاش کن!" 88 | font.pixelSize: 20 89 | bgColor: error_page.criticalStatus == ErrorHandler.Critical ? "#E53935" : "#19B99A" 90 | radius: 5 91 | onClicked: { 92 | if (error_page.criticalStatus == ErrorHandler.Critical) { 93 | Qt.quit() 94 | return; 95 | } 96 | sideBarItem.enabled = _sideBarItemVisiblity 97 | callback_function() 98 | } 99 | 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /qml/Pages/LoginPage.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * This is Login Page. 3 | * In this page the first request will being sent(with init handler) 4 | * This page has no right_pane so the right_pane should be disabled when this page is visible. 5 | */ 6 | 7 | import QtQuick 2.15 8 | import QtQuick.Controls 2.15 9 | import QtGraphicalEffects 1.15 10 | import QtQuick.Layouts 1.15 11 | import API.InitHandler 1.0 12 | import API.LoginHandler 1.0 13 | import API.Settings 1.0 14 | import "../Controls" 15 | 16 | PageBase { 17 | id: login_page 18 | 19 | InitHandler { 20 | id: init_handler 21 | Component.onCompleted: start(); 22 | onFinished: { 23 | if (init_handler.success) 24 | captcha_handler.loadCaptcha(captcha_pic) 25 | else 26 | error_handler.raiseError(this, function() {init_handler.start()}) 27 | } 28 | } 29 | 30 | LoginHandler { 31 | id: login_handler 32 | onFinished: { 33 | // login handler job is finished so the submit_loading should not be visible 34 | submit_loading.visible = false 35 | if (login_handler.success) { 36 | let uname = username_input.text; 37 | if (remember_checkbox.checked) { 38 | Settings.setValue("username", uname, true) 39 | Settings.setValue("password", password_input.text, true) 40 | } 41 | universal_storage.studentName = getName() 42 | universal_storage.username = uname; 43 | right_pane.enabled = true; 44 | // redirect to dashboard page and don't cache current page 45 | right_pane.toOption(0, false); 46 | return; 47 | } 48 | error_handler.raiseError(this, function(){init_handler.start()}, notifier, true) 49 | } 50 | } 51 | 52 | CaptchaHandler { 53 | id: captcha_handler 54 | onFinished: { 55 | if(captcha_handler.success) { 56 | captcha_pic.source = "file:/" + ApplicationPath + "captcha.png" 57 | return 58 | } 59 | error_handler.raiseError(this, function(){captcha_handler.loadCaptcha(captcha_pic)}, notifier) 60 | } 61 | 62 | 63 | function loadCaptcha(cpic) { 64 | cpic.source = "file:/" + ApplicationPath + "/pic/captcha.png" 65 | getCaptcha() 66 | } 67 | } 68 | 69 | Rectangle { 70 | id: form_background 71 | anchors.fill: parent 72 | color: "#262125" 73 | 74 | Image { 75 | anchors.bottom: form_container_bg.top 76 | anchors.bottomMargin: -135 77 | anchors.horizontalCenter: parent.horizontalCenter 78 | source: "qrc:/pics/boostan-logo.svg" 79 | width: 400 80 | height: 320 81 | z: 1 82 | } 83 | 84 | Rectangle { 85 | id: form_container_bg 86 | anchors.bottom: parent.bottom 87 | anchors.horizontalCenter: parent.horizontalCenter 88 | width: parent.width * 0.35 89 | height: parent.height * 0.75 90 | color: "#19B99A" 91 | gradient: Gradient { 92 | GradientStop { color: "#262125"; position: 0.0 } 93 | GradientStop { color: "#19B99A"; position: 0.7 } 94 | } 95 | } 96 | 97 | Rectangle { 98 | id: form_container 99 | anchors.centerIn: form_container_bg 100 | width: 320 101 | height: 320 102 | radius: 20 103 | color: "#424242" 104 | } 105 | 106 | ColumnLayout { 107 | id: form_layout 108 | anchors.fill: form_container 109 | spacing: 0 110 | Item { 111 | Layout.preferredHeight: 10 112 | } 113 | 114 | MyTextInput { 115 | id: username_input 116 | Layout.alignment: Qt.AlignHCenter 117 | width: 240 118 | height: 40 119 | direction: Qt.RightToLeft 120 | placeHolder: "نام کاربری" 121 | icon: "\ue805" // profile icon 122 | text: Settings.getValue("username", true) ?? "" 123 | } 124 | 125 | MyTextInput { 126 | id: password_input 127 | Layout.alignment: Qt.AlignHCenter 128 | width: 240 129 | height: 40 130 | direction: Qt.RightToLeft 131 | placeHolder: "رمز عبور" 132 | mode: TextInput.Password 133 | icon: "\ue800" // profile icon 134 | iconSize: 24 135 | text: Settings.getValue("password", true) ?? "" 136 | } 137 | 138 | /* 139 | * Captcha Layout 140 | */ 141 | RowLayout { 142 | Layout.alignment: Qt.AlignHCenter 143 | Layout.maximumWidth: 240 144 | 145 | Icon { 146 | id: reload_icon 147 | Layout.leftMargin: -15 148 | color: "#EEEEEE" 149 | text: "\ue801" // reload icon 150 | clickAble: true 151 | onClicked: { 152 | rotate.start() 153 | captcha_handler.loadCaptcha(captcha_pic) 154 | } 155 | 156 | RotationAnimation { 157 | id: rotate 158 | target: reload_icon 159 | from: 0 160 | to: 360 161 | duration: 400 162 | easing.type: Easing.InOutQuad 163 | } 164 | 165 | } 166 | 167 | Image { 168 | id: captcha_pic 169 | Layout.preferredWidth: 110 170 | Layout.preferredHeight: 40 171 | cache: false 172 | source: "file:/" + ApplicationPath + "/pic/captcha.png" 173 | LoadingAnimationColor { 174 | id: captcha_loading 175 | anchors.fill: captcha_pic 176 | visible: captcha_handler.working 177 | } 178 | } 179 | 180 | MyTextInput { 181 | id: captcha_input 182 | Layout.fillWidth: true 183 | height: 40 184 | placeHolder: "تصویر امنیتی" 185 | horizontalAlignment: TextInput.AlignHCenter 186 | } 187 | } 188 | 189 | /* 190 | * Remember me Layout 191 | */ 192 | RowLayout { 193 | Layout.alignment: Qt.AlignHCenter 194 | Layout.maximumWidth: 240 195 | Layout.maximumHeight: 20 196 | Layout.bottomMargin: 20 197 | Layout.topMargin: -10 198 | layoutDirection: Qt.RightToLeft 199 | spacing: 0 200 | CheckBox { 201 | id: remember_checkbox 202 | Layout.preferredWidth: 15 203 | } 204 | Label { 205 | Layout.rightMargin: -15 206 | text: "مرا به خاطر بسپار" 207 | font.weight: Font.Bold 208 | color: "#EEEEEE" 209 | font.family: regular_font.name 210 | } 211 | } 212 | 213 | MyButton { 214 | id: submit_button 215 | enabled: captcha_handler.finished && captcha_handler.success 216 | Layout.alignment: Qt.AlignHCenter 217 | Layout.preferredWidth: 240 218 | Layout.preferredHeight: 50 219 | text: "ورود" 220 | bgColor: "#19B99A" 221 | radius: 5 222 | font.pixelSize: 15 223 | font.family: regular_font.name 224 | onClicked: { 225 | if (username_input.isEmpty || password_input.isEmpty || captcha_input.isEmpty) { 226 | notifier.text = "ورودی ها نباید خالی باشن!" 227 | notifier.solution = "یک بار دیگه فرم رو بررسی کن و همه ورودی هارو پر کن" 228 | notifier.show() 229 | return 230 | } 231 | login_handler.tryLogin(username_input.text, password_input.text, captcha_input.text) 232 | submit_loading.visible = true 233 | } 234 | 235 | Rectangle { 236 | // the visiblity of this component is managed by 237 | // login_handler.onFinished and submit_button.onCLicked 238 | id: submit_loading 239 | visible: false 240 | anchors.fill: submit_button 241 | color: "#19B99A" 242 | LoadingAnimationPulse { 243 | anchors.centerIn: parent 244 | running: parent.visible 245 | barCount: 3 246 | color: "#FAFAFA" 247 | width: 40 248 | height: 25 249 | } 250 | 251 | MouseArea { 252 | anchors.fill: parent 253 | } 254 | } 255 | } 256 | 257 | Item { 258 | Layout.preferredHeight: 10 259 | } 260 | } 261 | } 262 | 263 | Notifier { 264 | id: notifier 265 | anchors.horizontalCenter: parent.horizontalCenter 266 | bgColor: "#262125" 267 | font.family: regular_font.name 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /qml/Pages/ScoresPage.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a scores page for student 3 | * In this page, the users can view their scores and their status per semesters. 4 | */ 5 | 6 | import QtQuick 2.15 7 | import QtQuick.Controls 2.15 8 | import QtQuick.Layouts 1.15 9 | import API.ScoresHandler 1.0 10 | import "../Controls" 11 | import "../Helpers" 12 | 13 | PageBase { 14 | id: scores_page 15 | 16 | ScoresHandler { 17 | id: scores_handler 18 | 19 | Component.onCompleted: { 20 | right_pane.disableNavigator() 21 | 22 | /** Prepare a model for using in combo box 'btn_select_semester' **/ 23 | 24 | // our model storage 25 | let combo_model = [] 26 | let currentSem = universal_storage.currentSemester 27 | // the semester is like 3,xxx. we just need the 'x's. thus we used slice(2) 28 | let currentSemText = "نیمسال " + Number(currentSem).toLocaleString(Qt.locale("fa_IR"), "f", 0).slice(2) 29 | 30 | for (var i = 0; i < universal_storage.semesters.length; ++i) { 31 | let sem_value = universal_storage.semesters[i]; 32 | let sem_text = "نیمسال " + Number(sem_value).toLocaleString(Qt.locale("fa_IR"), "f", 0).slice(2); 33 | combo_model.push({"value": sem_value, "text": sem_text}) 34 | } 35 | combo_model.push({"value": currentSem, "text": currentSemText}) 36 | btn_select_semester.model = combo_model; 37 | btn_select_semester.currentIndex = universal_storage.semesters.length 38 | 39 | start(currentSem, universal_storage.studentUid) 40 | } 41 | 42 | onFinished: { 43 | right_pane.enableNavigator() 44 | if (!success) { 45 | error_handler.raiseError(this, function(){scores_handler.start(universal_storage.currentSemester, universal_storage.studentUid)}, notifier) 46 | return; 47 | } 48 | 49 | scores_table.model = getScores(); 50 | brief_scores_table.model = getBriefScores() 51 | } 52 | 53 | } 54 | 55 | Rectangle { 56 | id: page_background 57 | anchors.fill: parent 58 | color: "#262A2F" 59 | } 60 | 61 | Notifier { 62 | id: notifier 63 | showType: Notifier.ShowType.LeftToRight 64 | anchors.top: parent.top 65 | anchors.topMargin: 50 66 | z: 2 67 | font.family: regular_font.name 68 | bgColor: "#E65100" 69 | } 70 | 71 | MyComboBox { 72 | enabled: !scores_handler.working 73 | id: btn_select_semester 74 | anchors.right: parent.right 75 | anchors.rightMargin: 20 76 | y: 20 77 | width: 170 78 | height: 60 79 | popupMaxHeight: 200 80 | comboItem.textRole: "text" 81 | comboItem.valueRole: "value" 82 | 83 | model: 0 84 | 85 | onActivated: { 86 | // request for selected semester scores 87 | right_pane.disableNavigator() 88 | scores_handler.getScoresOf(currentValue) 89 | } 90 | 91 | } 92 | 93 | ColumnLayout { 94 | id: layout 95 | // just show whenever the data is completely ready 96 | visible: !scores_handler.working && scores_handler.success 97 | width: parent.width - 40 98 | // 30 is the btn_select_semester.y + 10 and 40 is our specific margin 99 | height: parent.height - btn_select_semester.height - 30 - 40 100 | anchors.top: btn_select_semester.bottom 101 | anchors.topMargin: 15 102 | anchors.horizontalCenter: parent.horizontalCenter 103 | spacing: 0 104 | 105 | MyTableView { 106 | id: scores_table 107 | Layout.preferredWidth: parent.width * 0.85 108 | Layout.maximumHeight: parent.height * 0.8 109 | Layout.alignment: Qt.AlignTop | Qt.AlignHCenter 110 | 111 | autoHeight: true 112 | choosable: false 113 | model: [] 114 | columnRoles: ["name", "weight", "score", "status"] 115 | columnWidthRatios: [0.5, 0.1, 0.15, 0.25] 116 | columnTitles: ["درس", "واحد","نمره", "وضعیت"] 117 | columnItem: scores_table_cell 118 | rowHeight: 55 119 | } 120 | 121 | MyTableView { 122 | id: brief_scores_table 123 | Layout.preferredWidth: parent.width * 0.9 124 | Layout.preferredHeight: 100 125 | Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter 126 | 127 | interactive: false 128 | choosable: false 129 | model: [] 130 | columnRoles: ["average", "semesterUnits", "passedUnits", "totalAvg", "totalPassedUnits"] 131 | columnWidthRatios: [0.2, 0.2, 0.2, 0.15, 0.25] 132 | columnTitles: ["معدل این ترم", "چند واحد گرفتی", "چند واحد پاس شدی", "معدل کل", "کلا چند واحد پاس کردی"] 133 | rowHeight: 50 134 | 135 | } 136 | 137 | } 138 | 139 | // loading animations and error sections are here 140 | Item { 141 | anchors.fill: layout 142 | LoadingAnimationColor { 143 | id: scores_loading_anim 144 | width: parent.width * 0.87 145 | height: parent.height * 0.8 146 | radius: 5 147 | visible: scores_handler.working 148 | anchors.horizontalCenter: parent.horizontalCenter 149 | } 150 | ErrorRectangle { 151 | id: scores_err_rec 152 | visible: !scores_handler.working && scores_handler.is_empty 153 | width: parent.width * 0.87 154 | height: parent.height * 0.8 155 | anchors.horizontalCenter: parent.horizontalCenter 156 | name: "نمرات" 157 | radius: 5 158 | } 159 | 160 | LoadingAnimationColor { 161 | y: parent.height - 100 162 | id: briefscores_loading_anim 163 | width: parent.width * 0.92 164 | height: 100 165 | radius: 5 166 | visible: scores_loading_anim.visible 167 | anchors.horizontalCenter: parent.horizontalCenter 168 | } 169 | ErrorRectangle { 170 | visible: scores_err_rec.visible 171 | y: parent.height - 100 172 | width: parent.width * 0.92 173 | height: 100 174 | name: "نیمسال" 175 | radius: 5 176 | anchors.horizontalCenter: parent.horizontalCenter 177 | } 178 | 179 | } 180 | 181 | Component { 182 | id: scores_table_cell 183 | MyTableView.BaseColumnItem { 184 | property var model_text: model[role] 185 | property var text_color: "#FFFFFF" 186 | Component.onCompleted: { 187 | // determine the color of the text by checking the status 188 | // also make model.status human readable 189 | if (model.status === ScoresHandler.Deleted) { 190 | model_text = role === "status" ? "حذف شده" : model[role] 191 | text_color = "#757575" 192 | } else if (model.status === ScoresHandler.Passed) { 193 | model_text = role === "status" ? "قبول" : model[role] 194 | text_color = role === "status" || role === "score" ? "#22FF32" : "#FFFFFF" 195 | } else if (model.status === ScoresHandler.Failed) { 196 | model_text = role === "status" ? "مردود" : model[role] 197 | text_color = role === "status" || role === "score" ? "#FF6363" : "#FFFFFF" 198 | } else if (model.status === ScoresHandler.Temporary) { 199 | model_text = role === "status" ? "موقت" : model[role] 200 | text_color = "#F7FF7D" 201 | } else if (model.status === ScoresHandler.Undefined) { 202 | model_text = role === "status" ? "نامشخص" : model[role] 203 | } 204 | } 205 | 206 | Label { 207 | anchors.centerIn: parent 208 | width: parent.width - 5 209 | horizontalAlignment: Label.AlignHCenter 210 | wrapMode: Label.WordWrap 211 | font.family: regular_font.name 212 | text: parent.model_text 213 | color: parent.text_color 214 | font.weight: Font.DemiBold 215 | } 216 | } 217 | } 218 | 219 | } 220 | -------------------------------------------------------------------------------- /qml/Pages/SettingsPage.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a Settings pge 3 | * In this page, We provide 3 type of information to users: 4 | * -Golestan settings: changing golestan account credentials 5 | * -Boostan Settings: changing this client(Boostan) configurations 6 | * -About: some information about the Boostan project 7 | */ 8 | 9 | import QtQuick 2.15 10 | import QtQuick.Controls 2.15 11 | import QtQuick.Layouts 1.15 12 | import "../Controls" 13 | import "../Helpers/SettingsPage" 14 | 15 | PageBase { 16 | id: settings_page 17 | 18 | Rectangle { 19 | id: page_background 20 | anchors.fill: parent 21 | color: "#262A2F" 22 | } 23 | 24 | Notifier { 25 | id: notifier 26 | anchors.horizontalCenter: parent.horizontalCenter 27 | font.family: regular_font.name 28 | } 29 | 30 | Rectangle { 31 | id: tabbar_bg 32 | anchors.horizontalCenter: parent.horizontalCenter 33 | anchors.bottom: container_bg.top 34 | anchors.bottomMargin: 20 35 | width: 400 36 | height: 35 37 | color: "#33363A" 38 | radius: 20 39 | RowLayout { 40 | width: parent.width 41 | height: parent.height 42 | spacing: 0 43 | Repeater { 44 | id: tabbar_repeater 45 | model: ["درباره" ,"تنظیمات بوستان" ,"تنظیمات گلستان"] 46 | delegate: tabbar_comp 47 | signal tabChanged(var index) 48 | onTabChanged: { 49 | stack_layout.currentIndex = index 50 | } 51 | Component.onCompleted: { 52 | tabbar_repeater.itemAt(stack_layout.currentIndex).selected = true 53 | } 54 | } 55 | 56 | } 57 | } 58 | 59 | 60 | Rectangle { 61 | id: container_bg 62 | anchors.centerIn: parent 63 | width: 350 64 | height: 400 65 | color: "#1D2025" 66 | radius: 15 67 | 68 | StackLayout { 69 | id: stack_layout 70 | anchors.fill: parent 71 | currentIndex: 2 72 | 73 | About { } 74 | 75 | BoostanSettings { } 76 | 77 | GolestanSettings { } 78 | 79 | } 80 | } 81 | 82 | 83 | 84 | Component { 85 | id: tabbar_comp 86 | Rectangle { 87 | id: tabbar_comp_root 88 | property bool selected: false 89 | Layout.fillHeight: true 90 | Layout.fillWidth: true 91 | color: "transparent" 92 | radius: tabbar_bg.radius 93 | Behavior on color { 94 | ColorAnimation { 95 | duration: 200 96 | } 97 | } 98 | 99 | Label { 100 | anchors.centerIn: parent 101 | text: modelData 102 | color: "#FCFCFC" 103 | font.family: regular_font.name 104 | font.weight: Font.Bold 105 | } 106 | 107 | MouseArea { 108 | id: m_area1 109 | anchors.fill: parent 110 | onClicked: { 111 | if (tabbar_comp_root.selected) 112 | return; 113 | tabbar_comp_root.selected = true 114 | tabbar_repeater.tabChanged(index) 115 | } 116 | } 117 | 118 | onSelectedChanged: { 119 | color = selected ? "#159E84" : "transparent" 120 | } 121 | 122 | Connections { 123 | target: tabbar_repeater 124 | function onTabChanged(ind) 125 | { 126 | if (ind !== index) { 127 | tabbar_comp_root.selected = false 128 | } 129 | } 130 | } 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /qml/fonts/Mj_Afsoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moork0/Boostan-Desktop/cc21c55f844d9a0c185317e86c4319fdc7043e73/qml/fonts/Mj_Afsoon.ttf -------------------------------------------------------------------------------- /qml/fonts/Tanha.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moork0/Boostan-Desktop/cc21c55f844d9a0c185317e86c4319fdc7043e73/qml/fonts/Tanha.ttf -------------------------------------------------------------------------------- /qml/fonts/Vazir-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moork0/Boostan-Desktop/cc21c55f844d9a0c185317e86c4319fdc7043e73/qml/fonts/Vazir-Regular.ttf -------------------------------------------------------------------------------- /qml/fonts/icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moork0/Boostan-Desktop/cc21c55f844d9a0c185317e86c4319fdc7043e73/qml/fonts/icons.ttf -------------------------------------------------------------------------------- /qml/main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | import QtQuick.Controls 2.15 3 | import API.Settings 1.0 4 | import "Helpers" 5 | import "Controls" 6 | 7 | ApplicationWindow { 8 | id: mainwindow 9 | visible: true 10 | width: 1150 11 | height: 700 12 | minimumWidth: 1000 13 | minimumHeight: 600 14 | title: "Boostan" 15 | 16 | FontLoader { source: "fonts/icons.ttf"; } 17 | FontLoader { source: "fonts/Tanha.ttf" } 18 | FontLoader { source: "fonts/Mj_Afsoon.ttf" } 19 | FontLoader { id: regular_font; source: "fonts/Vazir-Regular.ttf"; } 20 | 21 | // An object to store the universal necessary data's 22 | QtObject { 23 | id: universal_storage 24 | property var semesters: [] 25 | property int currentSemester: 3992 26 | property string studentName: "" 27 | property string studentUid: "" 28 | property string username: "" 29 | 30 | // update prefix uid 31 | onStudentUidChanged: Settings.setPrefixUid(studentUid) 32 | } 33 | 34 | ViewManager { 35 | width: parent.width - right_pane.width 36 | height: parent.height 37 | id: stackview 38 | Component.onCompleted: { 39 | // check if there is any error occured in application initializing 40 | if (UniversalError) { 41 | error_handler.raiseUniversalError(UniversalErrorCode) 42 | return; 43 | } 44 | stackview.rawPush("qrc:/Pages/LoginPage.qml", "qrc:/Pages/LoginPage.qml") 45 | 46 | } 47 | } 48 | 49 | SideBar { 50 | id: right_pane 51 | enabled: false 52 | 53 | SideBarItem { 54 | title: "پیشخوان" 55 | componentPath: "qrc:/Pages/DashboardPage.qml" 56 | iconText: "\uf19d" 57 | } 58 | 59 | SideBarItem { 60 | title: "دروس ارائه شده" 61 | componentPath: "qrc:/Pages/OfferedCoursePage.qml" 62 | iconText: "\uf0ce" 63 | } 64 | 65 | SideBarItem { 66 | title: "کارنامه" 67 | componentPath: "qrc:/Pages/ScoresPage.qml" 68 | iconText: "\ue80f" 69 | } 70 | 71 | SideBarItem { 72 | title: "تنظیمات" 73 | componentPath: "qrc:/Pages/SettingsPage.qml" 74 | iconText: "\ue80e" 75 | } 76 | 77 | } 78 | 79 | ErrorHandler { 80 | id: error_handler 81 | viewItem: stackview 82 | sideBar: right_pane 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /qml/pics/boostan-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /qml/pics/boostan.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moork0/Boostan-Desktop/cc21c55f844d9a0c185317e86c4319fdc7043e73/qml/pics/boostan.ico -------------------------------------------------------------------------------- /qml/pics/icon-boy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /qml/pics/icon-girl.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /qml/qml.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | main.qml 4 | qtquickcontrols2.conf 5 | Pages/LoginPage.qml 6 | Pages/ErrorPage.qml 7 | Pages/DashboardPage.qml 8 | Controls/MyTextInput.qml 9 | Controls/MyButton.qml 10 | Controls/ClickableText.qml 11 | Controls/Icon.qml 12 | Controls/Notifier.qml 13 | Controls/Plot.qml 14 | Controls/LoadingAnimationColor.qml 15 | Controls/LoadingAnimationPulse.qml 16 | Helpers/SideBar.qml 17 | Helpers/ErrorHandler.qml 18 | fonts/icons.ttf 19 | fonts/Tanha.ttf 20 | fonts/Mj_Afsoon.ttf 21 | pics/error-logo.svg 22 | Controls/ScreenShot.qml 23 | pics/boostan-logo.svg 24 | Pages/OfferedCoursePage.qml 25 | fonts/Vazir-Regular.ttf 26 | Controls/ScheduleTable.qml 27 | Controls/MyTableView.qml 28 | Helpers/SideBarItem.qml 29 | Helpers/SideBarDelegate.qml 30 | Controls/PageBase.qml 31 | Controls/ViewManager.qml 32 | pics/icon-boy.svg 33 | pics/icon-girl.svg 34 | Helpers/ErrorRectangle.qml 35 | Pages/ScoresPage.qml 36 | Controls/MyComboBox.qml 37 | Pages/SettingsPage.qml 38 | Helpers/SettingsPage/GolestanSettings.qml 39 | Controls/MySwitch.qml 40 | Helpers/SettingsPage/BoostanSettings.qml 41 | Helpers/SettingsPage/About.qml 42 | pics/boostan.ico 43 | 44 | 45 | -------------------------------------------------------------------------------- /qml/qtquickcontrols2.conf: -------------------------------------------------------------------------------- 1 | ; ‫‪https://doc.qt.io/qt-5/qtquickcontrols2-styles.html 2 | 3 | [Controls] 4 | Style=Material 5 | 6 | [Universal] 7 | Theme=Light 8 | ;Accent=Steel 9 | 10 | 11 | [Material] 12 | Theme=Light 13 | ;Variant=Dense 14 | Accent=#19B99A 15 | Primary=#262125 16 | -------------------------------------------------------------------------------- /source/base/network.cpp: -------------------------------------------------------------------------------- 1 | #include "header/base/network.h" 2 | 3 | Network::Network(QObject *parent) : QObject(parent) 4 | { 5 | connect(&netaccman, &QNetworkAccessManager::finished, this, &Network::finished); 6 | } 7 | 8 | Network::Network(QUrl url, QObject *parent) : Network {parent} 9 | { 10 | this->url = url; 11 | } 12 | 13 | QHash Network::getHeaders() const 14 | { 15 | return headers; 16 | } 17 | 18 | void Network::setHeaders(const QHash &value) 19 | { 20 | headers = value; 21 | } 22 | 23 | QUrl Network::getUrl() const 24 | { 25 | return url; 26 | } 27 | 28 | void Network::setUrl(const QUrl &value) 29 | { 30 | url = value; 31 | } 32 | 33 | void Network::addHeader(const QByteArray &header, const QByteArray &value) 34 | { 35 | this->headers[header] = value; 36 | } 37 | 38 | // send a POST request to 'url' with data 'data' and return request status 39 | bool Network::post(const QByteArray& data) 40 | { 41 | QNetworkRequest request(this->url); 42 | request.setAttribute(QNetworkRequest::CookieSaveControlAttribute, QNetworkRequest::Manual); 43 | setRequestHeader(request); 44 | QNetworkReply *reply = netaccman.post(request, data); 45 | if (Settings::getValue(QStringLiteral("logging"), true).toBool() == true) { 46 | Logger::log(QStringLiteral("Sending POST request: %1").arg(this->url.toString()).toUtf8(), true); 47 | Logger::log(data); 48 | } 49 | return reply->error() == QNetworkReply::NoError; 50 | } 51 | 52 | // send a GET request to 'url' return request status 53 | bool Network::get() 54 | { 55 | QNetworkRequest request(this->url); 56 | request.setAttribute(QNetworkRequest::CookieSaveControlAttribute, QNetworkRequest::Manual); 57 | setRequestHeader(request); 58 | QNetworkReply* reply = netaccman.get(request); 59 | if (Settings::getValue(QStringLiteral("logging"), true).toBool() == true) { 60 | Logger::log(QStringLiteral("Sending GET request: %1").arg(this->url.toString()).toUtf8()); 61 | } 62 | return reply->error() == QNetworkReply::NoError; 63 | } 64 | 65 | void Network::finished(QNetworkReply* reply) 66 | { 67 | emit complete(*reply); 68 | } 69 | 70 | // actually sets a request 'req' headers to 'headers' 71 | void Network::setRequestHeader(QNetworkRequest &req) 72 | { 73 | QHash::const_iterator it = headers.cbegin(); 74 | while (it != headers.cend()) { 75 | req.setRawHeader(it.key(), it.value()); 76 | ++it; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /source/base/settings.cpp: -------------------------------------------------------------------------------- 1 | #include "header/base/settings.h" 2 | #include 3 | 4 | Settings::Settings() 5 | { 6 | 7 | } 8 | 9 | void Settings::setValue(const QString key, const QString value, const bool raw_key) 10 | { 11 | // The '//' is used in .ini format files to categorize the information 12 | if (raw_key) { 13 | settings.setValue(key, value); 14 | return; 15 | } 16 | settings.setValue(prefix_url % QStringLiteral("//") % prefix_uid % QStringLiteral("//") % key, value); 17 | } 18 | 19 | QVariant Settings::getValue(const QString key, const bool raw_key) 20 | { 21 | if (raw_key) 22 | return settings.value(key); 23 | return settings.value(prefix_url % QStringLiteral("//") % prefix_uid % QStringLiteral("//") % key); 24 | } 25 | 26 | void Settings::setPrefixUid(const QString uid) 27 | { 28 | prefix_uid = uid; 29 | } 30 | 31 | void Settings::setPrefixUrl(const QString url) 32 | { 33 | prefix_url = url; 34 | // remove the protocol due to disambiguation in .ini format 35 | prefix_url.remove(QStringLiteral("https://")); 36 | } 37 | 38 | // check if settings is writable and has some required default value 39 | bool Settings::checkSettings() 40 | { 41 | if (!settings.isWritable()) return false; 42 | if (!settings.contains("root_url")) 43 | settings.setValue("root_url", Constants::root_url); 44 | 45 | // set the initial value of prefix_url 46 | setPrefixUrl(settings.value(QStringLiteral("root_url")).toString()); 47 | return true; 48 | } 49 | -------------------------------------------------------------------------------- /source/controls/scheduletable.cpp: -------------------------------------------------------------------------------- 1 | #include "header/controls/scheduletable.h" 2 | #include "header/helpers/commonmacros.h" 3 | 4 | /* 5 | Our data structure is (at least)something like this: 6 | { 7 | element_UniqueId: {"name": "Course1", "row": [1, 2], "column": [2, 2], "length": [2, 3]} 8 | } 9 | 10 | It could have many other members but the members above are necessary for functionality. 11 | So, imagine a schedule with week days(starting at Saturday) as rows(starting at 0) 12 | and day hours(starting at 08:00) as columns(starting at 0). 13 | 14 | Thus the structure above would translated to something like this: 15 | "Course1" will held in sunday and monday both at 10:00 16 | and the sunday class would take long 2 blocks of time(each block represent an hour in here). 17 | So, "Course1" would be held in Sunday, at 10:00 and would take long 2 hours. 18 | */ 19 | ScheduleTable::ScheduleTable(QObject *parent) : QObject(parent) 20 | { 21 | 22 | } 23 | 24 | int ScheduleTable::calculateScheduleRow(const QString& day) 25 | { 26 | // list of days 27 | static const QStringList days_keyword{ MyStringLiteral("شنبه"), MyStringLiteral("يک"), MyStringLiteral("دو"), MyStringLiteral("سه"), MyStringLiteral("چهار"), MyStringLiteral("پنج"), MyStringLiteral("جمعه") }; 28 | static const int keyword_size {days_keyword.size()}; 29 | 30 | for (int i {0}; i < keyword_size; ++i) { 31 | if (day.startsWith(days_keyword[i])) { 32 | return i; 33 | } 34 | } 35 | return -1; 36 | } 37 | 38 | float ScheduleTable::calculateScheduleColumn(const QString& hour) 39 | { 40 | // 8:00 is hour that classes could start(We have no class befor 08:00) 41 | static constexpr int first_hour {8}; 42 | // 20:00 is hour that classes could end(We have no class after 20:00) 43 | static constexpr int last_hour {20}; 44 | static constexpr int columns_length {last_hour - first_hour}; 45 | 46 | QString current_hour; 47 | // iterate over hours between first_hour and last_hour 48 | // and find the correct hour and corresponding column number (i) 49 | /// TODO: There must be more efficient way instead of iteration. Find that! 50 | for (int i {0}; i <= columns_length; ++i) { 51 | current_hour = QString::number(first_hour + i); 52 | if (current_hour.size() == 1) 53 | current_hour = QString(QStringLiteral("0")) + current_hour; 54 | 55 | // Divide minutes by 60 to have result as hour. for examle: 10:30 => column number 2.5 56 | if (hour.startsWith(current_hour)) 57 | return i + (hour.midRef(3, 2).toFloat() / 60); 58 | } 59 | return -1; 60 | } 61 | 62 | float ScheduleTable::calculateScheduleLen(const QString& hour, const float start_column) 63 | { 64 | // 5 is the length of the last 5 character of "12:34-56:78" which is "56:78" 65 | float end_column {calculateScheduleColumn(hour.right(5))}; 66 | return end_column - start_column; 67 | } 68 | 69 | void ScheduleTable::addEelement(const QString uid, QVariantMap element) 70 | { 71 | model_data.insert(uid, element); 72 | } 73 | 74 | void ScheduleTable::removeEelement(const QString &uid) 75 | { 76 | model_data.remove(uid); 77 | } 78 | 79 | QVariantList ScheduleTable::checkCollision(const QVariantMap element) const 80 | { 81 | // initialize the necessary variables from 'element' 82 | const QVariantList columns (element.value("column").toList()); 83 | const QVariantList rows (element.value("row").toList()); 84 | const QVariantList lengths (element.value("length").toList()); 85 | const QStringList exam {element.value("exam").toString().split(QStringLiteral("
"))}; 86 | // number of course sessions in a week 87 | const int sessions_number {columns.size()}; 88 | 89 | // Initialize the necessary variables for being used in loop 90 | QHash::const_iterator iterator = model_data.cbegin(); 91 | QHash::const_iterator end = model_data.cend(); 92 | QVariantList iterator_columns, iterator_rows, iterator_lengths, exam_warnings; 93 | QVariantMap iterator_value; 94 | QStringList iterator_exam; 95 | 96 | // iterate over model_data elements 97 | for (; iterator != end; ++iterator) { 98 | 99 | iterator_value = iterator.value(); 100 | iterator_exam = iterator_value.value("exam").toString().split(QStringLiteral("
")); 101 | // check for any collision for exam times 102 | for (int itexam_index {0}; itexam_index < iterator_exam.size(); ++itexam_index) { 103 | if (iterator_exam.at(itexam_index) == MyStringLiteral("نامشخص")) 104 | continue; 105 | 106 | for (int exam_index {0}; exam_index < exam.size(); ++exam_index) { 107 | if (exam.at(exam_index) == MyStringLiteral("نامشخص")) 108 | continue; 109 | 110 | if (iterator_exam.at(itexam_index) == exam.at(exam_index)) 111 | return QVariantList {ExamCollision, iterator_value.value("name").toString()}; 112 | 113 | /// TODO: should find better way because there is no unique format of exam. unless I normalize them 114 | // we assume exam format is smt like this: 12.01/08:00. so the first 5 chars are representing the date. 115 | // Exams have no collision but they are in a same day. Add to warnings 116 | if (iterator_exam.at(itexam_index).leftRef(5) == exam.at(exam_index).leftRef(5)) 117 | exam_warnings.append(iterator.key()); 118 | } 119 | } 120 | 121 | iterator_columns = iterator_value.value("column").toList(); 122 | iterator_rows = iterator_value.value("row").toList(); 123 | iterator_lengths = iterator_value.value("length").toList(); 124 | 125 | // Check for collision in course times 126 | int size {iterator_columns.size()}; 127 | for (int iter_index {0}; iter_index < size; ++iter_index) { 128 | float iter_column {iterator_columns[iter_index].toFloat()}; 129 | float iter_len {iterator_lengths[iter_index].toFloat()}; 130 | 131 | // iterate over 'element' rows and columns 132 | for (int orig_index {0}; orig_index < sessions_number; ++orig_index) { 133 | if (iterator_rows[iter_index] != rows[orig_index]) continue; 134 | float orig_column {columns[orig_index].toFloat()}; 135 | float orig_len {lengths[orig_index].toFloat()}; 136 | 137 | if (iter_column >= orig_column && iter_column < (orig_column + orig_len)) 138 | return QVariantList {TimeCollision, iterator_value.value("name").toString()}; 139 | 140 | if (orig_column >= iter_column && orig_column < (iter_column + iter_len)) 141 | return QVariantList {TimeCollision, iterator_value.value("name").toString()}; 142 | } // end of third 'for' 143 | 144 | } // end of second 'for' 145 | 146 | } // end of first 'for' 147 | 148 | if (!exam_warnings.isEmpty()) 149 | return QVariantList {ExamWarning, exam_warnings}; 150 | 151 | return QVariantList {NoCollision, QString()}; 152 | 153 | } 154 | 155 | void ScheduleTable::clearAll() 156 | { 157 | model_data.clear(); 158 | } 159 | 160 | QString ScheduleTable::serialize() const 161 | { 162 | QByteArray data; 163 | QDataStream stream(&data, QIODevice::WriteOnly); 164 | stream << model_data; 165 | return data.toBase64(); 166 | } 167 | 168 | QHash ScheduleTable::deserialize(const QString& data) 169 | { 170 | QHash container; 171 | QByteArray raw_data {data.toUtf8()}; 172 | // convert to binary 173 | raw_data = QByteArray::fromBase64(raw_data); 174 | QDataStream stream(raw_data); 175 | stream >> container; 176 | return container; 177 | } 178 | 179 | QString ScheduleTable::getUid(const int course_number, const int course_group) 180 | { 181 | return QString::number(course_number) + QString::number(course_group); 182 | } 183 | 184 | QString ScheduleTable::getUid(const QString &course_number, const QString &course_group) 185 | { 186 | return course_number + course_group; 187 | } 188 | 189 | QString ScheduleTable::getCourseNames(const QVariantList uids) const 190 | { 191 | QString names; 192 | for (const QVariant& uid : uids) { 193 | names.append( model_data.value(uid.toString()).value("name").toString() + QStringLiteral("
")); 194 | } 195 | return names; 196 | } 197 | 198 | void ScheduleTable::setCourseWarnings(const QString uid, const QVariantList warning_list) 199 | { 200 | if (!model_data.contains(uid)) return; 201 | model_data[uid][QStringLiteral("warningForCourses")] = warning_list; 202 | } 203 | 204 | -------------------------------------------------------------------------------- /source/handlers/abstractxmldatahandler.cpp: -------------------------------------------------------------------------------- 1 | #include "header/handlers/abstractxmldatahandler.h" 2 | 3 | AbstractXmlDataHandler::AbstractXmlDataHandler() 4 | : _is_empty {true} 5 | { 6 | 7 | } 8 | 9 | void AbstractXmlDataHandler::setIsEmpty(bool state) 10 | { 11 | if (state == _is_empty) return; 12 | _is_empty = state; 13 | emit isEmptyChanged(); 14 | } 15 | -------------------------------------------------------------------------------- /source/handlers/accounthandler.cpp: -------------------------------------------------------------------------------- 1 | #include "header/handlers/accounthandler.h" 2 | 3 | AccountHandler::AccountHandler() 4 | { 5 | 6 | } 7 | 8 | void AccountHandler::requestTokens() 9 | { 10 | connect(&_request, &Network::complete, this, &AccountHandler::parseTokens); 11 | // reset the status and continer and flag for every request 12 | setFinished(false); 13 | 14 | QString tck_token {getTckToken()}; 15 | _request.setUrl(_root_url + _account_url + tck_token); 16 | _request.addHeader("Cookie", getCookies().toUtf8()); 17 | _request.get(); 18 | } 19 | 20 | void AccountHandler::requestChangeCreds() 21 | { 22 | connect(&_request, &Network::complete, this, &AccountHandler::parseChangeCreds); 23 | 24 | _request.setUrl(_root_url + _account_url + _request_validators[QStringLiteral("tck")]); 25 | _request.addHeader("Content-Type", "application/x-www-form-urlencoded"); 26 | _request.addHeader("Cookie", getCookies().toUtf8()); 27 | 28 | QString ticket_tbox {getTckToken()}; 29 | QString param_txtmiddle {QString(QStringLiteral("")).arg(_request_validators[QStringLiteral("uid")], _username, _password, _new_username, _new_password)}; 30 | QString data{ 31 | QStringLiteral("__VIEWSTATE=") % QUrl::toPercentEncoding(_request_validators["__VIEWSTATE"]) 32 | % QStringLiteral("&__VIEWSTATEGENERATOR=") % _request_validators["__VIEWSTATEGENERATOR"] 33 | % QStringLiteral("&__EVENTVALIDATION=") % QUrl::toPercentEncoding(_request_validators["__EVENTVALIDATION"]) 34 | % QStringLiteral("&TicketTextBox=") % ticket_tbox 35 | % QStringLiteral("&TxtMiddle=") % QUrl::toPercentEncoding(param_txtmiddle) 36 | % QStringLiteral("&Fm_Action=09&Frm_Type=&Frm_No=&XMLStdHlp=&ex=") 37 | }; 38 | 39 | _request.post(data.toUtf8()); 40 | } 41 | 42 | void AccountHandler::parseTokens(QNetworkReply &reply) 43 | { 44 | disconnect(&_request, &Network::complete, this, &AccountHandler::parseTokens); 45 | QString data; 46 | if (!verifyResponse(reply, data)) { 47 | reply.deleteLater(); 48 | setSuccess(false); 49 | setFinished(true); 50 | return; 51 | } 52 | 53 | reply.deleteLater(); 54 | _request_validators.insert(extractFormValidators(data)); 55 | requestChangeCreds(); 56 | 57 | } 58 | 59 | void AccountHandler::parseChangeCreds(QNetworkReply &reply) 60 | { 61 | disconnect(&_request, &Network::complete, this, &AccountHandler::parseChangeCreds); 62 | bool parse_success {true}; 63 | 64 | QString data; 65 | if (!verifyResponse(reply, data)) 66 | parse_success = false; 67 | 68 | if (parse_success && !isChangeSuccess(data)) { 69 | setErrorCode(Errors::ExtractError); 70 | parse_success = false; 71 | } 72 | 73 | reply.deleteLater(); 74 | if (!parse_success) { 75 | setSuccess(false); 76 | setFinished(true); 77 | return; 78 | } 79 | setSuccess(true); 80 | setFinished(true); 81 | } 82 | 83 | bool AccountHandler::isChangeSuccess(const QString &data) 84 | { 85 | const QString key {MyStringLiteral("SuccArr = new Array('شن")}; 86 | 87 | if (!data.contains(key)) 88 | return false; 89 | 90 | return true; 91 | } 92 | 93 | void AccountHandler::changeCreds(const QString username, const QString password, const QString new_password, const QString new_username) 94 | { 95 | _username = username; 96 | _password = password; 97 | _new_password = new_password; 98 | _new_username = new_username.isEmpty() ? username : new_username; 99 | requestTokens(); 100 | } 101 | -------------------------------------------------------------------------------- /source/handlers/briefinfohandler.cpp: -------------------------------------------------------------------------------- 1 | #include "header/handlers/briefinfohandler.h" 2 | 3 | BriefInfoHandler::BriefInfoHandler() : _locale{QLocale::Persian, QLocale::Iran}, _current_year{0} 4 | { 5 | // we don't need to numbers being separated by thousands 6 | _locale.setNumberOptions(QLocale::OmitGroupSeparator); 7 | // In this occasion, Unavailability of data is not acceptable. 8 | // So this error is Critical in this situation. 9 | // 18 is the number of error code which indicate the Limited access to the content. 10 | _error_handler.setCriticalStatus(18, Errors::Critical); 11 | } 12 | 13 | int BriefInfoHandler::getCurrentYear() const 14 | { 15 | return _current_year; 16 | } 17 | 18 | /* 19 | * Create and return a locale-aware QVariantMap from student_info 20 | */ 21 | QVariantMap BriefInfoHandler::getStudentInfo() const 22 | { 23 | QVariantMap data {_student_info}; 24 | data[_info_title[Index_Id]] = _locale.toString(data[_info_title[Index_Id]].toULongLong()); 25 | data[_info_title[Index_Passed]] = _locale.toString(static_cast(data[_info_title[Index_Passed]].toFloat())); 26 | data[_info_title[Index_TotalAvg]] = _locale.toString(data[_info_title[Index_TotalAvg]].toFloat()); 27 | return data; 28 | } 29 | 30 | void BriefInfoHandler::start() 31 | { 32 | requestTokens(); 33 | } 34 | 35 | QStringList BriefInfoHandler::getSemesterAvgs() const 36 | { 37 | return _passed_semesters_avg; 38 | } 39 | 40 | /* 41 | * Create and return a list of human readable(by removing the '3' at front and using locale) passed semesters 42 | */ 43 | QStringList BriefInfoHandler::getSemesterYears() const 44 | { 45 | QStringList list; 46 | for (int year : _passed_semesters) { 47 | // years are like this: 3XXX. we just need the XXX part. 48 | list << _locale.toString(year).remove(0, 1); 49 | } 50 | return list; 51 | } 52 | 53 | QList BriefInfoHandler::getRawSemesters() const 54 | { 55 | return _passed_semesters; 56 | } 57 | 58 | bool BriefInfoHandler::requestTokens() 59 | { 60 | connect(&_request, &Network::complete, this, &BriefInfoHandler::parseTokens); 61 | QString tck_token {getTckToken()}; 62 | _request.setUrl(_root_url + _user_info_url + tck_token); 63 | _request.addHeader("Cookie", getCookies().toUtf8()); 64 | return _request.get(); 65 | } 66 | 67 | void BriefInfoHandler::parseTokens(QNetworkReply& reply) 68 | { 69 | disconnect(&_request, &Network::complete, this, &BriefInfoHandler::parseTokens); 70 | QString data; 71 | if (!verifyResponse(reply, data)) { 72 | reply.deleteLater(); 73 | setSuccess(false); 74 | setFinished(true); 75 | return; 76 | } 77 | 78 | reply.deleteLater(); 79 | _request_validators.insert(extractFormValidators(data)); 80 | 81 | requestStuId(); 82 | } 83 | 84 | bool BriefInfoHandler::requestStuId() 85 | { 86 | connect(&_request, &Network::complete, this, &BriefInfoHandler::parseStuId); 87 | _request.setUrl(_root_url + _user_info_url + _request_validators["tck"]); 88 | _request.addHeader("Content-Type", "application/x-www-form-urlencoded"); 89 | _request.addHeader("Cookie", getCookies().toUtf8()); 90 | 91 | QString ticket_tbox {getTckToken()}; 92 | QString data{QStringLiteral("__VIEWSTATE=") % QUrl::toPercentEncoding(_request_validators["__VIEWSTATE"]) 93 | % QStringLiteral("&__VIEWSTATEGENERATOR=") % _request_validators["__VIEWSTATEGENERATOR"] 94 | % QStringLiteral("&__EVENTVALIDATION=") % QUrl::toPercentEncoding(_request_validators["__EVENTVALIDATION"]) 95 | % QStringLiteral("&TicketTextBox=") % ticket_tbox 96 | % QStringLiteral("&Fm_Action=00&Frm_Type=&Frm_No=&XMLStdHlp=&TxtMiddle=%3Cr%2F%3E&ex=")}; 97 | 98 | return _request.post(data.toUtf8()); 99 | } 100 | 101 | void BriefInfoHandler::parseStuId(QNetworkReply& reply) 102 | { 103 | disconnect(&_request, &Network::complete, this, &BriefInfoHandler::parseStuId); 104 | bool parse_success {true}; 105 | 106 | QString data, student_id; 107 | if (!verifyResponse(reply, data)) 108 | parse_success = false; 109 | 110 | student_id = extractStuId(data); 111 | if (parse_success && student_id.isEmpty()) { 112 | setErrorCode(Errors::ExtractError); 113 | parse_success = false; 114 | } 115 | 116 | reply.deleteLater(); 117 | if (!parse_success) { 118 | setSuccess(false); 119 | setFinished(true); 120 | return; 121 | } 122 | 123 | _request_validators.insert(extractFormValidators(data)); 124 | _student_info["id"] = student_id; 125 | requestBriefInfo(); 126 | } 127 | 128 | bool BriefInfoHandler::requestBriefInfo() 129 | { 130 | connect(&_request, &Network::complete, this, &BriefInfoHandler::parseUserInfo); 131 | _request.setUrl(_root_url + _user_info_url + _request_validators["tck"]); 132 | _request.addHeader("Content-Type", "application/x-www-form-urlencoded"); 133 | _request.addHeader("Cookie", getCookies().toUtf8()); 134 | 135 | QString ticket_tbox {getTckToken()}; 136 | QString data{QStringLiteral("__VIEWSTATE=") % QUrl::toPercentEncoding(_request_validators["__VIEWSTATE"]) 137 | % QStringLiteral("&__VIEWSTATEGENERATOR=") % _request_validators["__VIEWSTATEGENERATOR"] 138 | % QStringLiteral("&__EVENTVALIDATION=") % QUrl::toPercentEncoding(_request_validators["__EVENTVALIDATION"]) 139 | % QStringLiteral("&TicketTextBox=") % ticket_tbox 140 | % QStringLiteral("&TxtMiddle=%3Cr+F41251%3D%22") % _student_info["id"].toString() 141 | % QStringLiteral("%22%2F%3E&Fm_Action=08&Frm_Type=&Frm_No=&XMLStdHlp=&ex=")}; 142 | 143 | return _request.post(data.toUtf8()); 144 | } 145 | 146 | void BriefInfoHandler::parseUserInfo(QNetworkReply& reply) 147 | { 148 | disconnect(&_request, &Network::complete, this, &BriefInfoHandler::parseUserInfo); 149 | bool parse_success {true}; 150 | 151 | QString data; 152 | if (!verifyResponse(reply, data)) 153 | parse_success = false; 154 | 155 | if (parse_success && (!extractStudentInfo(data) || !extractStudentAvgs(data))) { 156 | setErrorCode(Errors::ExtractError); 157 | parse_success = false; 158 | } 159 | 160 | reply.deleteLater(); 161 | if (!parse_success) { 162 | setSuccess(false); 163 | setFinished(true); 164 | return; 165 | } 166 | 167 | emit studentInfoChanged(); 168 | setSuccess(true); 169 | setFinished(true); 170 | return; 171 | } 172 | 173 | bool BriefInfoHandler::extractStudentInfo(const QString& response) 174 | { 175 | const QList keywords {QStringLiteral("F17551"), // Field 176 | QStringLiteral("F41351"), // StudyType 177 | QStringLiteral("F41701"), // TotalAvg 178 | QStringLiteral("F41801")}; // Passed 179 | int position; 180 | QString value; 181 | /* increased Index_START by 2 because we want to skip the Id since we don't have Id in this data. 182 | (we did that in extractStuId) */ 183 | for (int title_index{Index_START + 2}, keyindex{0}; title_index < Index_END; ++title_index, ++keyindex) { 184 | position = response.indexOf(keywords[keyindex]); 185 | if (position == -1) return false; 186 | 187 | // 10 is the lentgh of keyword which for example is "F51851 = '" 188 | // so we need to skip this 189 | position += 10; 190 | for (int i{position}; response[i] != "'"; ++i) { 191 | value.append(response[i]); 192 | } 193 | _student_info[_info_title[title_index]] = value; 194 | value.clear(); 195 | } 196 | return true; 197 | } 198 | 199 | bool BriefInfoHandler::extractStudentAvgs(const QString &response) 200 | { 201 | int year_position, avg_position; 202 | QString year_value, avg_value; 203 | const QString year_keyword {QStringLiteral("F4350")}, avg_keyword {QStringLiteral("F4360")}; 204 | year_position = response.indexOf(year_keyword); 205 | avg_position = response.indexOf(avg_keyword); 206 | if (year_position == -1 || avg_position == -1) { 207 | return false; 208 | } 209 | 210 | while (year_position != -1 && avg_position != -1) { 211 | // 7 is the lentgh of actual keywords which for example is 'F4350="' 212 | // so we need to skip these characters 213 | year_position += 7; 214 | avg_position += 7; 215 | for (int i{year_position}; response[i] != '"'; ++i) { 216 | year_value.append(response[i]); 217 | } 218 | 219 | for (int i{avg_position}; response[i] != '"'; ++i) { 220 | avg_value.append(response[i]); 221 | } 222 | _passed_semesters.append(year_value.toInt()); 223 | _passed_semesters_avg.append(avg_value); 224 | 225 | year_value.clear(); 226 | avg_value.clear(); 227 | year_position = response.indexOf(year_keyword, year_position); 228 | avg_position = response.indexOf(avg_keyword, avg_position); 229 | } 230 | _current_year = _passed_semesters.last(); 231 | 232 | if (_passed_semesters_avg.last().isEmpty()) { 233 | _passed_semesters_avg.pop_back(); 234 | _passed_semesters.pop_back(); 235 | } 236 | emit currentYearChanged(); 237 | return true; 238 | } 239 | 240 | QString BriefInfoHandler::extractStuId(const QString &response) 241 | { 242 | int position {response.indexOf(QStringLiteral("=""))}; 243 | QString stu_number; 244 | if (position == -1) return QString(); // return error 245 | // 7 is the lentgh of string we searched. we need to skip this string. 246 | int char_position {position + 7}; 247 | while (response[char_position] != '&') { 248 | stu_number.append(response[char_position]); 249 | ++char_position; 250 | } 251 | return stu_number; 252 | } 253 | -------------------------------------------------------------------------------- /source/handlers/captchahandler.cpp: -------------------------------------------------------------------------------- 1 | #include "header/handlers/captchahandler.h" 2 | 3 | CaptchaHandler::CaptchaHandler() 4 | { 5 | 6 | } 7 | 8 | bool CaptchaHandler::getCaptcha() 9 | { 10 | connect(&_request, &Network::complete, this, &CaptchaHandler::parseGetCaptcha); 11 | setFinished(false); 12 | _request.setUrl(_root_url + _captcha_url); 13 | _request.addHeader("Cookie", getCookies().toUtf8()); 14 | return _request.get(); 15 | } 16 | 17 | bool CaptchaHandler::parseGetCaptcha(QNetworkReply& reply) 18 | { 19 | disconnect(&_request, &Network::complete, this, &CaptchaHandler::parseGetCaptcha); 20 | if (hasError(reply.error())) { 21 | reply.deleteLater(); 22 | setSuccess(false); 23 | setFinished(true); 24 | return false; 25 | } 26 | 27 | QFile file(_image_path); 28 | if (!file.open(QIODevice::WriteOnly)) { 29 | setErrorCode(Errors::CaptchaStoreError); 30 | reply.deleteLater(); 31 | setSuccess(false); 32 | setFinished(true); 33 | return false; 34 | } 35 | 36 | file.write(reply.readAll()); 37 | file.close(); 38 | 39 | reply.deleteLater(); 40 | setSuccess(true); 41 | setFinished(true); 42 | return true; 43 | } 44 | -------------------------------------------------------------------------------- /source/handlers/courseschedulehandler.cpp: -------------------------------------------------------------------------------- 1 | #include "header/handlers/courseschedulehandler.h" 2 | 3 | CourseScheduleHandler::CourseScheduleHandler() 4 | { 5 | } 6 | 7 | void CourseScheduleHandler::start(const QString current_semester) 8 | { 9 | setSemester(current_semester); 10 | 11 | requestTokens(); 12 | } 13 | 14 | QVariantList CourseScheduleHandler::getSchedule() const 15 | { 16 | return _weekly_schedule; 17 | } 18 | 19 | bool CourseScheduleHandler::requestTokens() 20 | { 21 | connect(&_request, &Network::complete, this, &CourseScheduleHandler::parseTokens); 22 | QString tck_token {getTckToken()}; 23 | _request.setUrl(_root_url + _schedule_url + tck_token); 24 | _request.addHeader("Cookie", getCookies().toUtf8()); 25 | return _request.get(); 26 | } 27 | 28 | void CourseScheduleHandler::parseTokens(QNetworkReply& reply) 29 | { 30 | disconnect(&_request, &Network::complete, this, &CourseScheduleHandler::parseTokens); 31 | QString data; 32 | if (!verifyResponse(reply, data)) { 33 | reply.deleteLater(); 34 | setSuccess(false); 35 | setFinished(true); 36 | return; 37 | } 38 | 39 | reply.deleteLater(); 40 | 41 | _request_validators.insert(extractFormValidators(data)); 42 | requestSchedule(); 43 | } 44 | 45 | bool CourseScheduleHandler::requestSchedule() 46 | { 47 | connect(&_request, &Network::complete, this, &CourseScheduleHandler::parseSchedule); 48 | QString tck_token {getTckToken()}; 49 | _request.setUrl(_root_url + _schedule_url + tck_token); 50 | _request.addHeader("Cookie", getCookies().toUtf8()); 51 | _request.addHeader("Content-Type", "application/x-www-form-urlencoded"); 52 | 53 | QString data{QStringLiteral("__VIEWSTATE=") % QUrl::toPercentEncoding(_request_validators["__VIEWSTATE"]) 54 | % QStringLiteral("&__VIEWSTATEGENERATOR=") % _request_validators["__VIEWSTATEGENERATOR"] 55 | % QStringLiteral("&__EVENTVALIDATION=") % QUrl::toPercentEncoding(_request_validators["__EVENTVALIDATION"]) 56 | % QStringLiteral("&TicketTextBox=") % tck_token 57 | 58 | // below is like this: in url encoded format 59 | % QStringLiteral("&XmlPriPrm=") % QString(QStringLiteral("%3CRoot%3E%3CN+UQID%3D%2210%22+id%3D%221%22+F%3D%22%1%22+T%3D%22%1%22%2F%3E%3C%2FRoot%3E")).arg(_semester) 60 | % QStringLiteral("&Fm_Action=09&Frm_Type=&Frm_No=&F_ID=&XmlPubPrm=&XmlMoredi=&F9999=&HelpCode=&Ref1=&Ref2=&Ref3=&Ref4=&Ref5=&NameH=&FacNoH=&GrpNoH=&RepSrc=&ShowError=&TxtMiddle=%3Cr%2F%3E&tbExcel=&txtuqid=&ex=")}; 61 | return _request.post(data.toUtf8()); 62 | } 63 | 64 | bool CourseScheduleHandler::getIsEmpty () const 65 | { 66 | return _is_empty; 67 | } 68 | 69 | void CourseScheduleHandler::parseSchedule(QNetworkReply& reply) 70 | { 71 | disconnect(&_request, &Network::complete, this, &CourseScheduleHandler::parseSchedule); 72 | QString data; 73 | bool parse_success {true}; 74 | 75 | if (!verifyResponse(reply, data)) 76 | parse_success = false; 77 | 78 | _request_validators.insert(extractFormValidators(data)); 79 | if (parse_success && !extractWeeklySchedule(data)) { 80 | setErrorCode(Errors::ExtractError); 81 | parse_success = false; 82 | } 83 | 84 | reply.deleteLater(); 85 | if (!parse_success) { 86 | setSuccess(false); 87 | setFinished(true); 88 | return; 89 | } 90 | setSuccess(true); 91 | setFinished(true); 92 | } 93 | 94 | void CourseScheduleHandler::setSemester(const QString &sem) 95 | { 96 | _semester = sem; 97 | } 98 | 99 | bool CourseScheduleHandler::extractWeeklySchedule(QString& response) 100 | { 101 | QRegularExpression re {_xmldata_pattern, QRegularExpression::UseUnicodePropertiesOption}; 102 | QRegularExpressionMatch match {re.match(response)}; 103 | QVariantMap map; 104 | if (!match.hasMatch()) return false; 105 | 106 | QXmlStreamReader reader(match.captured()); 107 | 108 | if (!reader.readNextStartElement()) return false; 109 | if (reader.name() != QStringLiteral("Root")) return false; 110 | 111 | QString temp_string, exam_string; 112 | QStringList temp_stringlist; 113 | QVariantList rows, columns, lengths; 114 | while(reader.readNextStartElement()) { 115 | if(reader.name() != QStringLiteral("row")) continue; 116 | 117 | QXmlStreamAttributes attribute {reader.attributes()}; 118 | map[QStringLiteral("teacher")] = attribute.value(QStringLiteral("C7")).toString(); 119 | map[QStringLiteral("name")] = attribute.value(QStringLiteral("C2")).toString(); 120 | 121 | temp_stringlist = attribute.value(QStringLiteral("C1")).toString().split(QStringLiteral("_")); 122 | // Generate unique id using ScheduleTable::getUid 123 | map[QStringLiteral("uid")] = ScheduleTable::getUid(temp_stringlist.at(0), temp_stringlist.at(1)); 124 | 125 | temp_stringlist = attribute.value("C8").toString().replace(MyStringLiteral("ك"), MyStringLiteral("ک")).simplified().split("،"); 126 | int counter {0}, exam_index {-1}; 127 | // clear data's for storing new informations 128 | rows.clear(); 129 | columns.clear(); 130 | lengths.clear(); 131 | 132 | for (QString& daytime_str : temp_stringlist) { 133 | daytime_str = daytime_str.simplified(); 134 | 135 | // find the index of exam time in temp_stringlist 136 | if (daytime_str.startsWith(MyStringLiteral("امتحان"))) { 137 | exam_index = counter; 138 | break; 139 | } 140 | 141 | int time_index {daytime_str.indexOf('-') - 5}; 142 | int day_index {daytime_str.indexOf(':') + 2}; 143 | // storing hour string: 12:34-56:78 144 | temp_string = daytime_str.mid(time_index, 11); 145 | float calculated_column {ScheduleTable::calculateScheduleColumn(temp_string)}; 146 | 147 | columns.append(calculated_column); 148 | rows.append(ScheduleTable::calculateScheduleRow(daytime_str.mid(day_index, time_index - 1 - day_index))); 149 | lengths.append(ScheduleTable::calculateScheduleLen(temp_string, calculated_column)); 150 | 151 | ++counter; 152 | } 153 | 154 | exam_string.clear(); 155 | // if we have exam time specified, Iterate over them. 156 | if (exam_index != -1) { 157 | for (; exam_index < temp_stringlist.size(); ++exam_index) { 158 | temp_string = temp_stringlist.at(exam_index); 159 | exam_string += temp_string.midRef(7, 10) % QStringLiteral(" ") % temp_string.rightRef(11) % QStringLiteral(" || "); 160 | } 161 | 162 | } else { 163 | exam_string = MyStringLiteral("نامشخص || "); 164 | } 165 | // remove " || " from end of the string 166 | exam_string.chop(4); 167 | 168 | map[QStringLiteral("row")] = rows; 169 | map[QStringLiteral("column")] = columns; 170 | map[QStringLiteral("length")] = lengths; 171 | map[QStringLiteral("exam")] = exam_string; 172 | 173 | _weekly_schedule.append(map); 174 | reader.skipCurrentElement(); 175 | } 176 | 177 | // check if the response data was empty or not 178 | if (!map.isEmpty()) 179 | setIsEmpty(false); 180 | 181 | return true; 182 | } 183 | -------------------------------------------------------------------------------- /source/handlers/handler.cpp: -------------------------------------------------------------------------------- 1 | #include "header/handlers/handler.h" 2 | 3 | 4 | Handler::Handler(QObject *parent) : QObject(parent), _is_finished{false}, _success{false} 5 | { 6 | _root_url = Settings::getValue(QStringLiteral("root_url"), true).toString(); 7 | } 8 | 9 | 10 | void Handler::setCookie(const QString& key, const QString& value) 11 | { 12 | _cookies[key] = value; 13 | } 14 | 15 | /* 16 | * split 'keyvalue' string into key,value and pass them to setCookie() 17 | */ 18 | void Handler::setCookie(const QString& keyvalue) 19 | { 20 | QList splited = keyvalue.split('='); 21 | QString key {splited.takeFirst()}; 22 | QString value {splited.join('=')}; 23 | setCookie(key, value); 24 | } 25 | 26 | QString Handler::getCookies() const 27 | { 28 | QString data; 29 | QHashString::const_iterator it = _cookies.cbegin(); 30 | for (; it != _cookies.cend(); ++it) { 31 | data += it.key() + "=" + it.value() + "; "; 32 | } 33 | data.chop(2); 34 | return data; 35 | } 36 | 37 | bool Handler::hasError(QNetworkReply::NetworkError ecode) 38 | { 39 | // since the ecode is one of the Qt default codes, we should add it to qt_offset 40 | // to prevent conflict with Golestan error codes. 41 | setErrorCode(ecode + Errors::qt_offset); 42 | if (ecode == QNetworkReply::NoError) return false; 43 | return true; 44 | } 45 | 46 | bool Handler::getFinished() const 47 | { 48 | return _is_finished; 49 | } 50 | 51 | void Handler::setFinished(bool value) 52 | { 53 | _is_finished = value; 54 | // we only wanna use finished() when an request is finished. 55 | if (_is_finished == true) emit finished(); 56 | emit workingChanged(); 57 | } 58 | 59 | bool Handler::getWorking() const 60 | { 61 | return !_is_finished; 62 | } 63 | 64 | uint Handler::getErrorCode() const 65 | { 66 | return _error_handler.getErrorCode(); 67 | } 68 | 69 | void Handler::setErrorCode(int ecode) 70 | { 71 | if (_error_handler.setErrorCode(ecode)) 72 | emit errorCodeChanged(); 73 | } 74 | 75 | int Handler::getErrorType() const 76 | { 77 | return _error_handler.getErrorType(); 78 | } 79 | 80 | QString Handler::getErrorString() const 81 | { 82 | return _error_handler.getErrorString(); 83 | } 84 | 85 | QString Handler::getErrorSolution() const 86 | { 87 | return _error_handler.getErrorSolution(); 88 | } 89 | 90 | void Handler::setSuccess(bool state) 91 | { 92 | if (_success == state) return; 93 | _success = state; 94 | emit successChanged(); 95 | } 96 | 97 | bool Handler::getSuccess() const 98 | { 99 | return _success; 100 | } 101 | 102 | void Handler::clearCookies() 103 | { 104 | _cookies.clear(); 105 | } 106 | 107 | bool Handler::updateTokens(const QString& data) 108 | { 109 | QHashString tokens {extractTokens(data)}; 110 | if (tokens.isEmpty()) return false; 111 | QHashString::iterator it {tokens.begin()}; 112 | // we should remove 'ctck' at every update 113 | // because we should use ctck only when Golestan says. 114 | _cookies.remove("ctck"); 115 | for (; it != tokens.end(); ++it) { 116 | if (it.key() == "tck") continue; 117 | _cookies[it.key()] = it.value(); 118 | } 119 | _request_validators["tck"] = tokens["tck"]; 120 | _request_validators[QStringLiteral("uid")] = tokens[QStringLiteral("u")]; 121 | return true; 122 | } 123 | 124 | void Handler::clearTokens() 125 | { 126 | clearCookies(); 127 | _request_validators.clear(); 128 | } 129 | 130 | bool Handler::verifyResponse(QNetworkReply& reply, QString& data) 131 | { 132 | if (hasError(reply.error())) { 133 | return false; 134 | } 135 | if (data.isEmpty()) data = reply.readAll(); 136 | // qDebug() << data; 137 | if (Settings::getValue(QStringLiteral("logging"), true).toBool() == true) { 138 | Logger::log(QStringLiteral("RECIEVIED: %1").arg(data).toUtf8()); 139 | } 140 | 141 | // try to update the tokens 142 | bool token_up_res {updateTokens(data)}; 143 | 144 | // find out whether we have error or not 145 | setErrorCode(extractDataError(data)); 146 | if (getErrorCode() != Errors::NoError) { 147 | return false; 148 | } 149 | 150 | if (!token_up_res) { 151 | // we don't know what will gonna prevent updateTokens() to not updating tokens. 152 | // so the error is unknown and no more progress can be done. 153 | setErrorCode(Errors::UnknownError); 154 | return false; 155 | } 156 | 157 | return true; 158 | } 159 | 160 | QHashString Handler::extractFormValidators(const QString& response) 161 | { 162 | QHashString result; 163 | int position {response.indexOf(_viewstate_keyword)}; 164 | int endpos; 165 | 166 | if (position == -1) return QHashString {}; 167 | position += 20; 168 | endpos = response.indexOf('"', position); 169 | result[QStringLiteral("__VIEWSTATE")] = response.mid(position, endpos - position); 170 | 171 | position = response.indexOf(_viewstate_gen_keyword); 172 | if (position == -1) return QHashString {}; 173 | position += 29; 174 | endpos = response.indexOf('"', position); 175 | result[QStringLiteral("__VIEWSTATEGENERATOR")] = response.mid(position, endpos - position); 176 | 177 | position = response.indexOf(_event_val_keyword); 178 | if (position == -1) return QHashString {}; 179 | position += 26; 180 | endpos = response.indexOf('"', position); 181 | result[QStringLiteral("__EVENTVALIDATION")] = response.mid(position, endpos - position); 182 | 183 | // qDebug() << result; 184 | return result; 185 | } 186 | 187 | QString Handler::getTckToken() const 188 | { 189 | return _cookies.contains("ctck") ? _cookies.value("ctck") : _request_validators.value("tck"); 190 | } 191 | 192 | QHashString Handler::extractTokens(const QString& response) 193 | { 194 | // tokens that Golestan will return at every request and we need these to be able to make 195 | // another requests. 196 | QHashString tokens {{QStringLiteral("u"), QString()}, {QStringLiteral("su"), QString()}, {QStringLiteral("ft"), QString()}, 197 | {QStringLiteral("f"), QString()}, {QStringLiteral("lt"), QString()}, {QStringLiteral("ctck"), QString()}, 198 | {QStringLiteral("seq"), QString()}, {QStringLiteral("tck"), QString()}}; 199 | QString capture; 200 | QRegularExpression re {_tokens_pattern}; 201 | QRegularExpressionMatch match {re.match(response)}; 202 | 203 | if (!match.hasMatch()) return QHashString {}; 204 | capture = match.captured().remove(QStringLiteral("SavAut(")).remove('\''); 205 | QStringList splited = capture.split(","); 206 | // tokens.size() - 1(we dont wanna tck now) = 7 207 | if (splited.size() < 7) return QHashString {}; 208 | tokens["u"] = splited[0]; 209 | tokens["su"] = splited[1]; 210 | tokens["ft"] = splited[2]; 211 | tokens["f"] = splited[3]; 212 | tokens["lt"] = splited[4]; 213 | tokens["ctck"] = splited[5]; 214 | tokens["seq"] = splited[6]; 215 | 216 | /* 217 | * Normally, 'tck' and 'ctck' are equal to each other and in this case, Golestan only needs 'tck'. 218 | * But sometimes Golestan explicitly returns tck in other way. in that case we use both 'tck' and 'ctck' 219 | */ 220 | // check if 'tck' is explicitly defined 221 | if (!response.contains(QStringLiteral("SetOpenerTck(")) || response.contains(QStringLiteral("SetOpenerTck('')"))) { 222 | // no 'tck' defined explicitly. use 'ctck' instead and remove 'ctck' from tokens. 223 | tokens["tck"] = splited[5]; // splited[5] == ctck 224 | tokens.remove("ctck"); 225 | } else { 226 | // 'tck' is defined explicitly. we extract that. 227 | int position {response.indexOf(_tck_keyword)}; 228 | if (position == -1) return QHashString {}; 229 | // 14 is the size of tck_keyword 230 | position += 14; 231 | // 16 is the size of tck value. 232 | tokens["tck"] = response.mid(position, 16); 233 | } 234 | return tokens; 235 | } 236 | 237 | int Handler::extractDataErrorCode(const QString& response) 238 | { 239 | // all error codes will come after the word 'code'(in persian) 240 | int code_position {response.indexOf("کد")}; 241 | QString code; 242 | if (code_position == -1) return Errors::NoCodeFound; 243 | 244 | // 2 is the length of 'code' in persian. we should skip this to capture actual value. 245 | int i = code_position + 2; 246 | while (response[i] != " ") { 247 | code.append(response[i]); 248 | ++i; 249 | } 250 | 251 | return code.toInt(); 252 | } 253 | /* 254 | * This function at first try to extract code from the response. 255 | * if no code found, then try to find a key word that matches the custom error key words. 256 | */ 257 | int Handler::extractDataError(const QString& response) 258 | { 259 | if (!response.contains(QStringLiteral("ErrorArr = new Array('"))) return Errors::NoError; 260 | int code {extractDataErrorCode(response)}; 261 | if (code != Errors::NoCodeFound) return code; 262 | QHash::const_iterator it {Errors::error_keywords.cbegin()}; 263 | for (; it != Errors::error_keywords.cend(); ++it) { 264 | if (response.contains(it.value())) { 265 | // key is a custom error code. 266 | return it.key(); 267 | } 268 | } 269 | // code has error but no corresponding custom error found. 270 | return Errors::UnknownError; 271 | } 272 | -------------------------------------------------------------------------------- /source/handlers/inithandler.cpp: -------------------------------------------------------------------------------- 1 | #include "header/handlers/inithandler.h" 2 | 3 | InitHandler::InitHandler() 4 | { 5 | 6 | } 7 | 8 | bool InitHandler::start() 9 | { 10 | connect(&_request, &Network::complete, this, &InitHandler::parseInit); 11 | // reset tokens 12 | clearTokens(); 13 | 14 | _request.setUrl(_root_url + _loginurl); 15 | return _request.get(); 16 | } 17 | 18 | /* 19 | * parse init response. 20 | * 1- parse cookies and set ASP_SESSIONID to a valid value. 21 | * 2- extract validators. 22 | */ 23 | bool InitHandler::parseInit(QNetworkReply& reply) 24 | { 25 | disconnect(&_request, &Network::complete, this, &InitHandler::parseInit); 26 | 27 | if (hasError(reply.error())) { 28 | reply.deleteLater(); 29 | setSuccess(false); 30 | setFinished(true); 31 | return false; 32 | } 33 | 34 | QString data = reply.readAll(); 35 | bool cookiefound{false}; 36 | // extract cookies from response headers and add them to our 'cookies'. 37 | for (const auto& [key, value] : reply.rawHeaderPairs()) { 38 | if (key == "Set-Cookie") { 39 | // qDebug() << value; 40 | QString sid {value}; 41 | sid = sid.split(';')[0]; 42 | setCookie(sid); 43 | cookiefound = true; 44 | break; 45 | } 46 | } 47 | 48 | if (!cookiefound) { 49 | setErrorCode(Errors::UnknownError); 50 | reply.deleteLater(); 51 | setSuccess(false); 52 | setFinished(true); 53 | return false; 54 | } 55 | 56 | _request_validators = extractFormValidators(data); 57 | if (_request_validators.empty()) { 58 | setErrorCode(Errors::UnknownError); 59 | reply.deleteLater(); 60 | setSuccess(false); 61 | setFinished(true); 62 | return false; 63 | } 64 | 65 | reply.deleteLater(); 66 | setSuccess(true); 67 | setFinished(true); 68 | return true; 69 | } 70 | -------------------------------------------------------------------------------- /source/handlers/loginhandler.cpp: -------------------------------------------------------------------------------- 1 | #include "header/handlers/loginhandler.h" 2 | 3 | LoginHandler::LoginHandler() 4 | { 5 | // In this occasion, Unavailability of data is not acceptable. 6 | // So this error is Critical in this situation. 7 | // 18 is the number of error code which indicate the Limited access to the content. 8 | _error_handler.setCriticalStatus(18, Errors::Critical); 9 | } 10 | 11 | /* 12 | * prepare a request and send it to log in to Golestan system 13 | */ 14 | bool LoginHandler::tryLogin(const QString username, const QString password, const QString captcha) 15 | { 16 | connect(&_request, &Network::complete, this, &LoginHandler::parseLogin); 17 | _request.setUrl(_root_url + _login_url); 18 | _request.addHeader("Cookie", getCookies().toUtf8()); 19 | _request.addHeader("Content-Type", "application/x-www-form-urlencoded"); 20 | 21 | // credentials would bind here 22 | QString logincreds = QString(QStringLiteral("")).arg(username, password, captcha); 23 | 24 | // data values should be in url-encoded format 25 | QString data{QStringLiteral("__VIEWSTATE=") % QUrl::toPercentEncoding(_request_validators["__VIEWSTATE"]) 26 | % QStringLiteral("&__VIEWSTATEGENERATOR=") % _request_validators["__VIEWSTATEGENERATOR"] 27 | % QStringLiteral("&__EVENTVALIDATION=") % QUrl::toPercentEncoding(_request_validators["__EVENTVALIDATION"]) 28 | % QStringLiteral("&TxtMiddle=") % QUrl::toPercentEncoding(logincreds) % QStringLiteral("&Fm_Action=09&Frm_Type=&Frm_No=&TicketTextBox=")}; 29 | 30 | return _request.post(data.toUtf8()); 31 | } 32 | 33 | bool LoginHandler::parseLogin(QNetworkReply& reply) 34 | { 35 | disconnect(&_request, &Network::complete, this, &LoginHandler::parseLogin); 36 | QString data; 37 | bool parse_success {true}; 38 | if (!verifyResponse(reply, data)) 39 | parse_success = false; 40 | 41 | 42 | // extract student name 43 | if (parse_success && !extractName(data)) { 44 | setErrorCode(Errors::ExtractError); 45 | parse_success = false; 46 | } 47 | 48 | reply.deleteLater(); 49 | 50 | if (!parse_success) { 51 | setSuccess(false); 52 | setFinished(true); 53 | return false; 54 | } 55 | setSuccess(true); 56 | setFinished(true); 57 | return true; 58 | } 59 | 60 | 61 | bool LoginHandler::extractName(QString& response) 62 | { 63 | const QString keyword {QStringLiteral("SetUsr('")}; 64 | int position {response.indexOf(keyword)}; 65 | if (position == -1) return false; 66 | // 8 is the length of "SetUsr('". we should skip this to capture the main value 67 | position += 8; 68 | QChar character; 69 | _user_name.clear(); 70 | // name in the response is like this: 'first name', 'lastname') 71 | // we skip the single-qoutes and at last remove the comma. 72 | while (response[position] != ")") { 73 | character = response[position]; 74 | if (character != '\'') { 75 | _user_name.append(character); 76 | } 77 | ++position; 78 | } 79 | _user_name.replace(',', ' '); 80 | return true; 81 | } 82 | 83 | QString LoginHandler::getName() const 84 | { 85 | return _user_name; 86 | } 87 | -------------------------------------------------------------------------------- /source/handlers/scoreshandler.cpp: -------------------------------------------------------------------------------- 1 | #include "header/handlers/scoreshandler.h" 2 | 3 | bool ScoresHandler::getIsEmpty() const 4 | { 5 | return _is_empty; 6 | } 7 | 8 | // Initialize the requests 9 | void ScoresHandler::start(const QString semester, const QString student_id) 10 | { 11 | _semester = semester; 12 | _student_id = student_id; 13 | requestTokens(); 14 | } 15 | 16 | // QML api for requesting scores of semester 'semester' 17 | void ScoresHandler::getScoresOf(const QString semester) 18 | { 19 | _semester = semester; 20 | requestScores(); 21 | 22 | } 23 | 24 | void ScoresHandler::requestTokens() 25 | { 26 | connect(&_request, &Network::complete, this, &ScoresHandler::parseTokens); 27 | QString tck_token {getTckToken()}; 28 | _request.setUrl(_root_url + _scores_url + tck_token); 29 | _request.addHeader("Cookie", getCookies().toUtf8()); 30 | _request.get(); 31 | } 32 | 33 | void ScoresHandler::parseTokens(QNetworkReply &reply) 34 | { 35 | disconnect(&_request, &Network::complete, this, &ScoresHandler::parseTokens); 36 | QString data; 37 | if (!verifyResponse(reply, data)) { 38 | reply.deleteLater(); 39 | setSuccess(false); 40 | setFinished(true); 41 | return; 42 | } 43 | 44 | reply.deleteLater(); 45 | _request_validators.insert(extractFormValidators(data)); 46 | requestScores(); 47 | } 48 | 49 | void ScoresHandler::requestScores() 50 | { 51 | connect(&_request, &Network::complete, this, &ScoresHandler::parseScores); 52 | // reset the status and continer and flag for every request 53 | setFinished(false); 54 | setIsEmpty(true); 55 | _need_custom_avg = false; 56 | _scores.clear(); 57 | 58 | _request.setUrl(_root_url + _scores_url + _request_validators["tck"]); 59 | _request.addHeader("Content-Type", "application/x-www-form-urlencoded"); 60 | _request.addHeader("Cookie", getCookies().toUtf8()); 61 | 62 | QString ticket_tbox {getTckToken()}; 63 | QString param_txtmiddle {QString(QStringLiteral("")).arg(_student_id, _semester)}; 64 | QString data{ 65 | QStringLiteral("__VIEWSTATE=") % QUrl::toPercentEncoding(_request_validators["__VIEWSTATE"]) 66 | % QStringLiteral("&__VIEWSTATEGENERATOR=") % _request_validators["__VIEWSTATEGENERATOR"] 67 | % QStringLiteral("&__EVENTVALIDATION=") % QUrl::toPercentEncoding(_request_validators["__EVENTVALIDATION"]) 68 | % QStringLiteral("&TicketTextBox=") % ticket_tbox 69 | % QStringLiteral("&TxtMiddle=") % QUrl::toPercentEncoding(param_txtmiddle) 70 | % QStringLiteral("&Fm_Action=80&Frm_Type=&Frm_No=&XMLStdHlp=&ex=") 71 | }; 72 | 73 | _request.post(data.toUtf8()); 74 | } 75 | 76 | void ScoresHandler::parseScores(QNetworkReply &reply) 77 | { 78 | disconnect(&_request, &Network::complete, this, &ScoresHandler::parseScores); 79 | bool parse_success {true}; 80 | 81 | QString data; 82 | if (!verifyResponse(reply, data)) 83 | parse_success = false; 84 | 85 | if (parse_success && (!extractScores(data) || !extractBirefScores(data))) { 86 | setErrorCode(Errors::ExtractError); 87 | parse_success = false; 88 | } 89 | 90 | reply.deleteLater(); 91 | if (!parse_success) { 92 | setSuccess(false); 93 | setFinished(true); 94 | return; 95 | } 96 | setSuccess(true); 97 | setFinished(true); 98 | } 99 | 100 | void ScoresHandler::normalizeName(QString &name) 101 | { 102 | name.replace(MyStringLiteral("ك"), MyStringLiteral("ک")); 103 | name.replace(MyStringLiteral("ي"), MyStringLiteral("ی")); 104 | name = name.simplified(); 105 | } 106 | 107 | bool ScoresHandler::extractScores(const QString& data) 108 | { 109 | const QString scores_pattern {QStringLiteral("T02XML='([\\W\\w]+<\\/Root>)")}; 110 | QRegularExpression re {scores_pattern, QRegularExpression::UseUnicodePropertiesOption}; 111 | QRegularExpressionMatch match {re.match(data)}; 112 | if (!match.hasMatch()) return false; 113 | 114 | QXmlStreamReader reader(match.captured(1)); 115 | 116 | if (!reader.readNextStartElement()) 117 | return false; 118 | 119 | if (reader.name() != QStringLiteral("Root")) 120 | return false; 121 | 122 | int weight_sum {0}, int_weight; 123 | float scores_sum {0}; 124 | QString name, score, score_result, score_status, weight; 125 | while(reader.readNextStartElement()) { 126 | if(reader.name() != QStringLiteral("N")) 127 | continue; 128 | 129 | QXmlStreamAttributes attribute {reader.attributes()}; 130 | 131 | // course name 132 | name = attribute.value(QStringLiteral("F0200")).toString(); 133 | normalizeName(name); 134 | 135 | // score 136 | score = attribute.value(QStringLiteral("F3945")).toString(); 137 | // score result 138 | score_result = attribute.value(QStringLiteral("F3965")).toString(); 139 | // score status 140 | score_status = attribute.value(QStringLiteral("F3955")).toString(); 141 | // course weight 142 | weight = attribute.value(QStringLiteral("F0205")).toString(); 143 | int_weight = weight.toInt(); 144 | 145 | int status = Temporary; 146 | weight_sum += int_weight; 147 | 148 | if (score_status.startsWith("حذف")) { 149 | status = Deleted; 150 | score = "-"; 151 | // we don't wanna include deleted courses in our calculation 152 | weight_sum -= int_weight; 153 | } else if (score.isEmpty()) { 154 | status = Undefined; 155 | score = "-"; 156 | // we don't wanna include undefined courses in our calculation 157 | weight_sum -= int_weight; 158 | } else if (score_result.startsWith(MyStringLiteral("قبول"))) { 159 | status = Passed; 160 | } else if (score_result.startsWith(MyStringLiteral("رد"))) { 161 | status = Failed; 162 | } 163 | 164 | // summation of scores when each score multiplied to their corresponding course weight 165 | scores_sum += (score.toFloat() * int_weight); 166 | if (status == Temporary) 167 | _need_custom_avg = true; 168 | 169 | _scores.append( 170 | QVariantMap { 171 | {QStringLiteral("name"), name}, 172 | {QStringLiteral("weight"), weight}, 173 | {QStringLiteral("score"), score}, 174 | {QStringLiteral("status"), status} 175 | } 176 | ); 177 | 178 | reader.skipCurrentElement(); 179 | } 180 | 181 | if (!_scores.isEmpty()) { 182 | setIsEmpty(false); 183 | 184 | // calculate average 185 | if (_need_custom_avg) 186 | _custom_average = scores_sum / weight_sum; 187 | } 188 | 189 | return true; 190 | } 191 | 192 | bool ScoresHandler::extractBirefScores(const QString& data) 193 | { 194 | const QString scores_pattern {QStringLiteral("T01XML='([\\W\\w]+<\\/Root>)")}; 195 | QRegularExpression re {scores_pattern, QRegularExpression::UseUnicodePropertiesOption}; 196 | QRegularExpressionMatch match {re.match(data)}; 197 | if (!match.hasMatch()) return false; 198 | 199 | QString texts {match.captured(1)}; 200 | // if _need_custom average, use our custom average. otherwise, use Golestan-provided average 201 | QString average = _need_custom_avg ? QString::number(_custom_average) : extractXmlAttr(texts, QStringLiteral("F4360=\"")); 202 | if (average.isEmpty()) 203 | average = QStringLiteral("-"); 204 | _score_brief.insert(QStringLiteral("average"), average); 205 | _score_brief.insert(QStringLiteral("passedUnits"), extractXmlAttr(texts, QStringLiteral("F4370=\""), false)); 206 | _score_brief.insert(QStringLiteral("semesterUnits"), extractXmlAttr(texts, QStringLiteral("F4365=\""), false)); 207 | _score_brief.insert(QStringLiteral("totalAvg"), extractXmlAttr(texts, QStringLiteral("F4360=\""), false)); 208 | _score_brief.insert(QStringLiteral("totalPassedUnits"), extractXmlAttr(texts, QStringLiteral("F4370=\""), false)); 209 | return true; 210 | } 211 | 212 | // extract the value of 'key' in XML stored in 'data' 213 | // start_from_first determine that if the function should start from the first of 'data' or continue from the last 214 | // position 215 | QString ScoresHandler::extractXmlAttr(const QString &data, const QString& key, const bool start_from_first) const 216 | { 217 | static int start_point {0}; 218 | int start_index, end_index; 219 | 220 | if (start_from_first) 221 | start_index = data.indexOf(key); 222 | else 223 | start_index = data.indexOf(key, start_point); 224 | 225 | if (start_index == -1) 226 | return QString(); 227 | 228 | // the 7 is the specific offset for Golestan keys. 229 | // keys are something like this: XXXXX=" 230 | start_index += 7; 231 | end_index = data.indexOf(QChar('"'), start_index); 232 | start_point = start_index; 233 | return data.mid(start_index, end_index - start_index); 234 | } 235 | 236 | ScoresHandler::ScoresHandler() 237 | { 238 | 239 | } 240 | 241 | QVariantList ScoresHandler::getScores() const 242 | { 243 | return _scores; 244 | } 245 | 246 | QVariantList ScoresHandler::getBriefScores() const 247 | { 248 | return QVariantList {_score_brief}; 249 | } 250 | -------------------------------------------------------------------------------- /source/helpers/errors.cpp: -------------------------------------------------------------------------------- 1 | #include "header/helpers/errors.h" 2 | 3 | Errors::Errors(QObject *parent) : QObject(parent), error_code{NoError} 4 | { 5 | 6 | } 7 | 8 | QString Errors::getErrorString() const 9 | { 10 | return error_strings[error_code]; 11 | } 12 | 13 | QString Errors::getErrorSolution() const 14 | { 15 | return error_solutions[error_code]; 16 | } 17 | 18 | int Errors::getErrorCode() const 19 | { 20 | return error_code; 21 | } 22 | 23 | // set 'error_code' to proper code by parsing 'ecode' 24 | bool Errors::setErrorCode(int ecode) 25 | { 26 | if (error_code == ecode) return false; 27 | 28 | // determine wether the ecode is about an actual error or just a success status 29 | if (ecode == NoError || ecode == (QNetworkReply::NoError + qt_offset)) 30 | error_code = NoError; 31 | 32 | // check if ecode is one of QNetworkReply::Error's then mark them all as ServerConnectionError 33 | else if (ecode >= (QNetworkReply::ConnectionRefusedError + qt_offset) && ecode <= (QNetworkReply::UnknownServerError + qt_offset)) 34 | error_code = ServerConnenctionError; 35 | 36 | // is this error code discovered befor? if not, we can't do anything about it. 37 | else if (!error_strings.contains(ecode)) 38 | error_code = UnknownError; 39 | 40 | else 41 | error_code = ecode; 42 | 43 | return true; 44 | } 45 | 46 | // returns the type of error 47 | int Errors::getErrorType() const 48 | { 49 | return critical_status.value(error_code, Normal); 50 | } 51 | 52 | void Errors::setCriticalStatus(const int ecode, const Errors::error_type type) 53 | { 54 | critical_status[ecode] = type; 55 | } 56 | 57 | void Errors::reset() 58 | { 59 | setErrorCode(NoError); 60 | } 61 | -------------------------------------------------------------------------------- /source/helpers/logger.cpp: -------------------------------------------------------------------------------- 1 | #include "header/helpers/logger.h" 2 | #include 3 | #include 4 | 5 | bool Logger::init() 6 | { 7 | if (!_file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { 8 | return false; 9 | } 10 | return true; 11 | } 12 | 13 | void Logger::log(const QByteArray &data, const bool more) 14 | { 15 | if (!_file.isOpen() && !_file.open(QIODevice::Append)) 16 | return; 17 | 18 | _file.write((QStringLiteral("=================\n[") + (QTime::currentTime()).toString() + QStringLiteral("]: ")).toUtf8()); 19 | _file.write(data); 20 | _file.write("\n=================\n"); 21 | 22 | if (!more) 23 | _file.close(); 24 | } 25 | -------------------------------------------------------------------------------- /source/main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * This file handles everything. 3 | * QML exposures occur here. 4 | * Application initialization occur here. 5 | */ 6 | 7 | #include 8 | //#include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "header/helpers/errors.h" 15 | #include "header/base/settings.h" 16 | //! handlers 17 | #include "header/handlers/inithandler.h" 18 | #include "header/handlers/loginhandler.h" 19 | #include "header/handlers/captchahandler.h" 20 | #include "header/handlers/briefinfohandler.h" 21 | #include "header/handlers/courseschedulehandler.h" 22 | #include "header/handlers/offeredcoursehandler.h" 23 | #include "header/handlers/scoreshandler.h" 24 | #include "header/handlers/accounthandler.h" 25 | //! models 26 | #include "header/models/offeredcoursemodel.h" 27 | //! Controls 28 | #include "header/controls/scheduletable.h" 29 | 30 | /// TODO: use QString::at() instead of [] for readonly purpose 31 | int main(int argc, char *argv[]) 32 | { 33 | 34 | // QDir::setCurrent("/home/moorko/cpp/boostan/boostan/test/"); 35 | // QFile file("res.html"); 36 | // if (file.open(QIODevice::ReadOnly)) { 37 | // QString rr {file.readAll()}; 38 | // OfferedCourseHandler obj; 39 | // obj.extractOfferedCourses(rr); 40 | // } else { 41 | // qDebug() << file.errorString(); 42 | // } 43 | 44 | bool universal_error{false}; 45 | int universal_error_code {0}; 46 | //! TODO: Settings will be just a static class and this shall remove 47 | Settings settings; 48 | // check if settings are available 49 | if (!settings.checkSettings()) { 50 | universal_error = true; 51 | universal_error_code = Errors::SettingsError; 52 | } 53 | 54 | QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); 55 | QApplication app(argc, argv); 56 | // QGuiApplication app(argc, argv); 57 | app.setOrganizationName(Constants::application_name); 58 | app.setOrganizationDomain(Constants::domain_name); 59 | app.setApplicationName(Constants::organization_name); 60 | app.setWindowIcon(QIcon(":/pics/boostan.ico")); 61 | 62 | QQmlApplicationEngine engine; 63 | 64 | // QML exposures 65 | 66 | /** Constants **/ 67 | 68 | engine.rootContext()->setContextProperty(QStringLiteral("ApplicationPath"), Constants::application_path); 69 | engine.rootContext()->setContextProperty(QStringLiteral("TodayDate"), Constants::today_date); 70 | engine.rootContext()->setContextProperty(QStringLiteral("Version"), Constants::version); 71 | engine.rootContext()->setContextProperty(QStringLiteral("UniversalError"), universal_error); 72 | engine.rootContext()->setContextProperty(QStringLiteral("UniversalErrorCode"), universal_error_code); 73 | 74 | /** Types **/ 75 | 76 | /* Controls back-end */ 77 | qmlRegisterType ("API.Controls.ScheduleTable", 1, 0, "ScheduleTableBackEnd"); 78 | 79 | /* Handler types */ 80 | qmlRegisterType ("API.InitHandler", 1, 0, "InitHandler"); 81 | qmlRegisterType ("API.LoginHandler", 1, 0, "LoginHandler"); 82 | qmlRegisterType ("API.LoginHandler", 1, 0, "CaptchaHandler"); 83 | qmlRegisterType ("API.BriefInfoHandler", 1, 0, "BriefInfoHandler"); 84 | qmlRegisterType ("API.CourseScheduleHandler", 1, 0, "CourseScheduleHandler"); 85 | qmlRegisterType ("API.OfferedCourseHandler", 1, 0, "OfferedCourseHandler"); 86 | qmlRegisterType ("API.ScoresHandler", 1, 0, "ScoresHandler"); 87 | qmlRegisterType ("API.AccountHandler", 1, 0, "AccountHandler"); 88 | 89 | 90 | /* Models */ 91 | qmlRegisterType ("API.OfferedCourseHandler", 1, 0, "OfferedCourseModel"); 92 | 93 | /* Helper types */ 94 | qmlRegisterType ("API.Errors", 1, 0, "Error"); 95 | /// TODO: settings will be a non-creatable object and this should change to non creatable 96 | qmlRegisterSingletonInstance("API.Settings", 1, 0, "Settings", &settings); 97 | 98 | 99 | const QUrl url(QStringLiteral("qrc:/main.qml")); 100 | QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, &app, 101 | [url](QObject *obj, const QUrl &objUrl){ 102 | if (!obj && url == objUrl) 103 | QCoreApplication::exit(-1); 104 | }, Qt::QueuedConnection); 105 | engine.load(url); 106 | return app.exec(); 107 | } 108 | -------------------------------------------------------------------------------- /source/models/offeredcoursemodel.cpp: -------------------------------------------------------------------------------- 1 | #include "header/models/offeredcoursemodel.h" 2 | 3 | OfferedCourseModel::OfferedCourseModel(QObject *parent) 4 | : QAbstractListModel(parent) 5 | { 6 | } 7 | 8 | OfferedCourseModel::~OfferedCourseModel() 9 | { 10 | cleanUp(); 11 | } 12 | 13 | int OfferedCourseModel::rowCount(const QModelIndex &parent) const 14 | { 15 | // For list models only the root node (an invalid parent) should return the list's size. For all 16 | // other (valid) parents, rowCount() should return 0 so that it does not become a tree model. 17 | if (parent.isValid()) 18 | return 0; 19 | return _data_container.size(); 20 | } 21 | 22 | QVariant OfferedCourseModel::data(const QModelIndex &index, int role) const 23 | { 24 | if (!index.isValid()) return QVariant(); 25 | 26 | int row = index.row(); 27 | int column = role - ROLE_START - 1; 28 | if (row < 0 || row >= _data_container.size()) return QVariant(); 29 | 30 | return _data_container.at(row)->at(column); 31 | } 32 | 33 | QHash OfferedCourseModel::roleNames() const 34 | { 35 | QHash roles; 36 | int column_number{0}; 37 | for (int i{ROLE_START + 1}; i != ROLE_END; ++i, ++column_number) { 38 | roles.insert(i, this->columns[column_number].toUtf8()); 39 | } 40 | return roles; 41 | } 42 | 43 | bool OfferedCourseModel::setData(const QModelIndex &index, const QVariant &value, int role) 44 | { 45 | if (data(index, role) == value) return false; 46 | 47 | int row = index.row(); 48 | int column = role - ROLE_START - 1; 49 | if (row < 0 || row >= _data_container.size()) return false; 50 | 51 | _data_container[row]->replace(column , value); 52 | emit dataChanged(index, index, QVector() << role); 53 | return true; 54 | } 55 | 56 | Qt::ItemFlags OfferedCourseModel::flags(const QModelIndex &index) const 57 | { 58 | if (!index.isValid()) return Qt::NoItemFlags; 59 | 60 | return Qt::ItemIsEditable; 61 | } 62 | 63 | int OfferedCourseModel::roleToIndex(OfferedCourseModel::roles role) 64 | { 65 | return role - ROLE_START - 1; 66 | } 67 | 68 | void OfferedCourseModel::cleanUp() 69 | { 70 | if (_data_container.isEmpty()) return; 71 | for(QVariantList* element : qAsConst(_data_container)) { 72 | delete element; 73 | element = nullptr; 74 | } 75 | _data_container.clear(); 76 | } 77 | 78 | void OfferedCourseModel::setDataContainer(QList& container) 79 | { 80 | cleanUp(); 81 | _data_container.swap(container); 82 | } 83 | 84 | QVariantMap OfferedCourseModel::toScheduleFormat(const int index) const 85 | { 86 | QVariantMap map; 87 | if (index < 0 || index >= _data_container.size()) return map; 88 | map[QStringLiteral("teacher")] = _data_container[index]->at(roleToIndex(teacherRole)); 89 | map[QStringLiteral("name")] = _data_container[index]->at(roleToIndex(courseNameRole)); 90 | map[QStringLiteral("exam")] = _data_container[index]->at(roleToIndex(examRole)).toString().replace(QStringLiteral("
"), QStringLiteral(" ")); 91 | map[QStringLiteral("warningForCourses")] = QVariantList(); 92 | map[QStringLiteral("uid")] = ScheduleTable::getUid(_data_container[index]->at(roleToIndex(courseNumberRole)).toString(), _data_container[index]->at(roleToIndex(groupRole)).toString()); 93 | 94 | QStringList times = _data_container[index]->at(roleToIndex(timeRole)).toString().split("
"); 95 | QVariantList rows, columns, lengths; 96 | float calculated_column; 97 | for (QString& time : times) { 98 | time = time.trimmed(); 99 | // the first 12 character of the 'time' is name of the day. 100 | rows.append(ScheduleTable::calculateScheduleRow(time.chopped(12))); 101 | // the length of '00:00-00:00' is 11. (the last 11 character of the 'time' string) 102 | calculated_column = ScheduleTable::calculateScheduleColumn(time.right(11)); 103 | columns.append(calculated_column); 104 | lengths.append(ScheduleTable::calculateScheduleLen(time.right(11), calculated_column)); 105 | } 106 | map[QStringLiteral("row")] = rows; 107 | map[QStringLiteral("column")] = columns; 108 | map[QStringLiteral("length")] = lengths; 109 | 110 | return map; 111 | } 112 | 113 | int OfferedCourseModel::getCourseWeight(const int index) const 114 | { 115 | return _data_container.at(index)->at(roleToIndex(weightRole)).toInt(); 116 | } 117 | 118 | void OfferedCourseModel::clearAllChoosed(const QList index_list) 119 | { 120 | for (uint index : index_list) { 121 | setData(QAbstractItemModel::createIndex(index, 0), false, isChoosedRole); 122 | } 123 | } 124 | 125 | --------------------------------------------------------------------------------