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 }