Droidstar-DMR/iOS_OnlyFIles/QsoTab.qml

640 lines
19 KiB
QML

/*
Copyright (C) 2024 Rohith Namboothiri
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 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 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 <https://www.gnu.org/licenses/>.
*/
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Dialogs
Item {
id: qsoTab
width: 400
height: 600
property MainTab mainTab: null
property int dmrID: -1
property int tgid: -1
property string logFileName: "logs.json"
property string savedFilePath: ""
property int latestSerialNumber: 0
property bool isLoading: true
signal firstRowDataChanged(string serialNumber, string callsign, string handle, string country)
signal secondRowDataChanged(string serialNumber, string callsign, string handle, string country)
ListModel {
id: logModel
}
function updateRowData() {
if (logModel.count > 0) {
var firstRow = logModel.get(0);
firstRowDataChanged(firstRow.serialNumber, firstRow.callsign, firstRow.fname, firstRow.country);
} else {
firstRowDataChanged("0", "N/A", "N/A", "N/A");
}
if (logModel.count > 1) {
var secondRow = logModel.get(1);
secondRowDataChanged(secondRow.serialNumber, secondRow.callsign, secondRow.fname, secondRow.country);
} else {
secondRowDataChanged("0", "N/A", "N/A", "N/A");
}
}
Connections {
target: logModel
onCountChanged: {
updateRowData();
}
}
Component.onCompleted: {
mainTab.dataUpdated.connect(onDataUpdated);
updateRowData(); // Ensure both rows are updated initially
loadSettings();
if (mainTab === null) {
console.error("mainTab is null. Ensure it is passed correctly from the parent.");
}
}
function saveSettings() {
var logData = [];
for (var i = 0; i < logModel.count; i++) {
logData.push(logModel.get(i));
}
logHandler.saveLog(logFileName, logData);
}
function loadSettings() {
var savedData = logHandler.loadLog(logFileName);
for (var i = 0; i < savedData.length; i++) {
savedData[i].checked = false;
logModel.append(savedData[i]);
latestSerialNumber = Math.max(latestSerialNumber, savedData[i].serialNumber + 1);
}
isLoading = false;
}
function clearSettings() {
logModel.clear();
logHandler.clearLog(logFileName);
latestSerialNumber = 0; // Reset the serial number counter to 0
}
function exportLog() {
saveFileNameDialog.open(); // Prompt for file name
}
// Header Row with Text and Clear Button
Text {
id: headerText
text: "This page logs lastheard stations in descending order. An upgrade to a native Log book is coming soon."
wrapMode: Text.WordWrap
font.bold: true
font.pointSize: 12
color: "white"
width: parent.width - 50
x: 20
y: 10
}
Button {
id: clearButton
text: "Clear"
x: 20
y: headerText.y + headerText.height + 12
onClicked: {
clearSettings();
}
background: Rectangle {
color: "red" // Set the background color to red
radius: 4 // Optional: Add some rounding to the corners
}
contentItem: Text {
text: clearButton.text
color: "white" // Set text color to white for contrast
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
anchors.centerIn: parent
font.bold: true
}
}
Button {
id: exportButton
text: "Export Log"
x: clearButton.x + clearButton.width + 10
y: headerText.y + headerText.height + 12
onClicked: exportLog()
background: Rectangle {
color: "green" // Set the background color to green
radius: 4 // Optional: Add some rounding to the corners
}
contentItem: Text {
text: exportButton.text
color: "white" // Set text color to white for contrast
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
anchors.centerIn: parent
font.bold: true
}
}
Button {
id: clonedButton
visible: mainTab.buttonTX.visible
enabled: mainTab.buttonTX.enabled
x: exportButton.x + exportButton.width + 10
y: exportButton.y
width: exportButton.width * 1.5
height: exportButton.height
background: Rectangle {
color: mainTab.buttonTX.tx ? "#800000" : "steelblue"
radius: 4
Column {
anchors.centerIn: parent
spacing: 2
Text {
id: clonedText
font.pointSize: 20
text: qsTr("TX")
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
}
// Ensure cloned button follows the same behavior as the original
onClicked: mainTab.buttonTX.clicked()
onPressed: mainTab.buttonTX.pressed()
onReleased: mainTab.buttonTX.released()
onCanceled: mainTab.buttonTX.canceled()
}
// Dialog to prompt for file name
Dialog {
id: saveFileNameDialog
title: "Enter File Name and Choose Format"
standardButtons: Dialog.Ok | Dialog.Cancel
modal: true
contentItem: Column {
spacing: 10
TextField {
id: fileNameInput
placeholderText: "Enter file name"
width: parent.width - 20
}
Row {
spacing: 10
RadioButton {
id: csvRadioButton
text: "CSV"
checked: true // Default to CSV
}
RadioButton {
id: adifRadioButton
text: "ADIF"
}
}
}
onAccepted: {
var logData = [];
var hasSelection = false;
for (var i = 0; i < logModel.count; i++) {
var entry = logModel.get(i);
if (entry.checked) {
logData.push(entry);
hasSelection = true;
}
}
if (!hasSelection) { // If no selections, export all
for (var i = 0; i < logModel.count; i++) {
logData.push(logModel.get(i));
}
}
var fileName = fileNameInput.text;
var filePath = logHandler.getDSLogPath() + "/" + fileName;
if (csvRadioButton.checked) {
fileName += ".csv";
filePath += ".csv";
if (logHandler.exportLogToCsv(filePath, logData)) {
savedFilePath = logHandler.getFriendlyPath(filePath);
fileSavedDialog.open();
} else {
console.error("Failed to save the CSV file.");
}
} else if (adifRadioButton.checked) {
fileName += ".adi";
filePath += ".adi";
if (logHandler.exportLogToAdif(filePath, logData)) {
savedFilePath = logHandler.getFriendlyPath(filePath);
fileSavedDialog.open();
} else {
console.error("Failed to save the ADIF file.");
}
}
}
}
// Error Dialog to show if file name is empty
Dialog {
id: errorDialog
title: "Error"
standardButtons: Dialog.Ok
modal: true
width: 300
contentItem: Text {
text: "Empty/Invalid File Name."
wrapMode: Text.WordWrap
color: "red"
width: parent.width * 0.9
}
}
// Dialog to show that the file was saved
Dialog {
id: fileSavedDialog
title: "File Saved"
standardButtons: Dialog.Ok
width: 300
onAccepted: {
console.log("File saved successfully!");
}
background: Rectangle {
color: "#80c342"
radius: 8
}
contentItem: Column {
spacing: 10
// Text to display the success message
Text {
text: "File saved successfully to " + savedFilePath
font.pointSize: 14
color: "black"
wrapMode: Text.WordWrap
width: parent.width * 0.9
}
// Row for the buttons
Row {
spacing: 10
anchors.horizontalCenter: parent.horizontalCenter
Button {
text: "Cancel"
onClicked: fileSavedDialog.accept()
}
Button {
text: "Share"
onClicked: {
logHandler.shareFile();
}
}
}
}
}
Row {
id: tableHeader
width: parent.width
height: 25
y: clearButton.y + clearButton.height + 10
spacing: 4
Rectangle {
width: parent.width / 8
height: parent.height
color: "darkgrey"
Text {
anchors.centerIn: parent
text: "Sr.No"
font.bold: true
font.pixelSize: 12
}
}
Rectangle {
width: parent.width / 6
height: parent.height
color: "darkgrey"
Text {
anchors.centerIn: parent
text: "Callsign"
font.bold: true
font.pixelSize: 12
}
}
Rectangle {
width: parent.width / 6
height: parent.height
color: "darkgrey"
Text {
anchors.centerIn: parent
text: "DMR ID"
font.bold: true
font.pixelSize: 12
}
}
Rectangle {
width: parent.width / 6
height: parent.height
color: "darkgrey"
Text {
anchors.centerIn: parent
text: "TGID"
font.bold: true
font.pixelSize: 12
}
}
Rectangle {
width: parent.width / 6
height: parent.height
color: "darkgrey"
Text {
anchors.centerIn: parent
text: "Handle"
font.bold: true
font.pixelSize: 12
}
}
Rectangle {
width: parent.width / 6
height: parent.height
color: "darkgrey"
Text {
anchors.centerIn: parent
text: "Country"
font.bold: true
font.pixelSize: 12
}
}
}
TableView {
id: tableView
x: 0
y: tableHeader.y + tableHeader.height + 10
width: parent.width
height: parent.height - (tableHeader.y + tableHeader.height + 30)
model: logModel
delegate: Rectangle {
width: tableView.width
implicitWidth: tableView.width
implicitHeight: 100
height: implicitHeight
color: checkBox.checked ? "#b9fbd7" : (index % 2 === 0 ? "lightgrey" : "white")
Row {
width: parent.width
height: 40
Rectangle {
width: parent.width / 8
height: parent.height
color: "transparent"
Text {
anchors.centerIn: parent
text: serialNumber
font.pixelSize: 12
}
}
Rectangle {
width: parent.width / 6
height: parent.height
color: "transparent"
Text {
anchors.centerIn: parent
text: callsign
font.pixelSize: 12
}
}
Rectangle {
width: parent.width / 6
height: parent.height
color: "transparent"
Text {
anchors.centerIn: parent
text: dmrID
font.pixelSize: 12
}
}
Rectangle {
width: parent.width / 6
height: parent.height
color: "transparent"
Text {
anchors.centerIn: parent
text: tgid
font.pixelSize: 12
}
}
Rectangle {
width: parent.width / 6
height: parent.height
color: "transparent"
Text {
anchors.centerIn: parent
text: fname
font.pixelSize: 12
}
}
Rectangle {
width: parent.width / 6
height: parent.height
color: "transparent"
Text {
anchors.centerIn: parent
text: country
font.pixelSize: 12
}
}
}
Row {
width: parent.width - 20
anchors.horizontalCenter: parent.horizontalCenter
spacing: 10
y: 40
CheckBox {
id: checkBox
checked: model.checked !== undefined ? model.checked : false
onCheckedChanged: {
model.checked = checked;
}
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: model.currentTime
wrapMode: Text.WordWrap
font.pixelSize: 12
width: parent.width - checkBox.width - 50
anchors.verticalCenter: checkBox.verticalCenter
elide: Text.ElideRight
}
Text {
id: menuIcon
text: "\uf0c9"
font.family: "FontAwesome"
font.pixelSize: 20
anchors.verticalCenter: parent.verticalCenter
color: "black"
MouseArea {
anchors.fill: parent
onClicked: {
if (contextMenu.visible) {
contextMenu.close();
} else {
contextMenu.x = menuIcon.x + menuIcon.width
contextMenu.y = menuIcon.y
contextMenu.open()
}
}
}
}
}
Menu {
id: contextMenu
title: "Lookup Options"
visible: false // Ensure it's not visible by default
MenuItem {
text: "Lookup QRZ"
onTriggered: Qt.openUrlExternally("https://qrz.com/lookup/" + model.callsign)
}
MenuItem {
text: "Lookup BM"
onTriggered: Qt.openUrlExternally("https://brandmeister.network/index.php?page=profile&call=" + model.callsign)
}
MenuItem {
text: "Lookup APRS"
onTriggered: Qt.openUrlExternally("https://aprs.fi/#!call=a%2F" + model.callsign)
}
// onClosed: visible = false
}
}
}
function onDataUpdated(receivedDmrID, receivedTGID) {
console.log("Received dmrID:", receivedDmrID, "and TGID:", receivedTGID);
qsoTab.dmrID = receivedDmrID;
qsoTab.tgid = receivedTGID;
fetchData(receivedDmrID, receivedTGID);
}
function fetchData(dmrID, tgid) {
var xhr = new XMLHttpRequest();
xhr.open("GET", "https://radioid.net/api/dmr/user/?id=" + dmrID, true);
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
var response = JSON.parse(xhr.responseText);
if (response.count > 0) {
var result = response.results[0];
var data = {
callsign: result.callsign,
dmrID: result.id,
tgid: tgid, // Include TGID in the data object
country: result.country,
fname: result.fname,
currentTime: Qt.formatDateTime(new Date(), "yyyy-MM-dd HH:mm:ss") // Current local time
};
addEntry(data);
}
} else {
console.error("Failed to fetch data. Status:", xhr.status);
}
}
};
xhr.send();
}
function addEntry(data) {
console.log("Processing data in QsoTab:", JSON.stringify(data));
if (!data || typeof data !== 'object') {
console.error("Invalid data received:", data);
return;
}
isLoading = false;
// Check and update the country field
if (data.country === "United States") {
data.country = "USA";
} else if (data.country === "United Kingdom") {
data.country = "UK";
}
logModel.insert(0, {
serialNumber: latestSerialNumber,
callsign: data.callsign,
dmrID: data.dmrID,
tgid: data.tgid,
country: data.country,
fname: data.fname,
currentTime: data.currentTime,
checked: false
});
latestSerialNumber += 1;
saveSettings();
// Ensure that the log doesn't exceed the maximum number of entries
const maxEntries = 250;
while (logModel.count > maxEntries) {
logModel.remove(logModel.count - 1);
}
}
}