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 }