diff --git a/src/app/HotelDelegate.qml b/src/app/HotelDelegate.qml index 580cb6f..8192855 100644 --- a/src/app/HotelDelegate.qml +++ b/src/app/HotelDelegate.qml @@ -1,71 +1,75 @@ /* Copyright (C) 2018 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ import QtQuick 2.5 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.1 as QQC2 import org.kde.kirigami 2.4 as Kirigami import org.kde.itinerary 1.0 import "." as App App.TimelineDelegate { id: root header: Rectangle { id: headerBackground Kirigami.Theme.colorSet: Kirigami.Theme.Complementary Kirigami.Theme.inherit: false color: Kirigami.Theme.backgroundColor implicitHeight: headerLayout.implicitHeight + Kirigami.Units.largeSpacing * 2 anchors.leftMargin: -root.leftPadding anchors.topMargin: -root.topPadding anchors.rightMargin: -root.rightPadding RowLayout { id: headerLayout anchors.fill: parent anchors.margins: Kirigami.Units.largeSpacing QQC2.Label { - text: qsTr("🏨 %1").arg(reservation.reservationFor.name) + text: root.rangeType == TimelineModel.RangeEnd ? + qsTr("🏨 Check-out %1").arg(reservation.reservationFor.name) : + qsTr("🏨 %1").arg(reservation.reservationFor.name) color: Kirigami.Theme.textColor font.pointSize: Kirigami.Theme.defaultFont.pointSize * root.headerFontScale Layout.fillWidth: true } } } contentItem: ColumnLayout { id: topLayout App.PlaceDelegate { place: reservation.reservationFor Layout.fillWidth: true } QQC2.Label { text: qsTr("Check-in time: %1") .arg(Localizer.formatTime(reservation, "checkinTime")) color: Kirigami.Theme.textColor + visible: root.rangeType == TimelineModel.RangeBegin } QQC2.Label { - text: qsTr("Check-out time: %1") - .arg(Localizer.formatDateTime(reservation, "checkoutTime")) + text: root.rangeType == TimelineModel.RangeBegin ? + qsTr("Check-out time: %1").arg(Localizer.formatDateTime(reservation, "checkoutTime")) : + qsTr("Check-out time: %1").arg(Localizer.formatTime(reservation, "checkoutTime")) color: Kirigami.Theme.textColor } } } diff --git a/src/app/TimelineDelegate.qml b/src/app/TimelineDelegate.qml index 32c7480..dd515a9 100644 --- a/src/app/TimelineDelegate.qml +++ b/src/app/TimelineDelegate.qml @@ -1,57 +1,58 @@ /* Copyright (C) 2018 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ import QtQuick 2.0 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.1 as QQC2 import org.kde.kirigami 2.4 as Kirigami import org.kde.itinerary 1.0 import "." as App Kirigami.AbstractCard { id: root property var reservation property var pass property string passId + property var rangeType readonly property double headerFontScale: 1.25 function showBoardingPass() { applicationWindow().pageStack.push(pkpassComponent); } function showTicket() { applicationWindow().pageStack.push(ticketPageComponent); } Component { id: pkpassComponent App.PkPassPage { passId: root.passId pass: root.pass } } Component { id: ticketPageComponent App.TicketPage { reservation: root.reservation } } } diff --git a/src/app/TimelinePage.qml b/src/app/TimelinePage.qml index a68c5c9..c511530 100644 --- a/src/app/TimelinePage.qml +++ b/src/app/TimelinePage.qml @@ -1,122 +1,143 @@ /* Copyright (C) 2018 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ import QtQuick 2.5 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.1 as QQC2 import org.kde.kirigami 2.4 as Kirigami import org.kde.itinerary 1.0 import "." as App Kirigami.ScrollablePage { id: root title: qsTr("My Itinerary") // context drawer content actions { contextualActions: [ Kirigami.Action { text: qsTr("Today") iconName: "view-calendar-day" onTriggered: listView.positionViewAtIndex(_timelineModel.todayRow, ListView.Beginning); } ] } // page content Component { id: flightDelegate - App.FlightDelegate {} + App.FlightDelegate { + reservation: modelData.reservation + passId: modelData.passId + pass: modelData.pass + rangeType: modelData.rangeType + } } Component { id: hotelDelegate - App.HotelDelegate {} + App.HotelDelegate { + reservation: modelData.reservation + passId: modelData.passId + pass: modelData.pass + rangeType: modelData.rangeType + } } Component { id: trainDelegate - App.TrainDelegate {} + App.TrainDelegate { + reservation: modelData.reservation + passId: modelData.passId + pass: modelData.pass + rangeType: modelData.rangeType + } } Component { id: busDelegate - App.BusDelegate {} + App.BusDelegate { + reservation: modelData.reservation + passId: modelData.passId + pass: modelData.pass + rangeType: modelData.rangeType + } } Component { id: restaurantDelegate - App.RestaurantDelegate {} + App.RestaurantDelegate { + reservation: modelData.reservation + passId: modelData.passId + pass: modelData.pass + rangeType: modelData.rangeType + } } Component { id: todayDelegate Item { implicitHeight: visible ? label.implicitHeight : 0 QQC2.Label { id: label anchors.fill: parent text: qsTr("Nothing on the itinerary for today."); color: Kirigami.Theme.textColor horizontalAlignment: Qt.AlignHCenter } } } Kirigami.CardsListView { id: listView model: _timelineModel delegate: Loader { property var modelData: model height: item ? item.implicitHeight : 0 sourceComponent: { if (!modelData) return; switch (modelData.type) { case TimelineModel.Flight: return flightDelegate; case TimelineModel.Hotel: return hotelDelegate; case TimelineModel.TrainTrip: return trainDelegate; case TimelineModel.BusTrip: return busDelegate; case TimelineModel.Restaurant: return restaurantDelegate; case TimelineModel.TodayMarker: return todayDelegate; } } onLoaded: { - if (modelData.type != TimelineModel.TodayMarker) { - item.reservation = Qt.binding(function() { return modelData.reservation; }); - item.passId = Qt.binding(function() { return modelData.passId; }); - item.pass = Qt.binding(function() { return modelData.pass; }); - } else { + if (modelData.type == TimelineModel.TodayMarker) { item.visible = modelData.isTodayEmpty; } } } section.property: "sectionHeader" section.delegate: Item { implicitHeight: headerItem.implicitHeight + Kirigami.Units.largeSpacing*2 implicitWidth: ListView.view.width Kirigami.BasicListItem { id: headerItem label: section backgroundColor: Kirigami.Theme.backgroundColor icon: "view-calendar-day" } } section.criteria: ViewSection.FullString section.labelPositioning: ViewSection.CurrentLabelAtStart | ViewSection.InlineLabels } Component.onCompleted: listView.positionViewAtIndex(_timelineModel.todayRow, ListView.Beginning); } diff --git a/src/app/timelinemodel.cpp b/src/app/timelinemodel.cpp index 69ca8f9..96013d6 100644 --- a/src/app/timelinemodel.cpp +++ b/src/app/timelinemodel.cpp @@ -1,269 +1,271 @@ /* Copyright (C) 2018 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "timelinemodel.h" #include "pkpassmanager.h" #include "reservationmanager.h" #include #include #include #include #include #include #include #include #include #include #include using namespace KItinerary; static bool needsSplitting(const QVariant &res) { return res.userType() == qMetaTypeId(); } static QDateTime relevantDateTime(const QVariant &res, TimelineModel::RangeType range) { if (res.isNull()) { // today marker return QDateTime(QDate::currentDate(), QTime(0, 0)); } else if (res.userType() == qMetaTypeId()) { const auto flight = res.value().reservationFor().value(); if (flight.boardingTime().isValid()) { return flight.boardingTime(); } if (flight.departureTime().isValid()) { return flight.departureTime(); } return QDateTime(flight.departureDay(), QTime(23, 59, 59)); } else if (res.userType() == qMetaTypeId()) { return res.value().reservationFor().value().departureTime(); } else if (res.userType() == qMetaTypeId()) { return res.value().reservationFor().value().departureTime(); } else if (res.userType() == qMetaTypeId()) { return res.value().startTime(); } else if (res.userType() == qMetaTypeId()) { const auto hotel = res.value(); // hotel checkin/checkout is always considered the first/last thing of the day if (range == TimelineModel::RangeEnd) { return QDateTime(hotel.checkoutTime().date(), QTime(0, 0, 0)); } else { return QDateTime(hotel.checkinTime().date(), QTime(23, 59, 59)); } } return {}; } static QString passId(const QVariant &res) { const auto passTypeId = JsonLdDocument::readProperty(res, "pkpassPassTypeIdentifier").toString(); const auto serialNum = JsonLdDocument::readProperty(res, "pkpassSerialNumber").toString(); if (passTypeId.isEmpty() || serialNum.isEmpty()) return {}; return passTypeId + QLatin1Char('/') + QString::fromUtf8(serialNum.toUtf8().toBase64(QByteArray::Base64UrlEncoding)); } TimelineModel::TimelineModel(QObject *parent) : QAbstractListModel(parent) { } TimelineModel::~TimelineModel() = default; void TimelineModel::setPkPassManager(PkPassManager* mgr) { m_passMgr = mgr; } void TimelineModel::setReservationManager(ReservationManager* mgr) { beginResetModel(); m_resMgr = mgr; for (const auto &resId : mgr->reservations()) { const auto res = m_resMgr->reservation(resId); if (needsSplitting(res)) { m_elements.push_back(Element{resId, relevantDateTime(mgr->reservation(resId), RangeBegin), RangeBegin}); m_elements.push_back(Element{resId, relevantDateTime(mgr->reservation(resId), RangeEnd), RangeEnd}); } else { m_elements.push_back(Element{resId, relevantDateTime(mgr->reservation(resId), SelfContained), SelfContained}); } } m_elements.push_back(Element{{}, relevantDateTime({}, SelfContained), SelfContained}); // today marker std::sort(m_elements.begin(), m_elements.end(), [this](const Element &lhs, const Element &rhs) { return lhs.dt < rhs.dt; }); connect(mgr, &ReservationManager::reservationAdded, this, &TimelineModel::reservationAdded); connect(mgr, &ReservationManager::reservationUpdated, this, &TimelineModel::reservationUpdated); connect(mgr, &ReservationManager::reservationRemoved, this, &TimelineModel::reservationRemoved); endResetModel(); emit todayRowChanged(); } int TimelineModel::rowCount(const QModelIndex& parent) const { if (parent.isValid() || !m_resMgr) return 0; return m_elements.size(); } QVariant TimelineModel::data(const QModelIndex& index, int role) const { if (!index.isValid() || !m_resMgr) return {}; const auto &elem = m_elements.at(index.row()); const auto res = m_resMgr->reservation(elem.id); switch (role) { case PassRole: return QVariant::fromValue(m_passMgr->pass(passId(res))); case PassIdRole: return passId(res); case SectionHeader: { if (elem.dt.isNull()) return {}; if (elem.dt.date() == QDate::currentDate()) return i18n("Today"); return i18nc("weekday, date", "%1, %2", QLocale().dayName(elem.dt.date().dayOfWeek(), QLocale::LongFormat), QLocale().toString(elem.dt.date(), QLocale::ShortFormat)); } case ReservationRole: return res; case ReservationIdRole: return elem.id; case ElementTypeRole: if (res.isNull()) return TodayMarker; else if (res.userType() == qMetaTypeId()) return Flight; else if (res.userType() == qMetaTypeId()) return Hotel; else if (res.userType() == qMetaTypeId()) return TrainTrip; else if (res.userType() == qMetaTypeId()) return BusTrip; else if (res.userType() == qMetaTypeId()) return Restaurant; return {}; case TodayEmptyRole: if (res.isNull()) { return index.row() == (int)(m_elements.size() - 1) || m_elements.at(index.row() + 1).dt.date() > QDate::currentDate(); } return {}; case IsTodayRole: return elem.dt.date() == QDate::currentDate(); case ElementRangeRole: return elem.rangeType; } return {}; } QHash TimelineModel::roleNames() const { auto names = QAbstractListModel::roleNames(); names.insert(PassRole, "pass"); names.insert(PassIdRole, "passId"); names.insert(SectionHeader, "sectionHeader"); names.insert(ReservationRole, "reservation"); + names.insert(ReservationIdRole, "reservationId"); names.insert(ElementTypeRole, "type"); names.insert(TodayEmptyRole, "isTodayEmpty"); names.insert(IsTodayRole, "isToday"); + names.insert(ElementRangeRole, "rangeType"); return names; } int TimelineModel::todayRow() const { const auto it = std::find_if(m_elements.begin(), m_elements.end(), [](const Element &e) { return e.id.isEmpty(); }); return std::distance(m_elements.begin(), it); } void TimelineModel::reservationAdded(const QString &resId) { const auto res = m_resMgr->reservation(resId); if (needsSplitting(res)) { insertElement(Element{resId, relevantDateTime(res, RangeBegin), RangeBegin}); insertElement(Element{resId, relevantDateTime(res, RangeEnd), RangeEnd}); } else { insertElement(Element{resId, relevantDateTime(res, SelfContained), SelfContained}); } emit todayRowChanged(); } void TimelineModel::insertElement(Element &&elem) { auto it = std::lower_bound(m_elements.begin(), m_elements.end(), elem.dt, [](const Element &lhs, const QDateTime &rhs) { return lhs.dt < rhs; }); auto index = std::distance(m_elements.begin(), it); beginInsertRows({}, index, index); m_elements.insert(it, std::move(elem)); endInsertRows(); } void TimelineModel::reservationUpdated(const QString &resId) { const auto res = m_resMgr->reservation(resId); if (needsSplitting(res)) { updateElement(resId, res, RangeBegin); updateElement(resId, res, RangeEnd); } else { updateElement(resId, res, SelfContained); } } void TimelineModel::updateElement(const QString &resId, const QVariant &res, TimelineModel::RangeType rangeType) { const auto it = std::find_if(m_elements.begin(), m_elements.end(), [resId, rangeType](const Element &e) { return e.id == resId && e.rangeType == rangeType; }); if (it == m_elements.end()) { return; } const auto row = std::distance(m_elements.begin(), it); const auto newDt = relevantDateTime(res, rangeType); if ((*it).dt != newDt) { // element moved beginRemoveRows({}, row, row); m_elements.erase(it); endRemoveRows(); insertElement(Element{resId, newDt, rangeType}); } else { emit dataChanged(index(row, 0), index(row, 0)); } } void TimelineModel::reservationRemoved(const QString &resId) { const auto it = std::find_if(m_elements.begin(), m_elements.end(), [resId](const Element &e) { return e.id == resId; }); if (it == m_elements.end()) { return; } const auto isSplit = (*it).rangeType == RangeBegin; const auto row = std::distance(m_elements.begin(), it); beginRemoveRows({}, row, row); m_elements.erase(it); endRemoveRows(); emit todayRowChanged(); if (isSplit) { reservationRemoved(resId); } }