Warning, /pim/itinerary/src/app/TimelinePage.qml is written in an unsupported language. File is not indexed.

0001 /*
0002     SPDX-FileCopyrightText: 2018 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 import QtCore
0008 import QtQuick
0009 import QtQuick.Layouts
0010 import QtQuick.Controls as QQC2
0011 import Qt.labs.qmlmodels as Models
0012 import QtQuick.Dialogs
0013 import org.kde.kirigami as Kirigami
0014 import org.kde.itinerary
0015 
0016 Kirigami.ScrollablePage {
0017     id: root
0018     title: i18n("My Itinerary")
0019     onBackRequested: event => { event.accepted = true; }
0020     Kirigami.Theme.colorSet: Kirigami.Theme.Window
0021 
0022     leftPadding: 0
0023     rightPadding: 0
0024 
0025     /** Model index somewhat at the center of the currently display timeline. */
0026     function currentIndex() {
0027         let row = -1;
0028         for (let i = listView.contentY + listView.height * 0.8; row == -1 && i > listView.contentY; i -= 10) {
0029             row = listView.indexAt(0, i);
0030         }
0031         return listView.model.index(row, 0);
0032     }
0033 
0034     /** Date/time the given model index refers to, depending on the type of the element this refers to. */
0035     function dateTimeAtIndex(idx) {
0036         if (listView.model.data(idx, TimelineModel.IsTimeboxedRole) && !listView.model.data(idx, TimelineModel.IsCanceledRole)) {
0037             return listView.model.data(idx, TimelineModel.EndDateTimeRole);
0038         }
0039         return listView.model.data(idx, TimelineModel.StartDateTimeRole);
0040     }
0041 
0042     function addTrainTrip() {
0043         // find date/time at the current screen center
0044         const idx = currentIndex();
0045 
0046         const HOUR = 60 * 60 * 1000;
0047         var roundInterval = HOUR;
0048         var dt;
0049         if (listView.model.data(idx, TimelineModel.IsTimeboxedRole) && !listView.model.data(idx, TimelineModel.IsCanceledRole)) {
0050             dt = listView.model.data(idx, TimelineModel.EndDateTimeRole);
0051             roundInterval = 5 * 60 * 1000;
0052         } else {
0053             dt = listView.model.data(idx, TimelineModel.StartDateTimeRole);
0054         }
0055 
0056         // clamp to future times and round to the next plausible hour
0057         const now = new Date();
0058         if (!dt || dt.getTime() < now.getTime()) {
0059             dt = now;
0060         }
0061         if (dt.getTime() % HOUR == 0 && dt.getHours() == 0) {
0062             dt.setTime(dt.getTime() + HOUR * 8);
0063         } else {
0064             dt.setTime(dt.getTime() + roundInterval - (dt.getTime() % roundInterval));
0065         }
0066 
0067         // determine where we are at that time
0068         const place = TimelineModel.locationAtTime(dt);
0069         var country = Settings.homeCountryIsoCode;
0070         var departureLocation;
0071         if (place) {
0072             country = place.address.addressCountry;
0073             departureLocation = PublicTransport.locationFromPlace(place, undefined);
0074             departureLocation.name = place.name;
0075         }
0076 
0077         pageStack.clear()
0078         pageStack.push(Qt.resolvedUrl("JourneyRequestPage.qml"), {
0079             publicTransportManager: LiveDataManager.publicTransportManager,
0080             initialCountry: country,
0081             initialDateTime: dt,
0082             departureStop: departureLocation
0083         });
0084     }
0085     // context drawer content
0086     actions: [
0087         Kirigami.Action {
0088             text: i18n("Go To Now")
0089             icon.name: "view-calendar-day"
0090             onTriggered: listView.positionViewAtIndex(TripGroupProxyModel.todayRow, ListView.Beginning);
0091         },
0092         Kirigami.Action {
0093             text: i18n("Current Ticket")
0094             icon.name: "view-barcode-qr"
0095             enabled: TimelineModel.currentBatchId !== ""
0096             onTriggered: showDetailsPageForReservation(TimelineModel.currentBatchId)
0097         },
0098         Kirigami.Action {
0099             text: i18n("Add train trip...")
0100             icon.name: "list-add-symbolic"
0101             onTriggered: {
0102                 addTrainTrip()
0103             }
0104         },
0105         Kirigami.Action {
0106             text: i18n("Add ferry trip...")
0107             icon.name: "qrc:///images/ferry.svg"
0108             onTriggered: {
0109                 const dt = dateTimeAtIndex(currentIndex());
0110                 let res =  Factory.makeBoatReservation();
0111                 let trip = res.reservationFor;
0112                 trip.departureTime = new Date(dt.getFullYear(), dt.getMonth(), dt.getDate(), dt.getHours() == 0 ? 8 : dt.getHours() + 1, 0);
0113                 res.reservationFor = trip;
0114                 applicationWindow().pageStack.push(boatEditorPage, {reservation: res});
0115             }
0116         },
0117         Kirigami.Action {
0118             text: i18n("Add accommodation...")
0119             icon.name: "go-home-symbolic"
0120             onTriggered: {
0121                 const dt = dateTimeAtIndex(currentIndex());
0122                 let res =  Factory.makeLodgingReservation();
0123                 res.checkinTime = new Date(dt.getFullYear(), dt.getMonth(), dt.getDate(), 15, 0);
0124                 res.checkoutTime = new Date(dt.getFullYear(), dt.getMonth(), dt.getDate() + 1, 11, 0);
0125                 applicationWindow().pageStack.push(hotelEditorPage, {reservation: res});
0126             }
0127         },
0128         Kirigami.Action {
0129             text: i18n("Add event...")
0130             icon.name: "meeting-attending"
0131             onTriggered: {
0132                 const dt = dateTimeAtIndex(currentIndex());
0133                 let res = Factory.makeEventReservation();
0134                 let ev = res.reservationFor;
0135                 ev.startDate = new Date(dt.getFullYear(), dt.getMonth(), dt.getDate(), dt.getHours() == 0 ? 8 : dt.getHours() + 1, 0);
0136                 res.reservationFor = ev;
0137                 applicationWindow().pageStack.push(eventEditorPage, {reservation: res});
0138             }
0139         },
0140         Kirigami.Action {
0141             text: i18n("Add restaurant...")
0142             icon.name: "qrc:///images/foodestablishment.svg"
0143             onTriggered: {
0144                 const dt = dateTimeAtIndex(currentIndex());
0145                 let res =  Factory.makeFoodEstablishmentReservation();
0146                 res.startTime = new Date(dt.getFullYear(), dt.getMonth(), dt.getDate(), 20, 0);
0147                 res.endTime = new Date(dt.getFullYear(), dt.getMonth(), dt.getDate(), 22, 0);
0148                 applicationWindow().pageStack.push(restaurantEditorPage, {reservation: res});
0149             }
0150         }
0151     ]
0152 
0153     // page content
0154     Kirigami.PromptDialog {
0155         id: deleteTripGroupWarningDialog
0156         property string tripGroupId
0157 
0158         title: i18n("Delete Trip")
0159         subtitle: i18n("Do you really want to delete this trip?")
0160 
0161         standardButtons: QQC2.Dialog.Cancel
0162 
0163         customFooterActions: [
0164             Kirigami.Action {
0165                 text: i18n("Delete")
0166                 icon.name: "edit-delete"
0167                 onTriggered: {
0168                     TripGroupManager.removeReservationsInGroup(deleteTripGroupWarningDialog.tripGroupId);
0169                     deleteTripGroupWarningDialog.close();
0170                 }
0171             }
0172         ]
0173     }
0174 
0175     Kirigami.MenuDialog {
0176         id: exportTripGroupDialog
0177         property string tripGroupId
0178         title: i18n("Export")
0179         property list<QQC2.Action> _actions: [
0180             Kirigami.Action {
0181                 text: i18n("As Itinerary file...")
0182                 icon.name: "export-symbolic"
0183                 onTriggered: {
0184                     tripGroupFileExportDialog.tripGroupId = exportTripGroupDialog.tripGroupId;
0185                     tripGroupFileExportDialog.open();
0186                 }
0187             },
0188             Kirigami.Action {
0189                 text: i18n("As GPX file...")
0190                 icon.name: "map-globe"
0191                 onTriggered: {
0192                     tripGroupGpxExportDialog.tripGroupId = exportTripGroupDialog.tripGroupId;
0193                     tripGroupGpxExportDialog.open();
0194                 }
0195             }
0196         ]
0197         actions: exportTripGroupDialog._actions
0198         Instantiator {
0199             model: KDEConnectDeviceModel {
0200                 id: deviceModel
0201             }
0202             delegate: Kirigami.Action {
0203                 text: i18n("Send to %1", model.name)
0204                 icon.name: "kdeconnect-tray"
0205                 onTriggered: ApplicationController.exportTripToKDEConnect(exportTripGroupDialog.tripGroupId, model.deviceId)
0206             }
0207             onObjectAdded: exportTripGroupDialog._actions.push(object)
0208         }
0209         onVisibleChanged: {
0210             if (exportTripGroupDialog.visible)
0211                 deviceModel.refresh();
0212         }
0213     }
0214     FileDialog {
0215         id: tripGroupFileExportDialog
0216         property string tripGroupId
0217         fileMode: FileDialog.SaveFile
0218         title: i18n("Export Trip")
0219         currentFolder: StandardPaths.writableLocation(StandardPaths.DocumentsLocation)
0220         nameFilters: [i18n("Itinerary file (*.itinerary)")]
0221         onAccepted: ApplicationController.exportTripToFile(tripGroupId, selectedFile)
0222     }
0223     FileDialog {
0224         id: tripGroupGpxExportDialog
0225         property string tripGroupId
0226         fileMode: FileDialog.SaveFile
0227         title: i18n("Export Trip")
0228         currentFolder: StandardPaths.writableLocation(StandardPaths.DocumentsLocation)
0229         nameFilters: [i18n("GPX Files (*.gpx)")]
0230         onAccepted: ApplicationController.exportTripToGpx(tripGroupId, selectedFile)
0231     }
0232 
0233     Component {
0234         id: flightDetailsPage
0235         FlightPage {}
0236     }
0237     Component {
0238         id: trainDetailsPage
0239         TrainPage {}
0240     }
0241     Component {
0242         id: busDetailsPage
0243         BusPage {}
0244     }
0245     Component {
0246         id: hotelDetailsPage
0247         HotelPage { editor: hotelEditorPage }
0248     }
0249     Component {
0250         id: eventDetailsPage
0251         EventPage { editor: eventEditorPage }
0252     }
0253     Component {
0254         id: restaurantDetailsPage
0255         RestaurantPage { editor: restaurantEditorPage }
0256     }
0257     Component {
0258         id: carRentalDetailsPage
0259         CarRentalPage {}
0260     }
0261     Component {
0262         id: boatDetailsPage
0263         BoatPage { editor: boatEditorPage }
0264     }
0265     Component {
0266         id: touristAttractionDetailsPage
0267         TouristAttractionPage {}
0268     }
0269     Component {
0270         id: weatherForecastPage
0271         WeatherForecastPage {}
0272     }
0273 
0274     Component {
0275         id: boatEditorPage
0276         BoatEditor {}
0277     }
0278     Component {
0279         id: hotelEditorPage
0280         HotelEditor {}
0281     }
0282     Component {
0283         id: eventEditorPage
0284         EventEditor {}
0285     }
0286     Component {
0287         id: restaurantEditorPage
0288         RestaurantEditor {}
0289     }
0290 
0291     function detailsComponent(batchId) {
0292         const res = ReservationManager.reservation(batchId);
0293         if (!res) {
0294             return undefined;
0295         }
0296         switch (res.className) {
0297             case "FlightReservation": return flightDetailsPage;
0298             case "TrainReservation": return trainDetailsPage;
0299             case "BusReservation": return busDetailsPage;
0300             case "LodgingReservation": return hotelDetailsPage;
0301             case "EventReservation": return eventDetailsPage;
0302             case "FoodEstablishmentReservation": return restaurantDetailsPage;
0303             case "RentalCarReservation": return carRentalDetailsPage;
0304             case "TouristAttractionVisit": return touristAttractionDetailsPage;
0305         }
0306         console.log("unhandled reservation type:", res.className);
0307         return undefined;
0308     }
0309 
0310     function showDetailsPageForReservation(batchId) {
0311         const c = detailsComponent(batchId);
0312         if (c) {
0313             showDetailsPage(c, batchId);
0314         }
0315     }
0316 
0317     function showDetailsPage(detailsComponent, batchId) {
0318         while (applicationWindow().pageStack.depth > 1) {
0319             applicationWindow().pageStack.pop();
0320         }
0321         applicationWindow().pageStack.push(detailsComponent, { batchId: batchId });
0322     }
0323 
0324     Models.DelegateChooser {
0325         id: chooser
0326         role: "type"
0327         Models.DelegateChoice {
0328             roleValue: TimelineElement.Flight
0329             FlightDelegate {
0330                 batchId: model.batchId
0331                 rangeType: model.rangeType
0332             }
0333         }
0334         Models.DelegateChoice {
0335             roleValue: TimelineElement.Hotel
0336             HotelDelegate {
0337                 batchId: model.batchId
0338                 rangeType: model.rangeType
0339             }
0340         }
0341         Models.DelegateChoice {
0342             roleValue: TimelineElement.TrainTrip
0343             TrainDelegate {
0344                 batchId: model.batchId
0345                 rangeType: model.rangeType
0346             }
0347         }
0348         Models.DelegateChoice {
0349             roleValue: TimelineElement.BusTrip
0350             BusDelegate {
0351                 batchId: model.batchId
0352                 rangeType: model.rangeType
0353             }
0354         }
0355         Models.DelegateChoice {
0356             roleValue: TimelineElement.Restaurant
0357             RestaurantDelegate {
0358                 batchId: model.batchId
0359                 rangeType: model.rangeType
0360             }
0361         }
0362         Models.DelegateChoice {
0363             roleValue: TimelineElement.TouristAttraction
0364             TouristAttractionDelegate {
0365                 batchId: model.batchId
0366                 rangeType: model.rangeType
0367             }
0368         }
0369         Models.DelegateChoice {
0370             roleValue: TimelineElement.Event
0371             EventDelegate {
0372                 batchId: model.batchId
0373                 rangeType: model.rangeType
0374             }
0375         }
0376         Models.DelegateChoice {
0377             roleValue: TimelineElement.CarRental
0378             CarRentalDelegate {
0379                 batchId: model.batchId
0380                 rangeType: model.rangeType
0381             }
0382         }
0383         Models.DelegateChoice {
0384             roleValue: TimelineElement.BoatTrip
0385             BoatDelegate {
0386                 batchId: model.batchId
0387                 rangeType: model.rangeType
0388             }
0389         }
0390         Models.DelegateChoice {
0391             roleValue: TimelineElement.TodayMarker
0392             RowLayout {
0393                 width: ListView.view.width
0394                 Item{ Layout.fillWidth: true }
0395                 QQC2.Label {
0396                     Layout.maximumWidth: Kirigami.Units.gridUnit * 30
0397                     Layout.fillWidth: true
0398                     height: visible ? implicitHeight : 0
0399                     visible: model.isTodayEmpty
0400                     text: i18n("Nothing on the itinerary for today.");
0401                     color: Kirigami.Theme.textColor
0402                 }
0403                 Item{ Layout.fillWidth: true }
0404 
0405             }
0406         }
0407         Models.DelegateChoice {
0408             roleValue: TimelineElement.LocationInfo
0409             LocationInfoDelegate {
0410                 locationInfo: model.locationInformation
0411             }
0412         }
0413         Models.DelegateChoice {
0414             roleValue: TimelineElement.WeatherForecast
0415             WeatherForecastDelegate {}
0416         }
0417         Models.DelegateChoice {
0418             roleValue: TimelineElement.TripGroup
0419             TripGroupDelegate {
0420                 onRemoveTrip: (tripGroupId) => {
0421                     deleteTripGroupWarningDialog.tripGroupId = tripGroupId;
0422                     deleteTripGroupWarningDialog.open();
0423                 }
0424             }
0425         }
0426         Models.DelegateChoice {
0427             roleValue: TimelineElement.Transfer
0428             TransferDelegate {}
0429         }
0430     }
0431 
0432     Kirigami.CardsListView {
0433         id: listView
0434         model: TripGroupProxyModel
0435         delegate: chooser
0436         leftMargin: 0
0437         rightMargin: 0
0438 
0439         section {
0440             property: "sectionHeader"
0441             delegate: TimelineSectionDelegate { day: section }
0442             criteria: ViewSection.FullString
0443             labelPositioning: ViewSection.CurrentLabelAtStart | ViewSection.InlineLabels
0444         }
0445     }
0446 
0447     Connections {
0448         target: ApplicationController
0449         function onEditNewHotelReservation(res) {
0450             applicationWindow().pageStack.push(hotelEditorPage, {reservation: res});
0451         }
0452         function onEditNewRestaurantReservation(res) {
0453             applicationWindow().pageStack.push(restaurantEditorPage, {reservation: res});
0454         }
0455     }
0456 
0457     // work around initial positioning not working correctly below, as at that point
0458     // listView.height has bogus values. No idea why, possibly delayed layouting in the ScrollablePage,
0459     // or a side-effect of the binding loop on delegate heights
0460     Timer {
0461         id: positionTimer
0462         interval: 0
0463         repeat: false
0464         onTriggered: listView.positionViewAtIndex(TripGroupProxyModel.todayRow, ListView.Beginning);
0465     }
0466 
0467     Component.onCompleted: positionTimer.start()
0468 
0469     footer: null
0470 }