├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── api ├── models │ ├── couponmodel.cpp │ └── couponmodel.h ├── skidkzapi.cpp └── skidkzapi.h ├── assets ├── assets.qrc ├── i18n │ └── skidkz_ru.ts ├── images │ ├── bgvar1.png │ ├── example1.png │ ├── example2.png │ ├── example3.png │ └── skid_bg_2.jpg ├── qtlabscontrols.conf ├── skid.jpg └── skid.png ├── deployment.pri ├── main.cpp ├── qml ├── About.qml ├── ActualCouponsList.qml ├── ArchiveCouponsList.qml ├── CouponDetail.qml ├── CouponsDetailDelegate.qml ├── CouponsList.qml ├── CouponsListDelegate.qml ├── Search.qml ├── SearchCategories.qml ├── Statistics.qml ├── includes │ ├── Log.js │ └── Utils.qml ├── main.qml └── qml.qrc └── qtrest-example.pro /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is used to ignore files which are generated 2 | # ---------------------------------------------------------------------------- 3 | vendor/ 4 | *~ 5 | *.autosave 6 | *.a 7 | *.core 8 | *.moc 9 | *.o 10 | *.obj 11 | *.orig 12 | *.rej 13 | *.so 14 | *.so.* 15 | *_pch.h.cpp 16 | *_resource.rc 17 | *.qm 18 | .#* 19 | *.*# 20 | core 21 | !core/ 22 | tags 23 | .DS_Store 24 | .directory 25 | *.debug 26 | Makefile* 27 | *.prl 28 | *.app 29 | moc_*.cpp 30 | ui_*.h 31 | qrc_*.cpp 32 | Thumbs.db 33 | *.res 34 | *.rc 35 | /.qmake.cache 36 | /.qmake.stash 37 | 38 | # qtcreator generated files 39 | *.pro.user* 40 | 41 | # xemacs temporary files 42 | *.flc 43 | 44 | # Vim temporary files 45 | .*.swp 46 | 47 | # Visual Studio generated files 48 | *.ib_pdb_index 49 | *.idb 50 | *.ilk 51 | *.pdb 52 | *.sln 53 | *.suo 54 | *.vcproj 55 | *vcproj.*.*.user 56 | *.ncb 57 | *.sdf 58 | *.opensdf 59 | *.vcxproj 60 | *vcxproj.* 61 | 62 | # MinGW generated files 63 | *.Debug 64 | *.Release 65 | 66 | # Python byte code 67 | *.pyc 68 | 69 | # Binaries 70 | # -------- 71 | *.dll 72 | *.exe 73 | 74 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "3rd/QtAwesome"] 2 | path = 3rd/QtAwesome 3 | url = https://github.com/kafeg/QtAwesome.git 4 | 5 | [submodule "3rd/qtrest"] 6 | path = 3rd/qtrest 7 | url = https://github.com/kafeg/qtrest.git 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Vitaly Petrov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qtrest-example 2 | Full functionality example application for Qt REST library (https://github.com/kafeg/qtrest) 3 | 4 | This sample APP uses the next Backend sample as reference: https://github.com/qtrest/qtrest-example-backend/ 5 | 6 | In this example application you will found example C++ and QML code for working with API methods of http://skid.kz site. 7 | 8 | I will use it's REST API, who folows HATEOAS (https://github.com/yiisoft/yii2/blob/master/docs/guide/rest-quick-start.md): 9 | - GET /coupons: list all coupons page by page; 10 | - GET /coupons/1: return the details of the coupon 1. 11 | 12 | ## Building 13 | 14 | ``` 15 | 0. Minimal Qt version is Qt 5.7 16 | 1. Install qpm from http://qpm.io 17 | 2. git clone https://github.com/kafeg/qtrest-example.git 18 | 3. cd qtrest-example 19 | 4. lrelease qtrest-example.pro 20 | 5. git submodule init 21 | 6. git submodule update 22 | 7. qmake 23 | 8. make 24 | ``` 25 | 26 | ## Testing 27 | 28 | You may for example test this API by using those headers: 29 | - Accept: application/json 30 | - Authorization: Bearer 8aef452ee3b32466209535b96d456b06 31 | 32 | Be careful: my server is still small =) 33 | 34 | For example: curl -H "Accept: application/json" -H "Authorization: Bearer 8aef452ee3b32466209535b96d456b06" http://api.skid.kz/v1/coupon?fields=id,title,sourceServiceId,imagesLinks,mainImageLink,pageLink,cityId,boughtCount&sort=-id,title&isArchive=0 35 | 36 | Will return headers: 37 | ``` 38 | Server: nginx 39 | Date: Sun, 28 Feb 2016 11:49:34 GMT 40 | Content-Type: application/json; charset=UTF-8 41 | Transfer-Encoding: chunked 42 | Connection: keep-alive 43 | Keep-Alive: timeout=60 44 | X-Pagination-Total-Count: 7719 45 | X-Pagination-Page-Count: 386 46 | X-Pagination-Current-Page: 1 47 | X-Pagination-Per-Page: 20 48 | Link: ; rel=self, 49 | ; rel=next, 50 | ; rel=last 51 | ``` 52 | And body: 53 | ``` 54 | { 55 | { 56 | "id": 24957 57 | "sourceServiceId": 1 58 | "cityId": 1 59 | "title": "Салон красоты Vitality" 60 | "imagesLinks": { 61 | 0: "https://static.chocolife.me/static/upload/images/deal/for_deal_page/28000/27418/660x305/2_201602261123514564646959.jpg" 62 | 1: "https://static.chocolife.me/static/upload/images/deal/for_deal_page/28000/27418/660x305/3_201602261123614564646968353.jpg" 63 | 2: "https://static.chocolife.me/static/upload/images/deal/for_deal_page/28000/27418/660x305/4_20160226112111456464911014.jpg" 64 | 3: "https://static.chocolife.me/static/upload/images/deal/for_deal_page/28000/27418/660x305/5_201602261121914564649197072.jpg" 65 | 4: "https://static.chocolife.me/static/upload/images/deal/for_deal_page/28000/27418/310x240/1_20160226112361456464696371.jpg" 66 | } 67 | "mainImageLink": "https://static.chocolife.me/static/upload/images/deal/for_deal_page/28000/27418/310x240/1_20160226112361456464696371.jpg" 68 | "boughtCount": "0" 69 | "pageLink": "https://ala.chocolife.me/27418-salon-krasotyvitality/" 70 | "serviceName": "Chocolife.me" 71 | "cityName": "Алматы" 72 | }, 73 | { 74 | "id": 24956 75 | "sourceServiceId": 1 76 | "cityId": 1 77 | "title": "Салон красоты Vitality" 78 | "imagesLinks": { 79 | 0: "https://static.chocolife.me/static/upload/images/deal/for_deal_page/28000/27417/660x305/2_201602261024414564599644154.jpg" 80 | 1: "https://static.chocolife.me/static/upload/images/deal/for_deal_page/28000/27417/660x305/3_201602261020014564599803748.jpg" 81 | 2: "https://static.chocolife.me/static/upload/images/deal/for_deal_page/28000/27417/660x305/4_201602261025914564599799436.jpg" 82 | 3: "https://static.chocolife.me/static/upload/images/deal/for_deal_page/28000/27417/660x305/5_201602261024514564599651143.jpg" 83 | 4: "https://static.chocolife.me/static/upload/images/deal/for_deal_page/28000/27417/310x240/1_201602261025914564599791696.jpg" 84 | } 85 | "mainImageLink": "https://static.chocolife.me/static/upload/images/deal/for_deal_page/28000/27417/310x240/1_201602261025914564599791696.jpg" 86 | "boughtCount": "0" 87 | "pageLink": "https://ala.chocolife.me/27417-salon-krasoty-vitality/" 88 | "serviceName": "Chocolife.me" 89 | "cityName": "Алматы" 90 | }, 91 | ... 92 | } 93 | ``` 94 | ## Screenshots 95 | ![Qt Micro REST Client Framework](https://raw.githubusercontent.com/kafeg/qtrest-example/master/assets/images/example1.png "REST API Qt example 1") 96 | ![Qt Micro REST Client Framework](https://raw.githubusercontent.com/kafeg/qtrest-example/master/assets/images/example2.png "REST API Qt example 2") 97 | ![Qt Micro REST Client Framework](https://raw.githubusercontent.com/kafeg/qtrest-example/master/assets/images/example3.png "REST API Qt example 3") 98 | -------------------------------------------------------------------------------- /api/models/couponmodel.cpp: -------------------------------------------------------------------------------- 1 | #include "couponmodel.h" 2 | 3 | CouponModel::CouponModel(QObject *parent) : AbstractJsonRestListModel(parent) 4 | { 5 | 6 | } 7 | 8 | QNetworkReply *CouponModel::fetchMoreImpl(const QModelIndex &parent) 9 | { 10 | Q_UNUSED(parent) 11 | 12 | return static_cast(apiInstance())->getCoupons(sort(), pagination(), filters(), fields()); 13 | } 14 | 15 | QNetworkReply *CouponModel::fetchDetailImpl(QString id) 16 | { 17 | return static_cast(apiInstance())->getCouponDetail(id); 18 | } 19 | 20 | QVariantMap CouponModel::preProcessItem(QVariantMap item) 21 | { 22 | QDate date = QDateTime::fromString(item.value("createTimestamp").toString(), "yyyy-MM-dd hh:mm:ss").date(); 23 | item.insert("createDate", date.toString("dd.MM.yyyy")); 24 | 25 | QString originalCouponPrice = item.value("originalCouponPrice").toString().trimmed(); 26 | if (originalCouponPrice.isEmpty()) { originalCouponPrice = "?"; } 27 | QString discountPercent = item.value("discountPercent").toString().trimmed().remove("—").remove("-").remove("%"); 28 | if (discountPercent.isEmpty()) { discountPercent = "?"; } 29 | QString originalPrice = item.value("originalPrice").toString().trimmed(); 30 | if (originalPrice.isEmpty()) { originalPrice = "?"; } 31 | QString discountPrice = item.value("discountPrice").toString().remove("тг.").trimmed(); 32 | if (discountPrice.isEmpty()) { discountPrice = "?"; } 33 | 34 | QString discountType = item.value("discountType").toString(); 35 | QString discountString = tr("Undefined Type"); 36 | if (discountType == "freeCoupon" || discountType == "coupon") { 37 | discountString = tr("Coupon: %1. Discount: %2%").arg(originalCouponPrice).arg(discountPercent); 38 | } else if (discountType == "full") { 39 | discountString = tr("Cost: %1. Certificate: %2. Discount: %3%").arg(originalPrice).arg(discountPrice).arg(discountPercent); 40 | } 41 | 42 | item.insert("discountString", discountString); 43 | 44 | return item; 45 | } 46 | 47 | -------------------------------------------------------------------------------- /api/models/couponmodel.h: -------------------------------------------------------------------------------- 1 | #ifndef COUPONMODEL_H 2 | #define COUPONMODEL_H 3 | 4 | #include "abstractjsonrestlistmodel.h" 5 | #include "api/skidkzapi.h" 6 | 7 | class CouponModel : public AbstractJsonRestListModel 8 | { 9 | Q_OBJECT 10 | 11 | public: 12 | explicit CouponModel(QObject *parent = 0); 13 | 14 | static void declareQML() { 15 | AbstractJsonRestListModel::declareQML(); 16 | qmlRegisterType("com.github.qtrestexample.coupons", 1, 0, "CouponModel"); 17 | } 18 | 19 | protected: 20 | QNetworkReply *fetchMoreImpl(const QModelIndex &parent); 21 | QNetworkReply *fetchDetailImpl(QString id); 22 | QVariantMap preProcessItem(QVariantMap item); 23 | }; 24 | 25 | #endif // COUPONMODEL_H 26 | -------------------------------------------------------------------------------- /api/skidkzapi.cpp: -------------------------------------------------------------------------------- 1 | #include "skidkzapi.h" 2 | #include 3 | #include 4 | #include 5 | 6 | SkidKZApi::SkidKZApi() : APIBase(0) 7 | { 8 | 9 | } 10 | 11 | QNetworkReply *SkidKZApi::handleRequest(QString path, QStringList sort, Pagination *pagination, 12 | QVariantMap filters, QStringList fields, QString id) 13 | { 14 | if (path == "/v1/coupon") { 15 | return getCoupons(sort, pagination, filters, fields); 16 | } 17 | else if (path == "/v1/coupon/{id}") { 18 | return getCouponDetail(id); 19 | } 20 | else if (path == "/v1/categories") { 21 | return getCategories(sort, pagination); 22 | } 23 | } 24 | 25 | //In this methods we get list of objects, based on specified page number, filters, sort and fileds list. 26 | //We can fetch all fields or only needed in our list. 27 | QNetworkReply *SkidKZApi::getCoupons(QStringList sort, Pagination *pagination, QVariantMap filters, QStringList fields) 28 | { 29 | //URL and GET parameters 30 | QUrl url = QUrl(baseUrl()+"/v1/coupon"); 31 | QUrlQuery query; 32 | 33 | //Specify sort GET param 34 | if (!sort.isEmpty()) { 35 | query.addQueryItem("sort", sort.join(",")); 36 | } 37 | 38 | //Specify pagination. We use pagination type from model. 39 | switch(pagination->policy()) { 40 | case Pagination::PageNumber: 41 | query.addQueryItem("per-page", QString::number(pagination->perPage())); 42 | query.addQueryItem("page", QString::number(pagination->currentPage())); 43 | break; 44 | case Pagination::None: 45 | case Pagination::Infinity: 46 | case Pagination::LimitOffset: 47 | case Pagination::Cursor: 48 | default: 49 | break; 50 | } 51 | 52 | //if we need to filter our model, we use filters. 53 | //Be careful, if you use this methods, your curent pagintaion wil be broken 54 | //and you must full reaload your model data when you specify new filters 55 | 56 | if (!filters.isEmpty()) { 57 | QMapIterator i(filters); 58 | while (i.hasNext()) { 59 | i.next(); 60 | query.addQueryItem(i.key(), i.value().toString()); 61 | } 62 | } 63 | 64 | //We may to get all or spicified fields in this method. 65 | if (!fields.isEmpty()) { 66 | query.addQueryItem("fields", fields.join(",")); 67 | } 68 | 69 | url.setQuery(query.query()); 70 | 71 | QNetworkReply *reply = get(url); 72 | 73 | return reply; 74 | } 75 | 76 | //If we fetch e.g. 3 of 10 fields in our 'getCoupons' methods, 77 | //we may to get full information from needed item by it ID 78 | QNetworkReply *SkidKZApi::getCouponDetail(QString id) 79 | { 80 | if (id.isEmpty()) { 81 | qDebug() << "ID is empty!"; 82 | return 0; 83 | } 84 | 85 | QUrl url = QUrl(baseUrl()+"/v1/coupon/"+id); 86 | 87 | QNetworkReply *reply = get(url); 88 | 89 | return reply; 90 | } 91 | 92 | QNetworkReply *SkidKZApi::getCategories(QStringList sort, Pagination *pagination) 93 | { 94 | //URL and GET parameters 95 | QUrl url = QUrl(baseUrl()+"/v1/categories"); 96 | QUrlQuery query; 97 | 98 | //Specify sort GET param 99 | if (!sort.isEmpty()) { 100 | query.addQueryItem("sort", sort.join(",")); 101 | } 102 | 103 | //Specify pagination. We use pagination type from model. 104 | switch(pagination->policy()) { 105 | case Pagination::PageNumber: 106 | query.addQueryItem("per-page", QString::number(pagination->perPage())); 107 | query.addQueryItem("page", QString::number(pagination->currentPage())); 108 | break; 109 | case Pagination::None: 110 | case Pagination::Infinity: 111 | case Pagination::LimitOffset: 112 | case Pagination::Cursor: 113 | default: 114 | break; 115 | } 116 | 117 | url.setQuery(query.query()); 118 | 119 | QNetworkReply *reply = get(url); 120 | 121 | return reply; 122 | } 123 | -------------------------------------------------------------------------------- /api/skidkzapi.h: -------------------------------------------------------------------------------- 1 | #ifndef SKIDKZAPI_H 2 | #define SKIDKZAPI_H 3 | 4 | #include "apibase.h" 5 | #include 6 | 7 | class SkidKZApi : public APIBase 8 | { 9 | Q_OBJECT 10 | public: 11 | Q_INVOKABLE explicit SkidKZApi(); 12 | 13 | static void declareQML() { 14 | qmlRegisterType("com.github.qtrestexample.skidkzapi", 1, 0, "SkidKZApi"); 15 | } 16 | 17 | //requests 18 | QNetworkReply *handleRequest(QString path, QStringList sort, Pagination *pagination, 19 | QVariantMap filters = QVariantMap(), QStringList fields = QStringList(), QString id = 0); 20 | 21 | //get list of objects 22 | QNetworkReply *getCoupons(QStringList sort, Pagination *pagination, 23 | QVariantMap filters = QVariantMap(), QStringList fields = QStringList()); 24 | //get full data for specified item 25 | QNetworkReply *getCouponDetail(QString id); 26 | 27 | QNetworkReply *getCategories(QStringList sort, Pagination *pagination); 28 | }; 29 | 30 | #endif // SKIDKZAPI_H 31 | -------------------------------------------------------------------------------- /assets/assets.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | images/bgvar1.png 4 | images/skid_bg_2.jpg 5 | i18n/skidkz_ru.qm 6 | qtlabscontrols.conf 7 | 8 | 9 | -------------------------------------------------------------------------------- /assets/i18n/skidkz_ru.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | About 6 | 7 | 8 | About 9 | О программе 10 | 11 | 12 | 13 | ActualCouponsList 14 | 15 | 16 | Actual 17 | Актуальные 18 | 19 | 20 | 21 | ArchiveCouponsList 22 | 23 | 24 | Archive 25 | Архив 26 | 27 | 28 | 29 | CouponDetail 30 | 31 | 32 | Detail 33 | Подробности 34 | 35 | 36 | 37 | CouponModel 38 | 39 | 40 | Undefined Type 41 | Скидка неизвестна 42 | 43 | 44 | 45 | Coupon: %1. Discount: %2% 46 | Купон: %1. Скидка: %2% 47 | 48 | 49 | 50 | Cost: %1. Certificate: %2. Discount: %3% 51 | Цена: %1. Сертификат: %2. Скидка: %3% 52 | 53 | 54 | 55 | CouponsDetailDelegate 56 | 57 | 58 | Bought 59 | Купили 60 | 61 | 62 | 63 | City 64 | Город 65 | 66 | 67 | 68 | Date 69 | Добавлено 70 | 71 | 72 | 73 | Service 74 | Сервис 75 | 76 | 77 | 78 | Buy 79 | 80 | 81 | 82 | 83 | Full description 84 | 85 | 86 | 87 | 88 | CouponsList 89 | 90 | 91 | List is empty 92 | Список пуст 93 | 94 | 95 | 96 | CouponsListDelegate 97 | 98 | 99 | Bought 100 | Купили 101 | 102 | 103 | 104 | City 105 | Город 106 | 107 | 108 | 109 | Date 110 | Добавлено 111 | 112 | 113 | 114 | Service 115 | Сервис 116 | 117 | 118 | 119 | Search 120 | 121 | 122 | Search 123 | 124 | 125 | 126 | 127 | Statistics 128 | 129 | 130 | Statistics 131 | Статистика 132 | 133 | 134 | 135 | main 136 | 137 | 138 | Skid.KZ 139 | Skid.KZ 140 | 141 | 142 | 143 | Actual 144 | Актуальные 145 | 146 | 147 | 148 | Archive 149 | Архив 150 | 151 | 152 | 153 | Statistics 154 | Статистика 155 | 156 | 157 | 158 | About 159 | О программе 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /assets/images/bgvar1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qtrest/qtrest-example/f20d2924b7d74609e32ccd53ef765298105cde22/assets/images/bgvar1.png -------------------------------------------------------------------------------- /assets/images/example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qtrest/qtrest-example/f20d2924b7d74609e32ccd53ef765298105cde22/assets/images/example1.png -------------------------------------------------------------------------------- /assets/images/example2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qtrest/qtrest-example/f20d2924b7d74609e32ccd53ef765298105cde22/assets/images/example2.png -------------------------------------------------------------------------------- /assets/images/example3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qtrest/qtrest-example/f20d2924b7d74609e32ccd53ef765298105cde22/assets/images/example3.png -------------------------------------------------------------------------------- /assets/images/skid_bg_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qtrest/qtrest-example/f20d2924b7d74609e32ccd53ef765298105cde22/assets/images/skid_bg_2.jpg -------------------------------------------------------------------------------- /assets/qtlabscontrols.conf: -------------------------------------------------------------------------------- 1 | [Controls] 2 | Style=Material 3 | 4 | [Material] 5 | Primary=LightGreen 6 | Accent=LightGreen 7 | Theme=Dark 8 | -------------------------------------------------------------------------------- /assets/skid.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qtrest/qtrest-example/f20d2924b7d74609e32ccd53ef765298105cde22/assets/skid.jpg -------------------------------------------------------------------------------- /assets/skid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qtrest/qtrest-example/f20d2924b7d74609e32ccd53ef765298105cde22/assets/skid.png -------------------------------------------------------------------------------- /deployment.pri: -------------------------------------------------------------------------------- 1 | unix:!android { 2 | isEmpty(target.path) { 3 | qnx { 4 | target.path = /tmp/$${TARGET}/bin 5 | } else { 6 | target.path = /opt/$${TARGET}/bin 7 | } 8 | export(target.path) 9 | } 10 | INSTALLS += target 11 | } 12 | 13 | export(INSTALLS) 14 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "api/models/couponmodel.h" 4 | #include "QtAwesomeAndroid.h" 5 | #include 6 | #include 7 | 8 | #include "jsonrestlistmodel.h" 9 | 10 | int main(int argc, char *argv[]) 11 | { 12 | QGuiApplication::setApplicationName("Skid.KZ"); 13 | QGuiApplication::setApplicationVersion("1.0"); 14 | QGuiApplication::setOrganizationName("Forsk.Ru"); 15 | QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); 16 | 17 | QGuiApplication app(argc, argv); 18 | 19 | //i18n 20 | QString languageCode = QLocale::system().name(); 21 | if (languageCode.contains("_")) { 22 | languageCode = languageCode.split("_").at(0); 23 | } 24 | QString fileName = "skidkz_" + languageCode; 25 | 26 | QTranslator qtTranslator; 27 | if ( !qtTranslator.load(fileName, ":/i18n/") ){ 28 | qDebug() << "Translation file not loaded:" << fileName; 29 | qDebug() << "Language " << languageCode << " not supported yet"; 30 | } 31 | app.installTranslator(&qtTranslator); 32 | 33 | //Font Awesome 34 | QtAwesomeAndroid* awesome = new QtAwesomeAndroid( qApp ); 35 | awesome->setDefaultOption( "color", QColor(255,255,255) ); 36 | awesome->initFontAwesome(); 37 | 38 | //api and models 39 | SkidKZApi::declareQML(); 40 | CouponModel::declareQML(); 41 | JsonRestListModel::declareQML(); 42 | 43 | //settings 44 | QSettings settings; 45 | qputenv("QT_LABS_CONTROLS_STYLE", settings.value("style").toByteArray()); 46 | 47 | QQmlApplicationEngine engine; 48 | engine.rootContext()->setContextProperty("awesome", awesome); 49 | engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); 50 | 51 | return app.exec(); 52 | } 53 | -------------------------------------------------------------------------------- /qml/About.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.6 2 | 3 | Rectangle { 4 | //anchors.fill: parent 5 | 6 | property string titleText: qsTr("About") 7 | 8 | Image { 9 | id: background 10 | source: "qrc:/images/bgvar1.png" 11 | fillMode: Image.Tile 12 | anchors.fill: parent 13 | } 14 | 15 | Text { 16 | id: about 17 | text: "About page" 18 | anchors.centerIn: parent 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /qml/ActualCouponsList.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.6 2 | import com.github.qtrest.pagination 1.0 3 | import com.github.qtrestexample.coupons 1.0 4 | 5 | import "includes/Log.js" as Log 6 | 7 | CouponsList { 8 | //anchors.fill: parent 9 | 10 | property string titleText: qsTr("Actual") 11 | 12 | function filter(filters) 13 | { 14 | filters.isArchive = 0; 15 | coupons.filters = filters; 16 | coupons.reload(); 17 | } 18 | 19 | couponsModel: CouponModel { 20 | id: coupons; 21 | api: skidKZApi 22 | 23 | filters: {'isArchive': '0'} 24 | idField: 'id' 25 | fields: ['id','title','sourceServiceId','imagesLinks','mainImageLink','pageLink','cityId','boughtCount','shortDescription', 26 | 'createTimestamp', 'serviceName', 'discountType', 'originalCouponPrice', 'originalPrice', 'discountPercent', 'discountPrice'] 27 | sort: ['-id'] 28 | 29 | pagination { 30 | policy: Pagination.PageNumber 31 | perPage: 20 32 | currentPageHeader: "X-Pagination-Current-Page" 33 | totalCountHeader: "X-Pagination-Total-Count" 34 | pageCountHeader: "X-Pagination-Page-Count" 35 | } 36 | 37 | Component.onCompleted: { console.log(pagination.perPage); reload(); } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /qml/ArchiveCouponsList.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.6 2 | import com.github.qtrest.pagination 1.0 3 | import com.github.qtrestexample.coupons 1.0 4 | 5 | CouponsList { 6 | //anchors.fill: parent 7 | 8 | property string titleText: qsTr("Archive") 9 | 10 | function filter(filters) 11 | { 12 | filters.isArchive = 0; 13 | coupons.filters = filters; 14 | coupons.reload(); 15 | } 16 | 17 | couponsModel: CouponModel { 18 | id: coupons; 19 | api: skidKZApi 20 | 21 | filters: {'isArchive': '1'} 22 | idField: 'id' 23 | fields: ['id','title','sourceServiceId','imagesLinks','mainImageLink','pageLink','cityId', 24 | 'boughtCount','shortDescription','createTimestamp', 'serviceName'] 25 | sort: ['-id'] 26 | 27 | pagination { 28 | policy: Pagination.PageNumber 29 | perPage: 20 30 | currentPageHeader: "X-Pagination-Current-Page" 31 | totalCountHeader: "X-Pagination-Total-Count" 32 | pageCountHeader: "X-Pagination-Page-Count" 33 | } 34 | 35 | Component.onCompleted: reload() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /qml/CouponDetail.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.6 2 | import QtQuick.Controls 2.0 3 | import com.github.qtrestexample.coupons 1.0 4 | 5 | Item { 6 | id: details 7 | anchors.fill: parent 8 | 9 | property string titleText: qsTr("Detail") 10 | 11 | property var detailsModel 12 | property var couponsModel 13 | 14 | property var loadingStatus: couponsModel.loadingStatus 15 | 16 | onLoadingStatusChanged: { 17 | if (loadingStatus == CouponModel.IdleDetails) { 18 | pageLoader.sourceComponent = detailComponent 19 | } 20 | } 21 | 22 | Loader { 23 | id: pageLoader 24 | } 25 | 26 | MouseArea { 27 | anchors.fill: parent 28 | onClicked: pageLoader.sourceComponent = detailComponent 29 | } 30 | 31 | BusyIndicator { 32 | id: loadingIndicator 33 | width: settings.busyIndicatorSize*1.5 34 | height: settings.busyIndicatorSize*1.5 35 | 36 | running: loadingStatus == CouponModel.LoadDetailsProcessing 37 | visible: opacity > 0 38 | opacity: loadingStatus == CouponModel.LoadDetailsProcessing ? 1 : 0 39 | anchors.centerIn: parent 40 | Behavior on opacity { 41 | NumberAnimation { duration: 400; } 42 | } 43 | } 44 | 45 | Component { 46 | id: detailComponent 47 | 48 | ListView { 49 | id: couponsList 50 | width:details.width 51 | height: details.height 52 | spacing: settings.spacing 53 | model: detailsModel 54 | maximumFlickVelocity: 5000 55 | interactive: false 56 | Component.onCompleted: console.log("loaded") 57 | 58 | header: Item { 59 | id: listHeader 60 | width: parent.width 61 | height: settings.spacing 62 | } 63 | 64 | delegate: CouponsDetailDelegate { 65 | id: delegate 66 | width: couponsList.width; 67 | anchors.horizontalCenter: parent.horizontalCenter 68 | 69 | //Component.onCompleted: { console.log(imagesLinks) } 70 | } 71 | //Component.onCompleted: { console.log("details count", detailsModel.rowCount()) } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /qml/CouponsDetailDelegate.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.6 2 | import QtQuick.Controls 2.0 3 | import QtQuick.Layouts 1.3 4 | 5 | Item { 6 | id: couponDelegate 7 | height: information.height 8 | 9 | property string detailSource: "qrc:/CouponDetail.qml" 10 | 11 | Flickable { 12 | anchors.fill: parent 13 | flickableDirection: Flickable.VerticalFlick 14 | 15 | Rectangle { 16 | id: rectangle1 17 | anchors.fill: parent 18 | color: "black" 19 | opacity: 0.5 20 | 21 | border.color: "black" 22 | border.width: 0 23 | } 24 | 25 | Column { 26 | id: information 27 | width: parent.width 28 | anchors.centerIn: parent 29 | 30 | Item { 31 | id: imageContainer 32 | width: parent.width 33 | height: width * 0.5 34 | 35 | anchors { 36 | horizontalCenter: parent.horizontalCenter 37 | } 38 | 39 | Image { 40 | id: bgImage 41 | width: parent.width 42 | height: parent.height 43 | source: "qrc:/images/skid_bg_2.jpg" 44 | fillMode: Image.PreserveAspectCrop 45 | clip: true 46 | visible: image.status == Image.Error || image.status == Image.Null 47 | } 48 | 49 | Item { 50 | anchors.fill: parent 51 | Image { 52 | id: image 53 | width: parent.width 54 | height: parent.height 55 | source: mainImageLink 56 | fillMode: Image.PreserveAspectCrop 57 | clip: true 58 | } 59 | 60 | Item { 61 | width: parent.width 62 | height: discountText.height*1.1 63 | 64 | Rectangle { 65 | anchors.fill: parent 66 | color:"black" 67 | opacity: 0.3 68 | } 69 | 70 | Text { 71 | id: discountText 72 | text: discountString 73 | font.bold: true 74 | font.pointSize: 14 75 | color: "white" 76 | wrapMode: Text.WordWrap 77 | width: parent.width-settings.spacing 78 | clip: true 79 | maximumLineCount: 2 80 | horizontalAlignment: Text.AlignHCenter 81 | anchors.horizontalCenter: parent.horizontalCenter 82 | } 83 | } 84 | } 85 | 86 | ProgressBar { 87 | id: progressBar 88 | indeterminate: true 89 | visible: image.status != Image.Ready 90 | width: parent.width / 2 91 | anchors.centerIn: parent 92 | } 93 | } 94 | 95 | Text { 96 | id: titleText 97 | text: title 98 | font.bold: true 99 | font.pointSize: 16 100 | color: "white" 101 | wrapMode: Text.WordWrap 102 | width: parent.width-settings.spacing 103 | clip: true 104 | maximumLineCount: 2 105 | horizontalAlignment: Text.AlignHCenter 106 | anchors.horizontalCenter: parent.horizontalCenter 107 | } 108 | 109 | Text { 110 | id: description 111 | text: shortDescription 112 | font.bold: true 113 | font.pointSize: 10 114 | color: "white" 115 | wrapMode: Text.WordWrap 116 | width: parent.width-settings.spacing 117 | clip: true 118 | maximumLineCount: 4 119 | horizontalAlignment: Text.AlignHCenter 120 | anchors.horizontalCenter: parent.horizontalCenter 121 | } 122 | 123 | Rectangle { 124 | width: parent.width 125 | height: 1 126 | border.color: "grey" 127 | color: "grey" 128 | } 129 | 130 | RowLayout { 131 | width: parent.width 132 | height: boughtCol.height*1.5 133 | spacing: 0 134 | 135 | Item { 136 | id: r1 137 | height: boughtCol.height 138 | 139 | Layout.fillWidth: true 140 | Column { 141 | id: boughtCol 142 | anchors.centerIn: parent 143 | Text { 144 | id: boughtText 145 | text: qsTr("Bought") 146 | anchors.horizontalCenter: parent.horizontalCenter 147 | color: "white" 148 | font.pointSize: 10 149 | } 150 | Text { 151 | id: boughtLbl 152 | text: boughtCount 153 | anchors.horizontalCenter: parent.horizontalCenter 154 | color: "white" 155 | font.pointSize: 8 156 | } 157 | } 158 | } 159 | 160 | Rectangle { 161 | width: 1 162 | height: parent.height 163 | border.color: "grey" 164 | color: "grey" 165 | } 166 | 167 | Item { 168 | id: r2 169 | height: cityCol.height 170 | 171 | Layout.fillWidth: true 172 | Column { 173 | id: cityCol 174 | anchors.centerIn: parent 175 | Text { 176 | id: cityText 177 | text: qsTr("City") 178 | anchors.horizontalCenter: parent.horizontalCenter 179 | color: "white" 180 | font.pointSize: 10 181 | } 182 | Text { 183 | id: cityLbl 184 | text: cityName 185 | anchors.horizontalCenter: parent.horizontalCenter 186 | color: "white" 187 | font.pointSize: 8 188 | } 189 | } 190 | } 191 | 192 | Rectangle { 193 | width: 1 194 | height: parent.height 195 | border.color: "grey" 196 | color: "grey" 197 | } 198 | 199 | Item { 200 | id: r3 201 | height: dateCol.height 202 | 203 | Layout.fillWidth: true 204 | Column { 205 | id: dateCol 206 | anchors.centerIn: parent 207 | Text { 208 | id: dateText 209 | text: qsTr("Date") 210 | anchors.horizontalCenter: parent.horizontalCenter 211 | color: "white" 212 | font.pointSize: 10 213 | } 214 | Text { 215 | id: dateLbl 216 | text: createDate 217 | anchors.horizontalCenter: parent.horizontalCenter 218 | color: "white" 219 | font.pointSize: 8 220 | } 221 | } 222 | } 223 | 224 | Rectangle { 225 | width: 1 226 | height: parent.height 227 | border.color: "grey" 228 | color: "grey" 229 | } 230 | 231 | Item { 232 | id: r4 233 | height: serviceCol.height 234 | 235 | Layout.fillWidth: true 236 | Column { 237 | id: serviceCol 238 | anchors.centerIn: parent 239 | Text { 240 | id: serviceText 241 | text: qsTr("Service") 242 | anchors.horizontalCenter: parent.horizontalCenter 243 | color: "white" 244 | font.pointSize: 10 245 | } 246 | Text { 247 | id: serviceLbl 248 | text: serviceName 249 | anchors.horizontalCenter: parent.horizontalCenter 250 | color: "white" 251 | font.pointSize: 8 252 | } 253 | } 254 | } 255 | } 256 | 257 | Rectangle { 258 | width: parent.width 259 | height: 1 260 | border.color: "grey" 261 | color: "grey" 262 | } 263 | 264 | Item { 265 | id: buyBtn 266 | height: btn.height*1.5 267 | width: parent.width 268 | 269 | Button { 270 | id: btn 271 | text: qsTr("Buy") 272 | onClicked: Qt.openUrlExternally(pageLink) 273 | anchors.centerIn: parent 274 | } 275 | } 276 | 277 | Rectangle { 278 | width: parent.width 279 | height: 1 280 | border.color: "grey" 281 | color: "grey" 282 | } 283 | 284 | Item { 285 | height: longDescriptionCol.height 286 | 287 | width: parent.width 288 | Column { 289 | id: longDescriptionCol 290 | anchors.centerIn: parent 291 | Text { 292 | id: longDescriptionColText 293 | text: qsTr("Full description") 294 | anchors.horizontalCenter: parent.horizontalCenter 295 | color: "white" 296 | font.pointSize: 14 297 | } 298 | Text { 299 | id: longDescriptionLbl 300 | text: longDescription 301 | width: parent.width 302 | color: "white" 303 | font.pointSize: 8 304 | } 305 | } 306 | } 307 | } 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /qml/CouponsList.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.6 2 | import QtQuick.Controls 2.0 3 | import QtQuick.Layouts 1.3 4 | import com.github.qtrestexample.coupons 1.0 5 | 6 | Item { 7 | id: couponsContainer 8 | 9 | property var couponsModel 10 | 11 | ProgressBar { 12 | id: progressBar 13 | 14 | property int percent: couponsList.contentY 15 | property int maxPercent: -200 16 | property int isLoading: couponsModel.count > 0 && (couponsContainer.couponsModel.loadingStatus == CouponModel.FullReloadProcessing) 17 | 18 | from: 0 19 | value: percent 20 | to: maxPercent 21 | 22 | indeterminate: isLoading 23 | 24 | opacity: isLoading ? 1 : position 25 | visible: opacity > 0 26 | 27 | width: parent.width 28 | padding: 0 29 | z: 100 30 | } 31 | 32 | ListView { 33 | id: couponsList 34 | anchors.fill: parent 35 | spacing: settings.spacing 36 | model: couponsContainer.couponsModel 37 | visible: couponsContainer.couponsModel.count > 0 38 | maximumFlickVelocity: 5000 39 | interactive: true 40 | 41 | ScrollIndicator.vertical: ScrollIndicator { } 42 | 43 | header: Item { 44 | id: listHeader 45 | width: parent.width 46 | height: settings.spacing 47 | } 48 | 49 | delegate: CouponsListDelegate { 50 | id: delegate 51 | opacity: 0 52 | width: couponsList.width - settings.spacing; 53 | anchors.horizontalCenter: parent.horizontalCenter 54 | 55 | Component.onCompleted: showAnim.start(); 56 | transform: [ 57 | Scale { id: scale; origin.x: width/2; origin.y: height/2; xScale: 0.5; yScale: 0.5 }, 58 | Rotation { id: rotation; origin.x: width/2; origin.y: height/2; axis { x: x; y: 0; z: 0 } angle: 0 } 59 | ] 60 | ParallelAnimation { 61 | id: showAnim 62 | running: false 63 | NumberAnimation { target: scale; to: 1; duration: 300; properties: "xScale,yScale" } 64 | RotationAnimation { target: rotation; from: (couponsList.verticalVelocity >= 0 ? -90 : 90); to: 0; duration: 300; property: "angle" } 65 | NumberAnimation { target: delegate; to: 1; duration: 300; properties: "opacity" } 66 | } 67 | } 68 | 69 | onContentYChanged: { 70 | if (!draggingVertically) { 71 | return; 72 | } 73 | if (contentY < -200) { 74 | couponsContainer.couponsModel.requestToReload() 75 | } else { 76 | couponsContainer.couponsModel.forceIdle() 77 | } 78 | } 79 | 80 | onMovementEnded: { 81 | if (couponsContainer.couponsModel.loadingStatus == CouponModel.RequestToReload) { 82 | couponsContainer.couponsModel.reload() 83 | } 84 | } 85 | } 86 | 87 | MouseArea { 88 | id: moveToTop 89 | width: parent.width 90 | height: settings.spacing 91 | anchors { 92 | top: couponsList.top 93 | left: couponsList.left 94 | } 95 | onClicked: { 96 | couponsList.positionViewAtBeginning() 97 | couponsModel.forceIdle() 98 | } 99 | } 100 | 101 | BusyIndicator { 102 | id: emptyIndicator 103 | width: settings.busyIndicatorSize*1.5 104 | height: settings.busyIndicatorSize*1.5 105 | 106 | running: couponsContainer.couponsModel.count == 0 && (couponsContainer.couponsModel.loadingStatus != CouponModel.Idle || couponsContainer.couponsModel.loadingStatus != CouponModel.Error) 107 | visible: opacity > 0 108 | opacity: couponsContainer.couponsModel.count == 0 && (couponsContainer.couponsModel.loadingStatus != CouponModel.Idle || couponsContainer.couponsModel.loadingStatus != CouponModel.Error) ? 1 : 0 109 | anchors.centerIn: parent 110 | Behavior on opacity { 111 | NumberAnimation { duration: 400; } 112 | } 113 | } 114 | 115 | Text { 116 | id: emptyText 117 | visible: opacity > 0 118 | opacity: couponsContainer.couponsModel.count == 0 && (couponsContainer.couponsModel.loadingStatus == CouponModel.Idle || couponsContainer.couponsModel.loadingStatus == CouponModel.Error) ? 1 : 0 119 | anchors.centerIn: parent 120 | text: couponsContainer.couponsModel.loadingStatus != CouponModel.Error ? qsTr("List is empty") : couponsContainer.couponsModel.loadingErrorString 121 | font.pointSize: 18 122 | font.bold: true 123 | color: "white" 124 | Behavior on opacity { 125 | NumberAnimation { duration: 400; } 126 | } 127 | } 128 | 129 | BusyIndicator { 130 | id: loadMoreIndicator 131 | width: settings.busyIndicatorSize 132 | height: settings.busyIndicatorSize 133 | 134 | y: couponsContainer.couponsModel.loadingStatus == CouponModel.LoadMoreProcessing ? couponsList.y + couponsList.height - settings.bottomPadding - height : root.height 135 | 136 | Behavior on y { 137 | NumberAnimation { duration: 400; easing.type: Easing.InOutBack } 138 | } 139 | 140 | anchors { 141 | horizontalCenter: parent.horizontalCenter 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /qml/CouponsListDelegate.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.6 2 | import QtQuick.Controls 2.0 3 | import QtQuick.Layouts 1.3 4 | 5 | Item { 6 | id: couponDelegate 7 | height: information.height 8 | 9 | property string detailSource: "qrc:/CouponDetail.qml" 10 | 11 | Rectangle { 12 | id: rectangle1 13 | anchors.fill: parent 14 | color: "black" 15 | opacity: 0.5 16 | 17 | border.color: "black" 18 | border.width: 0 19 | } 20 | 21 | Column { 22 | id: information 23 | width: parent.width 24 | //height: parent.height - couponsList.spacing 25 | anchors.centerIn: parent 26 | 27 | Item { 28 | id: imageContainer 29 | width: parent.width 30 | height: width * 0.5 31 | 32 | anchors { 33 | horizontalCenter: parent.horizontalCenter 34 | } 35 | 36 | Image { 37 | id: bgImage 38 | width: parent.width 39 | height: parent.height 40 | source: "qrc:/images/skid_bg_2.jpg" 41 | fillMode: Image.PreserveAspectCrop 42 | clip: true 43 | visible: image.status == Image.Error || image.status == Image.Null 44 | } 45 | 46 | Item { 47 | anchors.fill: parent 48 | Image { 49 | id: image 50 | width: parent.width 51 | height: parent.height 52 | source: mainImageLink 53 | fillMode: Image.PreserveAspectCrop 54 | clip: true 55 | } 56 | 57 | Item { 58 | width: parent.width 59 | height: discountText.height*1.1 60 | 61 | Rectangle { 62 | anchors.fill: parent 63 | color:"black" 64 | opacity: 0.3 65 | } 66 | 67 | Text { 68 | id: discountText 69 | text: discountString 70 | font.bold: true 71 | font.pointSize: 14 72 | color: "white" 73 | wrapMode: Text.WordWrap 74 | width: parent.width-settings.spacing 75 | clip: true 76 | maximumLineCount: 2 77 | horizontalAlignment: Text.AlignHCenter 78 | anchors.horizontalCenter: parent.horizontalCenter 79 | } 80 | } 81 | } 82 | 83 | ProgressBar { 84 | id: progressBar 85 | indeterminate: true 86 | visible: image.status != Image.Ready 87 | width: parent.width / 2 88 | anchors.centerIn: parent 89 | } 90 | } 91 | 92 | Text { 93 | id: titleText 94 | text: title 95 | font.bold: true 96 | font.pointSize: 16 97 | color: "white" 98 | wrapMode: Text.WordWrap 99 | width: parent.width-settings.spacing 100 | clip: true 101 | maximumLineCount: 2 102 | horizontalAlignment: Text.AlignHCenter 103 | anchors.horizontalCenter: parent.horizontalCenter 104 | } 105 | 106 | Text { 107 | id: description 108 | text: shortDescription 109 | font.bold: true 110 | font.pointSize: 10 111 | color: "white" 112 | wrapMode: Text.WordWrap 113 | width: parent.width-settings.spacing 114 | clip: true 115 | maximumLineCount: 4 116 | horizontalAlignment: Text.AlignHCenter 117 | anchors.horizontalCenter: parent.horizontalCenter 118 | } 119 | 120 | Rectangle { 121 | width: parent.width 122 | height: 1 123 | border.color: "grey" 124 | color: "grey" 125 | } 126 | 127 | RowLayout { 128 | width: parent.width 129 | height: boughtCol.height*1.5 130 | spacing: 0 131 | 132 | Item { 133 | id: r1 134 | height: boughtCol.height 135 | 136 | Layout.fillWidth: true 137 | Column { 138 | id: boughtCol 139 | anchors.centerIn: parent 140 | Text { 141 | id: boughtText 142 | text: qsTr("Bought") 143 | anchors.horizontalCenter: parent.horizontalCenter 144 | color: "white" 145 | font.pointSize: 10 146 | } 147 | Text { 148 | id: boughtLbl 149 | text: boughtCount 150 | anchors.horizontalCenter: parent.horizontalCenter 151 | color: "white" 152 | font.pointSize: 8 153 | } 154 | } 155 | } 156 | 157 | Rectangle { 158 | width: 1 159 | height: parent.height 160 | border.color: "grey" 161 | color: "grey" 162 | } 163 | 164 | Item { 165 | id: r2 166 | height: cityCol.height 167 | 168 | Layout.fillWidth: true 169 | Column { 170 | id: cityCol 171 | anchors.centerIn: parent 172 | Text { 173 | id: cityText 174 | text: qsTr("City") 175 | anchors.horizontalCenter: parent.horizontalCenter 176 | color: "white" 177 | font.pointSize: 10 178 | } 179 | Text { 180 | id: cityLbl 181 | text: cityName 182 | anchors.horizontalCenter: parent.horizontalCenter 183 | color: "white" 184 | font.pointSize: 8 185 | } 186 | } 187 | } 188 | 189 | Rectangle { 190 | width: 1 191 | height: parent.height 192 | border.color: "grey" 193 | color: "grey" 194 | } 195 | 196 | Item { 197 | id: r3 198 | height: dateCol.height 199 | 200 | Layout.fillWidth: true 201 | Column { 202 | id: dateCol 203 | anchors.centerIn: parent 204 | Text { 205 | id: dateText 206 | text: qsTr("Date") 207 | anchors.horizontalCenter: parent.horizontalCenter 208 | color: "white" 209 | font.pointSize: 10 210 | } 211 | Text { 212 | id: dateLbl 213 | text: createDate 214 | anchors.horizontalCenter: parent.horizontalCenter 215 | color: "white" 216 | font.pointSize: 8 217 | } 218 | } 219 | } 220 | 221 | Rectangle { 222 | width: 1 223 | height: parent.height 224 | border.color: "grey" 225 | color: "grey" 226 | } 227 | 228 | Item { 229 | id: r4 230 | height: serviceCol.height 231 | 232 | Layout.fillWidth: true 233 | Column { 234 | id: serviceCol 235 | anchors.centerIn: parent 236 | Text { 237 | id: serviceText 238 | text: qsTr("Service") 239 | anchors.horizontalCenter: parent.horizontalCenter 240 | color: "white" 241 | font.pointSize: 10 242 | } 243 | Text { 244 | id: serviceLbl 245 | text: serviceName 246 | anchors.horizontalCenter: parent.horizontalCenter 247 | color: "white" 248 | font.pointSize: 8 249 | } 250 | } 251 | } 252 | } 253 | } 254 | 255 | MouseArea { 256 | id: detail 257 | anchors.fill: parent 258 | 259 | onClicked: { 260 | couponsModel.fetchDetail(id) 261 | stackView.push(detailSource, {detailsModel: couponsModel.detailsModel, couponsModel: couponsModel}) 262 | drawer.close() 263 | } 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /qml/Search.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.6 2 | import QtQuick.Controls 2.0 3 | 4 | Rectangle { 5 | id: searchPage 6 | //anchors.fill: parent 7 | 8 | property var parentItem: stackView.get(stackView.depth-2, StackView.DontLoad) 9 | property var categoriesModel 10 | property string searchCategoriesSource: "qrc:/SearchCategories.qml" 11 | 12 | property var loadingStatus: categoriesModel.loadingStatus 13 | 14 | property string titleText: qsTr("Search") 15 | 16 | property string categoryIdentifier 17 | property string categoryName 18 | 19 | function setCategory(catNm, categoryId) 20 | { 21 | categoryIdentifier = categoryId 22 | categoryName = catNm 23 | categoryBtn.text = catNm 24 | } 25 | 26 | Image { 27 | id: background 28 | source: "qrc:/images/bgvar1.png" 29 | fillMode: Image.Tile 30 | anchors.fill: parent 31 | } 32 | 33 | Column { 34 | anchors.centerIn: parent 35 | Button { 36 | id: categoryBtn 37 | text: qsTr("Select category...") 38 | onClicked: stackView.push(searchCategoriesSource, {categoriesModel: categoriesRestModel}) 39 | anchors.horizontalCenter: parent.horizontalCenter 40 | } 41 | 42 | Button { 43 | id: searchBtn 44 | text: qsTr("Search") 45 | anchors.horizontalCenter: parent.horizontalCenter 46 | onClicked: { 47 | parentItem.filter({'sourceServiceCategories': categoryIdentifier}) 48 | stackView.pop() 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /qml/SearchCategories.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.6 2 | import QtQuick.Controls 2.0 3 | 4 | 5 | Rectangle { 6 | //anchors.fill: parent 7 | 8 | property var parentItem: stackView.get(stackView.depth-2, StackView.DontLoad) 9 | property var categoriesModel 10 | property var loadingStatus: categoriesModel.loadingStatus 11 | 12 | property string titleText: qsTr("Search") 13 | 14 | Image { 15 | id: background 16 | source: "qrc:/images/bgvar1.png" 17 | fillMode: Image.Tile 18 | anchors.fill: parent 19 | } 20 | 21 | ListView { 22 | id: listView 23 | anchors.fill: parent 24 | 25 | model: categoriesModel 26 | 27 | delegate: ItemDelegate { 28 | width: parent.width 29 | text: model.categoryName 30 | highlighted: ListView.isCurrentItem 31 | onClicked: { 32 | parentItem.setCategory(model.categoryName, model.categoryIdentifier) 33 | stackView.pop() 34 | } 35 | } 36 | 37 | ScrollIndicator.vertical: ScrollIndicator { } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /qml/Statistics.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.6 2 | 3 | Item { 4 | //anchors.fill: parent 5 | 6 | property string titleText: qsTr("Statistics") 7 | 8 | Text { 9 | id: statistics 10 | text: "Statistics page" 11 | anchors.centerIn: parent 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /qml/includes/Log.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 Nikita Krupenko 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 5 | * associated documentation files (the "Software"), to deal in the Software without restriction, 6 | * including without limitation the rights to use, copy, modify, merge, publish, distribute, 7 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 8 | * furnished to do so, subject to the following conditions: 9 | * 10 | * The above copyright notice and this permission notice shall be included in all copies or 11 | * substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 14 | * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 15 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 16 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 17 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | */ 19 | 20 | .pragma library 21 | 22 | function serialize(object, maxDepth) { 23 | function _processObject(object, maxDepth, level) { 24 | var output = Array() 25 | var pad = " " 26 | if (maxDepth == undefined) { 27 | maxDepth = -1 28 | } 29 | if (level == undefined) { 30 | level = 0 31 | } 32 | var padding = Array(level + 1).join(pad) 33 | 34 | output.push((Array.isArray(object) ? "[" : "{")) 35 | var fields = Array() 36 | for (var key in object) { 37 | var keyText = Array.isArray(object) ? "" : ("\"" + key + "\": ") 38 | if (typeof (object[key]) == "object" && key != "parent" && maxDepth != 0) { 39 | var res = _processObject(object[key], maxDepth > 0 ? maxDepth - 1 : -1, level + 1) 40 | fields.push(padding + pad + keyText + res) 41 | } else { 42 | fields.push(padding + pad + keyText + "\"" + object[key] + "\"") 43 | } 44 | } 45 | output.push(fields.join(",\n")) 46 | output.push(padding + (Array.isArray(object) ? "]" : "}")) 47 | 48 | return output.join("\n") 49 | } 50 | 51 | return _processObject(object, maxDepth) 52 | } 53 | -------------------------------------------------------------------------------- /qml/includes/Utils.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.4 2 | import QtQuick.Window 2.2 3 | 4 | Item { 5 | property int defaultWidth: 800 6 | property int defaultHeight: 480 7 | readonly property double scaleFactorPercent: Math.min(Math.min(root.width, root.height) / defaultHeight, Math.max(root.width, root.height) / defaultWidth); 8 | readonly property int scaleFactor: Screen.logicalPixelDensity; 9 | 10 | function dp(val) { return Math.floor(val ) } //millimeters 11 | function mm(val) { return Math.floor(val) } //millimeters 12 | function pt(val) { return Math.floor(val) } //points (dp) 13 | function pct(val) { return Math.floor(val) } //percents 14 | 15 | function getRandomInt(min, max) { 16 | return Math.floor(Math.random() * (max - min)) + min; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /qml/main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.5 2 | import QtQuick.Dialogs 1.2 3 | import QtQuick.Layouts 1.3 4 | 5 | import QtQuick.Controls 2.0 6 | import QtQuick.Controls.Universal 2.0 7 | import QtQuick.Controls.Material 2.0 8 | import Qt.labs.settings 1.0 9 | 10 | import com.github.qtrestexample.skidkzapi 1.0 11 | import com.github.qtrest.jsonrestlistmodel 1.0 12 | import com.github.qtrest.pagination 1.0 13 | import com.github.qtrest.requests 1.0 14 | 15 | import "includes" as I 16 | 17 | ApplicationWindow { 18 | id: root 19 | visible: true 20 | width: 480 21 | height: 800 22 | 23 | title: qsTr("Skid.KZ") 24 | 25 | property string searchSource: "qrc:/Search.qml" 26 | 27 | SkidKZApi { 28 | id: skidKZApi 29 | 30 | baseUrl: "http://api.skid.kz" 31 | 32 | authTokenHeader: "Authorization" 33 | authToken: "Bearer 8aef452ee3b32466209535b96d456b06" 34 | 35 | Component.onCompleted: console.log("completed!"); 36 | } 37 | 38 | JsonRestListModel { 39 | id: categoriesRestModel 40 | api: skidKZApi 41 | 42 | idField: 'id' 43 | 44 | requests { 45 | get: "/v1/categories" 46 | } 47 | 48 | sort: ['categoryName'] 49 | 50 | pagination { 51 | policy: Pagination.PageNumber 52 | perPage: 20 53 | currentPageHeader: "X-Pagination-Current-Page" 54 | totalCountHeader: "X-Pagination-Total-Count" 55 | pageCountHeader: "X-Pagination-Page-Count" 56 | } 57 | 58 | Component.onCompleted: { console.log(pagination.perPage); reload(); } 59 | } 60 | 61 | Settings { 62 | id: settings 63 | property string style: "Material" 64 | property int spacing: utils.mm(1) 65 | property int bottomPadding: utils.mm(2) 66 | property int topPadding: utils.mm(2) 67 | property int busyIndicatorSize: 40 68 | } 69 | 70 | header: ToolBar { 71 | RowLayout { 72 | spacing: 0 73 | anchors.fill: parent 74 | 75 | ToolButton { 76 | id: menuBtn 77 | indicator: Image { 78 | anchors.centerIn: parent 79 | source: stackView.depth > 1 ? awesome.iconLink( "chevronleft", {}, "mdpi" ) 80 | : awesome.iconLink( "bars", {}, "mdpi" ) 81 | } 82 | onClicked: { 83 | if (stackView.depth > 1) { 84 | stackView.pop() 85 | } else { 86 | drawer.open() 87 | } 88 | } 89 | } 90 | 91 | Label { 92 | id: titleLabel 93 | text: "Skid.KZ - " + stackView.currentItem.titleText 94 | font.pixelSize: 20 95 | elide: Label.ElideRight 96 | horizontalAlignment: Qt.AlignHCenter 97 | verticalAlignment: Qt.AlignVCenter 98 | Layout.fillWidth: true 99 | } 100 | 101 | Item { 102 | width: menuBtn.width 103 | height: width 104 | 105 | ToolButton { 106 | id: searchBtn 107 | visible: stackView.depth == 1 108 | indicator: Image { 109 | anchors.centerIn: parent 110 | source: awesome.iconLink( "search", {}, "mdpi" ) 111 | } 112 | onClicked: { 113 | if (stackView.depth > 1) { 114 | stackView.pop() 115 | } else { 116 | stackView.push(searchSource, {categoriesModel: categoriesRestModel}) 117 | } 118 | } 119 | } 120 | 121 | Item { 122 | visible: stackView.depth > 1 123 | width: menuBtn.width 124 | height: width 125 | } 126 | } 127 | } 128 | } 129 | 130 | I.Utils { id: utils } 131 | 132 | Image { 133 | id: background 134 | source: "qrc:/images/bgvar1.png" 135 | fillMode: Image.Tile 136 | anchors.fill: parent 137 | } 138 | 139 | Drawer { 140 | id: drawer 141 | 142 | width: 0.66 * root.width 143 | height: root.height 144 | 145 | Pane { 146 | padding: 0 147 | anchors.fill: parent 148 | 149 | MouseArea { 150 | anchors.fill: parent 151 | onClicked: drawer.close() 152 | } 153 | 154 | ListView { 155 | id: listView 156 | currentIndex: -1 157 | anchors.fill: parent 158 | 159 | delegate: ItemDelegate { 160 | width: parent.width 161 | text: model.title 162 | highlighted: ListView.isCurrentItem 163 | anchors.horizontalCenter: parent.horizontalCenter 164 | onClicked: { 165 | if (listView.currentIndex != index) { 166 | listView.currentIndex = index 167 | stackView.replace(model.source) 168 | } 169 | drawer.close() 170 | } 171 | } 172 | 173 | model: ListModel { 174 | ListElement { title: qsTr("Actual"); source: "qrc:/ActualCouponsList.qml" } 175 | ListElement { title: qsTr("Archive"); source: "qrc:/ArchiveCouponsList.qml" } 176 | ListElement { title: qsTr("Statistics"); source: "qrc:/Statistics.qml" } 177 | ListElement { title: qsTr("About"); source: "qrc:/About.qml" } 178 | } 179 | 180 | ScrollIndicator.vertical: ScrollIndicator { } 181 | } 182 | } 183 | //Component.onCompleted: drawer.close() 184 | } 185 | 186 | StackView { 187 | id: stackView 188 | anchors.fill: parent 189 | 190 | initialItem: ActualCouponsList { 191 | //anchors.fill: parent 192 | } 193 | 194 | pushEnter: Transition { 195 | XAnimator { from: root.width; to: 0; duration: 200; easing.type: Easing.OutCubic } 196 | } 197 | 198 | pushExit: Transition { 199 | XAnimator { from: 0; to: -root.width; duration: 200; easing.type: Easing.OutCubic } 200 | } 201 | 202 | popEnter: Transition { 203 | XAnimator { from: -root.width; to: 0; duration: 200; easing.type: Easing.OutCubic } 204 | } 205 | 206 | popExit: Transition { 207 | XAnimator { from: 0; to: root.width; duration: 200; easing.type: Easing.OutCubic } 208 | } 209 | 210 | replaceEnter: Transition { 211 | XAnimator { from: root.width; to: 0; duration: 200; easing.type: Easing.OutCubic } 212 | } 213 | 214 | replaceExit: Transition { 215 | XAnimator { from: 0; to: -root.width; duration: 200; easing.type: Easing.OutCubic } 216 | } 217 | 218 | // transform: Translate { 219 | // x: drawer.position * stackView.width * 0.33 220 | // } 221 | } 222 | 223 | onClosing: { 224 | if (stackView.depth > 1) { 225 | stackView.pop() 226 | close.accepted = false 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /qml/qml.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | main.qml 4 | CouponsList.qml 5 | CouponsListDelegate.qml 6 | includes/Utils.qml 7 | About.qml 8 | Statistics.qml 9 | ActualCouponsList.qml 10 | ArchiveCouponsList.qml 11 | CouponDetail.qml 12 | CouponsDetailDelegate.qml 13 | Search.qml 14 | SearchCategories.qml 15 | includes/Log.js 16 | 17 | 18 | -------------------------------------------------------------------------------- /qtrest-example.pro: -------------------------------------------------------------------------------- 1 | TEMPLATE = app 2 | 3 | QT += quick 4 | QT += widgets 5 | 6 | CONFIG += c++11 7 | 8 | 9 | SOURCES += main.cpp \ 10 | api/models/couponmodel.cpp \ 11 | api/skidkzapi.cpp 12 | 13 | HEADERS += \ 14 | api/models/couponmodel.h \ 15 | api/skidkzapi.h 16 | 17 | RESOURCES += qml/qml.qrc \ 18 | assets/assets.qrc 19 | 20 | # Additional import path used to resolve QML modules in Qt Creator's code model 21 | QML_IMPORT_PATH = 22 | 23 | #Translations 24 | lupdate_only { 25 | SOURCES += $$PWD/qml/*.qml \ 26 | $$PWD/qml/activities/*.qml \ 27 | $$PWD/qml/includes/*.qml \ 28 | $$PWD/qml/models/*.qml 29 | } 30 | 31 | TRANSLATIONS += assets/i18n/skidkz_ru.ts 32 | 33 | # Default rules for deployment. 34 | include(deployment.pri) 35 | 36 | #Font Awesome 37 | include(3rd/QtAwesome/QtAwesome/QtAwesome.pri) 38 | 39 | #AdCtl: Google Analytics, AdMob, StartAD.mobi, Qt-REST 40 | include(3rd/qtrest/com_github_kafeg_qtrest.pri) 41 | 42 | DISTFILES += \ 43 | QtMicroRestFramework.qmodel 44 | 45 | #INCLUDEPATH += $$PWD/vendor/com/github/kafeg/qtrest/src/ \ 46 | # $$PWD/vendor/com/github/kafeg/qtrest/src/models/ 47 | --------------------------------------------------------------------------------