diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 956401bac..c16cf1ffa 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -144,6 +144,8 @@ add_library(neochat STATIC models/timelinemodel.cpp models/timelinemodel.h enums/pushrule.h + models/itinerarymodel.cpp + models/itinerarymodel.h ) qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN @@ -303,6 +305,8 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN qml/glowdot.png ) +configure_file(config-neochat.h.in ${CMAKE_CURRENT_BINARY_DIR}/config-neochat.h) + if(WIN32) set_target_properties(neochat PROPERTIES OUTPUT_NAME "neochatlib") endif() diff --git a/src/config-neochat.h.in b/src/config-neochat.h.in new file mode 100644 index 000000000..f647d5876 --- /dev/null +++ b/src/config-neochat.h.in @@ -0,0 +1,8 @@ +/* + SPDX-FileCopyrightText: 2024 Tobias Fella + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#define CMAKE_INSTALL_FULL_LIBEXECDIR_KF6 "${KDE_INSTALL_FULL_LIBEXECDIR_KF}" diff --git a/src/models/itinerarymodel.cpp b/src/models/itinerarymodel.cpp new file mode 100644 index 000000000..8dd32b602 --- /dev/null +++ b/src/models/itinerarymodel.cpp @@ -0,0 +1,163 @@ +// SPDX-FileCopyrightText: 2024 Tobias Fella +// SPDX-License-Identifier: LGPL-2.0-or-later + +#include "itinerarymodel.h" + +#include + +#include "config-neochat.h" + +#ifndef Q_OS_ANDROID +#include +#endif + +ItineraryModel::ItineraryModel(QObject *parent) + : QAbstractListModel(parent) +{ +} + +void ItineraryModel::setConnection(NeoChatConnection *connection) +{ + if (m_connection == connection) { + return; + } + m_connection = connection; + Q_EMIT connectionChanged(); +} + +NeoChatConnection *ItineraryModel::connection() const +{ + return m_connection; +} + +QVariant ItineraryModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return {}; + } + auto row = index.row(); + auto data = m_data[row]; + if (role == NameRole) { + if (data[QStringLiteral("@type")] == QStringLiteral("TrainReservation")) { + return data[QStringLiteral("reservationFor")][QStringLiteral("trainNumber")]; + } + if (data[QStringLiteral("@type")] == QStringLiteral("LodgingReservation")) { + return data[QStringLiteral("reservationFor")][QStringLiteral("name")]; + } + } + if (role == TypeRole) { + return data[QStringLiteral("@type")]; + } + if (role == DepartureStationRole) { + return data[QStringLiteral("reservationFor")][QStringLiteral("departureStation")][QStringLiteral("name")]; + } + if (role == ArrivalStationRole) { + return data[QStringLiteral("reservationFor")][QStringLiteral("arrivalStation")][QStringLiteral("name")]; + } + if (role == DepartureTimeRole) { + const auto &time = data[QStringLiteral("reservationFor")][QStringLiteral("departureTime")]; + auto dateTime = (time.isString() ? time : time[QStringLiteral("@value")]).toVariant().toDateTime(); + if (const auto &timeZone = time[QStringLiteral("timezone")].toString(); timeZone.length() > 0) { + dateTime.setTimeZone(QTimeZone(timeZone.toLatin1().data())); + } + return dateTime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat)); + } + if (role == ArrivalTimeRole) { + const auto &time = data[QStringLiteral("reservationFor")][QStringLiteral("arrivalTime")]; + auto dateTime = (time.isString() ? time : time[QStringLiteral("@value")]).toVariant().toDateTime(); + if (const auto &timeZone = time[QStringLiteral("timezone")].toString(); timeZone.length() > 0) { + dateTime.setTimeZone(QTimeZone(timeZone.toLatin1().data())); + } + return dateTime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat)); + } + if (role == AddressRole) { + const auto &addressData = data[QStringLiteral("reservationFor")][QStringLiteral("address")]; + return QStringLiteral("%1 - %2 %3 %4") + .arg(addressData[QStringLiteral("streetAddress")].toString(), + addressData[QStringLiteral("postalCode")].toString(), + addressData[QStringLiteral("addressLocality")].toString(), + addressData[QStringLiteral("addressCountry")].toString()); + } + if (role == StartTimeRole) { + auto dateTime = data[QStringLiteral("checkinTime")][QStringLiteral("@value")].toVariant().toDateTime(); + return dateTime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat)); + } + if (role == EndTimeRole) { + auto dateTime = data[QStringLiteral("checkoutTime")][QStringLiteral("@value")].toVariant().toDateTime(); + return dateTime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat)); + } + if (role == DeparturePlatformRole) { + return data[QStringLiteral("reservationFor")][QStringLiteral("departurePlatform")]; + } + if (role == ArrivalPlatformRole) { + return data[QStringLiteral("reservationFor")][QStringLiteral("arrivalPlatform")]; + } + if (role == CoachRole) { + return data[QStringLiteral("reservedTicket")][QStringLiteral("ticketedSeat")][QStringLiteral("seatSection")]; + } + if (role == SeatRole) { + return data[QStringLiteral("reservedTicket")][QStringLiteral("ticketedSeat")][QStringLiteral("seatNumber")]; + } + return {}; +} + +int ItineraryModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return m_data.size(); +} + +QHash ItineraryModel::roleNames() const +{ + return { + {NameRole, "name"}, + {TypeRole, "type"}, + {DepartureStationRole, "departureStation"}, + {ArrivalStationRole, "arrivalStation"}, + {DepartureTimeRole, "departureTime"}, + {ArrivalTimeRole, "arrivalTime"}, + {AddressRole, "address"}, + {StartTimeRole, "startTime"}, + {EndTimeRole, "endTime"}, + {DeparturePlatformRole, "departurePlatform"}, + {ArrivalPlatformRole, "arrivalPlatform"}, + {CoachRole, "coach"}, + {SeatRole, "seat"}, + }; +} + +QString ItineraryModel::path() const +{ + return m_path; +} + +void ItineraryModel::setPath(const QString &path) +{ + if (path == m_path) { + return; + } + m_path = path; + Q_EMIT pathChanged(); + loadData(); +} + +void ItineraryModel::loadData() +{ + auto process = new QProcess(this); + process->start(QLatin1String(CMAKE_INSTALL_FULL_LIBEXECDIR_KF6) + QLatin1String("/kitinerary-extractor"), {m_path.mid(7)}); + connect(process, &QProcess::finished, this, [this, process]() { + auto data = process->readAllStandardOutput(); + beginResetModel(); + m_data = QJsonDocument::fromJson(data).array(); + endResetModel(); + }); +} + +void ItineraryModel::sendToItinerary() +{ +#ifndef Q_OS_ANDROID + auto job = new KIO::ApplicationLauncherJob(KService::serviceByDesktopName(QStringLiteral("org.kde.itinerary"))); + job->setUrls({QUrl::fromLocalFile(m_path.mid(7))}); + job->start(); +#endif +} diff --git a/src/models/itinerarymodel.h b/src/models/itinerarymodel.h new file mode 100644 index 000000000..6501c96dc --- /dev/null +++ b/src/models/itinerarymodel.h @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2024 Tobias Fella +// SPDX-License-Identifier: LGPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include "neochatconnection.h" + +class ItineraryModel : public QAbstractListModel +{ + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged) + Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged) + +public: + enum Roles { + NameRole = Qt::DisplayRole, + TypeRole, + DepartureStationRole, + ArrivalStationRole, + DepartureTimeRole, + ArrivalTimeRole, + AddressRole, + StartTimeRole, + EndTimeRole, + DeparturePlatformRole, + ArrivalPlatformRole, + CoachRole, + SeatRole, + }; + Q_ENUM(Roles) + explicit ItineraryModel(QObject *parent = nullptr); + + void setConnection(NeoChatConnection *connection); + NeoChatConnection *connection() const; + + QVariant data(const QModelIndex &index, int role) const override; + int rowCount(const QModelIndex &parent = {}) const override; + + QHash roleNames() const override; + + QString path() const; + void setPath(const QString &path); + + Q_INVOKABLE void sendToItinerary(); + +Q_SIGNALS: + void connectionChanged(); + void pathChanged(); + +private: + QPointer m_connection; + QJsonArray m_data; + QString m_path; + void loadData(); +}; diff --git a/src/qml/FileDelegate.qml b/src/qml/FileDelegate.qml index 7f0f97455..b4e074e21 100644 --- a/src/qml/FileDelegate.qml +++ b/src/qml/FileDelegate.qml @@ -5,6 +5,7 @@ import QtQuick import QtQuick.Controls as QQC2 import QtQuick.Layouts import Qt.labs.platform +import Qt.labs.qmlmodels import org.kde.coreaddons import org.kde.kirigami as Kirigami @@ -41,8 +42,11 @@ MessageDelegate { */ property bool autoOpenFile: false - onDownloadedChanged: if (autoOpenFile) { - openSavedFile(); + onDownloadedChanged: { + itineraryModel.path = root.progressInfo.localPath + if (autoOpenFile) { + openSavedFile(); + } } onOpenContextMenu: RoomManager.viewEventMenu(eventId, author, delegateType, plainText, "", "", mediaInfo.mimeType, progressInfo) @@ -57,135 +61,217 @@ MessageDelegate { UrlHelper.openUrl(root.progressInfo.localPath); } - bubbleContent: RowLayout { + bubbleContent: ColumnLayout { spacing: Kirigami.Units.largeSpacing + RowLayout { + spacing: Kirigami.Units.largeSpacing - states: [ - State { - name: "downloadedInstant" - when: root.progressInfo.completed && autoOpenFile + states: [ + State { + name: "downloadedInstant" + when: root.progressInfo.completed && autoOpenFile - PropertyChanges { - target: openButton - icon.name: "document-open" - onClicked: openSavedFile() + PropertyChanges { + target: openButton + icon.name: "document-open" + onClicked: openSavedFile() + } + + PropertyChanges { + target: downloadButton + icon.name: "download" + QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to download its file", "Download") + onClicked: saveFileAs() + } + }, + State { + name: "downloaded" + when: root.progressInfo.completed && !autoOpenFile + + PropertyChanges { + target: openButton + visible: false + } + + PropertyChanges { + target: downloadButton + icon.name: "document-open" + QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to open its downloaded file with an appropriate application", "Open File") + onClicked: openSavedFile() + } + }, + State { + name: "downloading" + when: root.progressInfo.active + + PropertyChanges { + target: openButton + visible: false + } + + PropertyChanges { + target: sizeLabel + text: i18nc("file download progress", "%1 / %2", Format.formatByteSize(root.progressInfo.progress), Format.formatByteSize(root.progressInfo.total)) + } + PropertyChanges { + target: downloadButton + icon.name: "media-playback-stop" + QQC2.ToolTip.text: i18nc("tooltip for a button on a message; stops downloading the message's file", "Stop Download") + onClicked: currentRoom.cancelFileTransfer(root.eventId) + } + }, + State { + name: "raw" + when: true + + PropertyChanges { + target: downloadButton + onClicked: root.saveFileAs() + } } + ] - PropertyChanges { - target: downloadButton - icon.name: "download" - QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to download its file", "Download") - onClicked: saveFileAs() - } - }, - State { - name: "downloaded" - when: root.progressInfo.completed && !autoOpenFile - - PropertyChanges { - target: openButton - visible: false - } - - PropertyChanges { - target: downloadButton - icon.name: "document-open" - QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to open its downloaded file with an appropriate application", "Open File") - onClicked: openSavedFile() - } - }, - State { - name: "downloading" - when: root.progressInfo.active - - PropertyChanges { - target: openButton - visible: false - } - - PropertyChanges { - target: sizeLabel - text: i18nc("file download progress", "%1 / %2", Format.formatByteSize(root.progressInfo.progress), Format.formatByteSize(root.progressInfo.total)) - } - PropertyChanges { - target: downloadButton - icon.name: "media-playback-stop" - QQC2.ToolTip.text: i18nc("tooltip for a button on a message; stops downloading the message's file", "Stop Download") - onClicked: currentRoom.cancelFileTransfer(root.eventId) - } - }, - State { - name: "raw" - when: true - - PropertyChanges { - target: downloadButton - onClicked: root.saveFileAs() - } - } - ] - - Kirigami.Icon { - source: root.mediaInfo.mimeIcon - fallback: "unknown" - } - - ColumnLayout { - spacing: 0 - QQC2.Label { - Layout.fillWidth: true - text: root.display - wrapMode: Text.Wrap - elide: Text.ElideRight - } - QQC2.Label { - id: sizeLabel - Layout.fillWidth: true - text: Format.formatByteSize(root.mediaInfo.size) - opacity: 0.7 - elide: Text.ElideRight - maximumLineCount: 1 - } - } - - QQC2.Button { - id: openButton - icon.name: "document-open" - onClicked: { - autoOpenFile = true; - currentRoom.downloadTempFile(root.eventId); + Kirigami.Icon { + source: root.mediaInfo.mimeIcon + fallback: "unknown" } - QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to open its downloaded file with an appropriate application", "Open File") - QQC2.ToolTip.visible: hovered - QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay - } + ColumnLayout { + spacing: 0 + QQC2.Label { + Layout.fillWidth: true + text: root.display + wrapMode: Text.Wrap + elide: Text.ElideRight + } + QQC2.Label { + id: sizeLabel + Layout.fillWidth: true + text: Format.formatByteSize(root.mediaInfo.size) + opacity: 0.7 + elide: Text.ElideRight + maximumLineCount: 1 + } + } - QQC2.Button { - id: downloadButton - icon.name: "download" + QQC2.Button { + id: openButton + icon.name: "document-open" + onClicked: { + autoOpenFile = true; + currentRoom.downloadTempFile(root.eventId); + } - QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to download its file", "Download") - QQC2.ToolTip.visible: hovered - QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay - } + QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to open its downloaded file with an appropriate application", "Open File") + QQC2.ToolTip.visible: hovered + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + } - Component { - id: fileDialog + QQC2.Button { + id: downloadButton + icon.name: "download" - FileDialog { - fileMode: FileDialog.SaveFile - folder: Config.lastSaveDirectory.length > 0 ? Config.lastSaveDirectory : StandardPaths.writableLocation(StandardPaths.DownloadLocation) - onAccepted: { - Config.lastSaveDirectory = folder - Config.save() - if (autoOpenFile) { - UrlHelper.copyTo(root.progressInfo.localPath, file) - } else { - currentRoom.download(root.eventId, file); + QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to download its file", "Download") + QQC2.ToolTip.visible: hovered + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + } + + Component { + id: fileDialog + + FileDialog { + fileMode: FileDialog.SaveFile + folder: Config.lastSaveDirectory.length > 0 ? Config.lastSaveDirectory : StandardPaths.writableLocation(StandardPaths.DownloadLocation) + onAccepted: { + Config.lastSaveDirectory = folder + Config.save() + if (autoOpenFile) { + UrlHelper.copyTo(root.progressInfo.localPath, file) + } else { + currentRoom.download(root.eventId, file); + } } } } } + Repeater { + id: itinerary + model: ItineraryModel { + id: itineraryModel + connection: root.connection + } + delegate: DelegateChooser { + role: "type" + DelegateChoice { + roleValue: "TrainReservation" + delegate: ColumnLayout { + Kirigami.Separator { + Layout.fillWidth: true + } + RowLayout { + QQC2.Label { + text: model.name + } + QQC2.Label { + text: model.coach ? i18n("Coach: %1, Seat: %2", model.coach, model.seat) : "" + visible: model.coach + opacity: 0.7 + } + } + RowLayout { + Layout.fillWidth: true + ColumnLayout { + QQC2.Label { + text: model.departureStation + (model.departurePlatform ? (" [" + model.departurePlatform + "]") : "") + } + QQC2.Label { + text: model.departureTime + opacity: 0.7 + } + } + Item { + Layout.fillWidth: true + } + ColumnLayout { + QQC2.Label { + text: model.arrivalStation + (model.arrivalPlatform ? (" [" + model.arrivalPlatform + "]") : "") + } + QQC2.Label { + text: model.arrivalTime + opacity: 0.7 + Layout.alignment: Qt.AlignRight + } + } + } + } + } + DelegateChoice { + roleValue: "LodgingReservation" + delegate: ColumnLayout { + Kirigami.Separator { + Layout.fillWidth: true + } + QQC2.Label { + text: model.name + } + QQC2.Label { + text: i18nc(" - ", "%1 - %2", model.startTime, model.endTime) + } + QQC2.Label { + text: model.address + } + } + } + } + } + QQC2.Button { + icon.name: "map-globe" + text: i18nc("@action", "Send to KDE Itinerary") + QQC2.ToolTip.visible: hovered + QQC2.ToolTip.text: text + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + onClicked: itineraryModel.sendToItinerary() + visible: itinerary.count > 0 + } } }