mirror of
				https://github.com/subsurface/subsurface.git
				synced 2025-02-19 22:16:15 +00:00 
			
		
		
		
	While the startup flow should make it obvious when a user is not correctly logged into the cloud, we still do see fairly frequent situations where a user has an incorrect password on a mobile device and is confused about why their data isn't syncing with their PC. Now this is clearly shown in the main menu. Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
		
			
				
	
	
		
			2403 lines
		
	
	
	
		
			79 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			2403 lines
		
	
	
	
		
			79 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| // SPDX-License-Identifier: GPL-2.0
 | |
| #include "qmlmanager.h"
 | |
| #include <QUrl>
 | |
| #include <QSettings>
 | |
| #include <QDebug>
 | |
| #include <QNetworkAccessManager>
 | |
| #include <QAuthenticator>
 | |
| #include <QDesktopServices>
 | |
| #include <QTextDocument>
 | |
| #include <QRegularExpression>
 | |
| #include <QApplication>
 | |
| #include <QElapsedTimer>
 | |
| #include <QTimer>
 | |
| #include <QDateTime>
 | |
| #include <QClipboard>
 | |
| #include <QFile>
 | |
| #include <QtConcurrent>
 | |
| #include <QFuture>
 | |
| #include <QUndoStack>
 | |
| 
 | |
| #include <QBluetoothLocalDevice>
 | |
| 
 | |
| #include "qt-models/completionmodels.h"
 | |
| #include "qt-models/messagehandlermodel.h"
 | |
| #include "qt-models/tankinfomodel.h"
 | |
| #include "qt-models/mobilelistmodel.h"
 | |
| #include "core/device.h"
 | |
| #include "core/errorhelper.h"
 | |
| #include "core/file.h"
 | |
| #include "core/divefilter.h"
 | |
| #include "core/divelog.h"
 | |
| #include "core/filterconstraint.h"
 | |
| #include "core/qthelper.h"
 | |
| #include "core/qt-gui.h"
 | |
| #include "core/git-access.h"
 | |
| #include "core/cloudstorage.h"
 | |
| #include "core/downloadfromdcthread.h"
 | |
| #include "core/subsurfacestartup.h" // for ignore_bt flag
 | |
| #include "core/subsurface-string.h"
 | |
| #include "core/string-format.h"
 | |
| #include "core/pref.h"
 | |
| #include "core/selection.h"
 | |
| #include "core/ssrf.h"
 | |
| #include "core/save-profiledata.h"
 | |
| #include "core/settings/qPrefLog.h"
 | |
| #include "core/settings/qPrefTechnicalDetails.h"
 | |
| #include "core/settings/qPrefPartialPressureGas.h"
 | |
| #include "core/settings/qPrefUnit.h"
 | |
| #include "core/trip.h"
 | |
| #include "core/tag.h"
 | |
| #include "backend-shared/exportfuncs.h"
 | |
| #include "core/worldmap-save.h"
 | |
| #include "core/uploadDiveLogsDE.h"
 | |
| #include "core/uploadDiveShare.h"
 | |
| #include "commands/command_base.h"
 | |
| #include "commands/command.h"
 | |
| 
 | |
| #if defined(Q_OS_ANDROID)
 | |
| #include <QtAndroid>
 | |
| #include "core/serial_usb_android.h"
 | |
| std::vector<android_usb_serial_device_descriptor> androidSerialDevices;
 | |
| #endif
 | |
| 
 | |
| QMLManager *QMLManager::m_instance = NULL;
 | |
| bool noCloudToCloud = false;
 | |
| 
 | |
| #define RED_FONT QLatin1String("<font color=\"red\">")
 | |
| #define END_FONT QLatin1String("</font>")
 | |
| 
 | |
| extern "C" void showErrorFromC(char *buf)
 | |
| {
 | |
| 	QString error(buf);
 | |
| 	free(buf);
 | |
| 	// By using invokeMethod with Qt:AutoConnection, the error string is safely
 | |
| 	// transported across thread boundaries, if not called from the UI thread.
 | |
| 	QMetaObject::invokeMethod(QMLManager::instance(), "registerError", Qt::AutoConnection, Q_ARG(QString, error));
 | |
| }
 | |
| 
 | |
| // this gets called from libdivecomputer
 | |
| static void progressCallback(const char *text)
 | |
| {
 | |
| 	QMLManager *self = QMLManager::instance();
 | |
| 	if (self) {
 | |
| 		self->appendTextToLog(QString(text));
 | |
| 		self->setProgressMessage(QString(text));
 | |
| 	}
 | |
| }
 | |
| 
 | |
| static void appendTextToLogStandalone(const char *text)
 | |
| {
 | |
| 	QMLManager *self = QMLManager::instance();
 | |
| 	if (self)
 | |
| 		self->appendTextToLog(QString(text));
 | |
| }
 | |
| 
 | |
| // This flag is set to true by operations that are not implemented in the
 | |
| // undo system. It is therefore only cleared on save and load.
 | |
| static bool dive_list_changed = false;
 | |
| 
 | |
| void mark_divelist_changed(bool changed)
 | |
| {
 | |
| 	if (dive_list_changed == changed)
 | |
| 		return;
 | |
| 	dive_list_changed = changed;
 | |
| 	updateWindowTitle();
 | |
| }
 | |
| 
 | |
| int unsaved_changes()
 | |
| {
 | |
| 	return dive_list_changed;
 | |
| }
 | |
| 
 | |
| // this callback is used from the uiNotification() function
 | |
| // the detour via callback allows us to keep the core code independent from QMLManager
 | |
| // I'm not sure it makes sense to have three different progress callbacks,
 | |
| // but the usage models (and the situations in the program flow where they are used)
 | |
| // are really vastly different...
 | |
| // this is mainly intended for the early stages of the app so the user sees that
 | |
| // things are progressing
 | |
| static void showProgress(QString msg)
 | |
| {
 | |
| 	QMLManager *self = QMLManager::instance();
 | |
| 	if (self)
 | |
| 		self->setNotificationText(msg);
 | |
| }
 | |
| 
 | |
| // show the git progress in the passive notification area
 | |
| extern "C" int gitProgressCB(const char *text)
 | |
| {
 | |
| 	// regular users, during regular operation, likely really don't
 | |
| 	// care at all about the git progress
 | |
| 	if (verbose) {
 | |
| 		showProgress(QString(text));
 | |
| 		appendTextToLogStandalone(text);
 | |
| 	}
 | |
| 	// return 0 so that we don't end the download
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| void QMLManager::registerError(QString error)
 | |
| {
 | |
| 	appendTextToLog(error);
 | |
| 	if (!m_lastError.isEmpty())
 | |
| 		m_lastError += '\n';
 | |
| 	m_lastError += error;
 | |
| }
 | |
| 
 | |
| QString QMLManager::consumeError()
 | |
| {
 | |
| 	QString ret;
 | |
| 	ret.swap(m_lastError);
 | |
| 	return ret;
 | |
| }
 | |
| 
 | |
| void QMLManager::btHostModeChange(QBluetoothLocalDevice::HostMode state)
 | |
| {
 | |
| 	BTDiscovery *btDiscovery = BTDiscovery::instance();
 | |
| 
 | |
| 	qDebug() << "btHostModeChange to " << state;
 | |
| 	if (state != QBluetoothLocalDevice::HostPoweredOff) {
 | |
| 		connectionListModel.removeAllAddresses();
 | |
| 		btDiscovery->BTDiscoveryReDiscover();
 | |
| 		m_btEnabled = btDiscovery->btAvailable();
 | |
| 	} else {
 | |
| 		connectionListModel.removeAllAddresses();
 | |
| 		set_non_bt_addresses();
 | |
| 		m_btEnabled = false;
 | |
| 	}
 | |
| 	emit btEnabledChanged();
 | |
| }
 | |
| 
 | |
| void QMLManager::btRescan()
 | |
| {
 | |
| 	BTDiscovery::instance()->BTDiscoveryReDiscover();
 | |
| }
 | |
| 
 | |
| void QMLManager::rescanConnections()
 | |
| {
 | |
| 	connectionListModel.removeAllAddresses();
 | |
| 	usbRescan();
 | |
| 	btRescan();
 | |
| #if defined(SERIAL_FTDI)
 | |
| 	connectionListModel.addAddress("FTDI");
 | |
| #endif
 | |
| }
 | |
| 
 | |
| void QMLManager::usbRescan()
 | |
| {
 | |
| #if defined(Q_OS_ANDROID)
 | |
| 	androidUsbPopoulateConnections();
 | |
| #endif
 | |
| }
 | |
| 
 | |
| extern void (*uiNotificationCallback)(QString);
 | |
| 
 | |
| // Currently we have two markers for unsaved changes:
 | |
| // 1) unsaved_changes() returns true for non-undoable changes.
 | |
| // 2) Command::isClean() returns false for undoable changes.
 | |
| static bool unsavedChanges()
 | |
| {
 | |
| 	return unsaved_changes() || !Command::isClean();
 | |
| }
 | |
| 
 | |
| QMLManager::QMLManager() :
 | |
| 	m_verboseEnabled(false),
 | |
| 	m_diveListProcessing(false),
 | |
| 	m_initialized(false),
 | |
| 	m_pluggedInDeviceName(""),
 | |
| 	m_showNonDiveComputers(false),
 | |
| 	m_oldStatus(qPrefCloudStorage::CS_UNKNOWN)
 | |
| {
 | |
| 	m_instance = this;
 | |
| 	m_lastDevicePixelRatio = qApp->devicePixelRatio();
 | |
| 	timer.start();
 | |
| 	// make upload signals available in QML
 | |
| 	// Remark: signal - signal connect
 | |
| 	connect(uploadDiveLogsDE::instance(), &uploadDiveLogsDE::uploadFinish,
 | |
| 			this, &QMLManager::uploadFinish);
 | |
| 	connect(uploadDiveLogsDE::instance(), &uploadDiveLogsDE::uploadProgress,
 | |
| 			this, &QMLManager::uploadProgress);
 | |
| 	connect(uploadDiveShare::instance(), &uploadDiveShare::uploadProgress,
 | |
| 			this, &QMLManager::uploadProgress);
 | |
| 
 | |
| 	// uploadDiveShare::uploadFinish() is defined with 3 parameters,
 | |
| 	// whereas QMLManager::uploadFinish() is defined with 2 parameters,
 | |
| 	// Solution add a slot as landing zone.
 | |
| 	connect(uploadDiveShare::instance(), &uploadDiveShare::uploadFinish,
 | |
| 			this, &QMLManager::uploadFinishSlot);
 | |
| 
 | |
| #if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
 | |
| #if defined(Q_OS_ANDROID)
 | |
| 	// on Android we first try the AppDataLocation (which allows sharing of files), then the
 | |
| 	// GenericDataLocation (typically /storage/emulated/0), and if that fails (as happened e.g. on a
 | |
| 	// Sony Xperia phone) we try several other default locations, with the TempLocation as last resort
 | |
| 	QStringList fileLocations =
 | |
| 		QStandardPaths::standardLocations(QStandardPaths::AppDataLocation) +
 | |
| 		QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation) +
 | |
| 		QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation) +
 | |
| 		QStandardPaths::standardLocations(QStandardPaths::DownloadLocation) +
 | |
| 		QStandardPaths::standardLocations(QStandardPaths::TempLocation);
 | |
| #elif defined(Q_OS_IOS)
 | |
| 	// on iOS we should save the data to the DocumentsLocation so it becomes accessible to the user
 | |
| 	QStringList fileLocations =
 | |
| 		QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation);
 | |
| #endif
 | |
| 	appLogFileOpen = false;
 | |
| 	for (const QString &fileLocation : fileLocations) {
 | |
| 		appLogFileName = fileLocation + "/subsurface.log";
 | |
| 		appLogFile.setFileName(appLogFileName);
 | |
| 		if (!appLogFile.open(QIODevice::ReadWrite|QIODevice::Truncate)) {
 | |
| 			appendTextToLog("Failed to open logfile " + appLogFileName
 | |
| 					+ " at " + QDateTime::currentDateTime().toString()
 | |
| 					+ " error: " + appLogFile.errorString());
 | |
| 		} else {
 | |
| 			// found a directory that works
 | |
| 			appLogFileOpen = true;
 | |
| 			break;
 | |
| 		}
 | |
| 	}
 | |
| 	if (appLogFileOpen) {
 | |
| 		appendTextToLog("Successfully opened logfile " + appLogFileName
 | |
| 				+ " at " + QDateTime::currentDateTime().toString());
 | |
| 		// if we were able to write the overall logfile, also write the libdivecomputer logfile
 | |
| 		QString libdcLogFileName = appLogFileName;
 | |
| 		libdcLogFileName = libdcLogFileName.replace("/subsurface.log", "/libdivecomputer.log");
 | |
| 		// remove the existing libdivecomputer logfile so we don't copy an old one by mistake
 | |
| 		QFile libdcLog(libdcLogFileName);
 | |
| 		libdcLog.remove();
 | |
| 		logfile_name = copy_qstring(libdcLogFileName);
 | |
| 	} else {
 | |
| 		appendTextToLog("No writeable location found, in-memory log only and no libdivecomputer log");
 | |
| 	}
 | |
| #endif
 | |
| 	set_error_cb(&showErrorFromC);
 | |
| 	uiNotificationCallback = showProgress;
 | |
| 	appendTextToLog("Starting " + getUserAgent());
 | |
| 	appendTextToLog(QStringLiteral("built with libdivecomputer v%1").arg(dc_version(NULL)));
 | |
| 	appendTextToLog(QStringLiteral("built with Qt Version %1, runtime from Qt Version %2").arg(QT_VERSION_STR).arg(qVersion()));
 | |
| 	int git_maj, git_min, git_rev;
 | |
| 	git_libgit2_version(&git_maj, &git_min, &git_rev);
 | |
| 	appendTextToLog(QStringLiteral("built with libgit2 %1.%2.%3").arg(git_maj).arg(git_min).arg(git_rev));
 | |
| 	appendTextToLog(QStringLiteral("Running on %1").arg(QSysInfo::prettyProductName()));
 | |
| #if defined(Q_OS_ANDROID)
 | |
| 	extern QString getAndroidHWInfo();
 | |
| 	appendTextToLog(getAndroidHWInfo());
 | |
| #endif
 | |
| 	if (ignore_bt) {
 | |
| 		m_btEnabled = false;
 | |
| 	} else {
 | |
| 		// ensure that we start the BTDiscovery - this should be triggered by the export of the class
 | |
| 		// to QML, but that doesn't seem to always work
 | |
| 		BTDiscovery *btDiscovery = BTDiscovery::instance();
 | |
| 		m_btEnabled = btDiscovery->btAvailable();
 | |
| 		connect(&btDiscovery->localBtDevice, &QBluetoothLocalDevice::hostModeStateChanged,
 | |
| 			this, &QMLManager::btHostModeChange);
 | |
| 	}
 | |
| 	progress_callback = &progressCallback;
 | |
| 	set_git_update_cb(&gitProgressCB);
 | |
| 
 | |
| 	// present dive site lists sorted by name
 | |
| 	locationModel.sort(LocationInformationModel::NAME);
 | |
| 
 | |
| 	// make sure we know if the current cloud repo has been successfully synced
 | |
| 	syncLoadFromCloud();
 | |
| 
 | |
| 	memset(&m_copyPasteDive, 0, sizeof(m_copyPasteDive));
 | |
| 	memset(&what, 0, sizeof(what));
 | |
| 
 | |
| 	// Let's set some defaults to be copied so users don't necessarily need
 | |
| 	// to know how to configure this
 | |
| 	what.diveguide = true;
 | |
| 	what.buddy = true;
 | |
| 	what.suit = true;
 | |
| 	what.tags = true;
 | |
| 	what.cylinders = true;
 | |
| 	what.weights = true;
 | |
| 
 | |
| 	// monitor when dives changed - but only in verbose mode
 | |
| 	// careful - changing verbose at runtime isn't enough (of course that could be added if we want it)
 | |
| 	if (verbose)
 | |
| 		connect(&diveListNotifier, &DiveListNotifier::divesChanged, this, &QMLManager::divesChanged);
 | |
| 
 | |
| 	// get updates to the undo/redo texts
 | |
| 	connect(Command::getUndoStack(), &QUndoStack::undoTextChanged, this, &QMLManager::undoTextChanged);
 | |
| 	connect(Command::getUndoStack(), &QUndoStack::redoTextChanged, this, &QMLManager::redoTextChanged);
 | |
| 
 | |
| 	// now that everything is setup, connect the application changed signal
 | |
| 	connect(qobject_cast<QApplication *>(QApplication::instance()), &QApplication::applicationStateChanged, this, &QMLManager::applicationStateChanged);
 | |
| 
 | |
| 	// we start out with clean data
 | |
| 	updateHaveLocalChanges(false);
 | |
| 
 | |
| 	// setup Command infrastructure
 | |
| 	Command::init();
 | |
| 	undoAction = Command::undoAction(this);
 | |
| }
 | |
| 
 | |
| void QMLManager::applicationStateChanged(Qt::ApplicationState state)
 | |
| {
 | |
| 	static bool initializeOnce = false;
 | |
| 	QString stateText;
 | |
| 	switch (state) {
 | |
| 	case Qt::ApplicationActive: stateText = "active"; break;
 | |
| 	case Qt::ApplicationHidden: stateText = "hidden"; break;
 | |
| 	case Qt::ApplicationSuspended: stateText = "suspended"; break;
 | |
| 	case Qt::ApplicationInactive: stateText = "inactive"; break;
 | |
| 	default: stateText = QString("none of the four: 0x") + QString::number(state, 16);
 | |
| 	}
 | |
| 	stateText.prepend("AppState changed to ");
 | |
| 	stateText.append(" with ");
 | |
| 	stateText.append((unsavedChanges() ? QLatin1String("") : QLatin1String("no ")) + QLatin1String("unsaved changes"));
 | |
| 	appendTextToLog(stateText);
 | |
| 
 | |
| 	if (state == Qt::ApplicationActive && !m_initialized && !initializeOnce) {
 | |
| 		// once the app UI is displayed, finish our setup and mark the app as initialized
 | |
| 		initializeOnce = true;
 | |
| 		finishSetup();
 | |
| 		appInitialized();
 | |
| 	}
 | |
| 	if (state == Qt::ApplicationInactive && unsavedChanges()) {
 | |
| 		// saveChangesCloud ensures that we don't have two conflicting saves going on
 | |
| 		appendTextToLog("trying to save data as user switched away from app");
 | |
| 		saveChangesCloud(false);
 | |
| 		appendTextToLog("done trying to save to git local / remote");
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void QMLManager::openLocalThenRemote(QString url)
 | |
| {
 | |
| 	// clear out the models and the fulltext index
 | |
| 	clear_dive_file_data();
 | |
| 	setDiveListProcessing(true);
 | |
| 	setNotificationText(tr("Open local dive data file"));
 | |
| 	appendTextToLog(QString("Open dive data file %1 - git_local only is %2").arg(url).arg(git_local_only));
 | |
| 	QByteArray encodedFilename = QFile::encodeName(url);
 | |
| 
 | |
| 	/* if this is a cloud storage repo and we have no local cache (i.e., it's the first time
 | |
| 	 * we try to open this), parse_file (which is called by openAndMaybeSync) will ALWAYS connect
 | |
| 	 * to the remote and populate the cache.
 | |
| 	 * Otherwise parse_file will respect the git_local_only flag and only update if that isn't set */
 | |
| 	int error = parse_file(encodedFilename.constData(), &divelog);
 | |
| 	if (error) {
 | |
| 		/* there can be 2 reasons for this:
 | |
| 		 * 1) we have cloud credentials, but there is no local repo (yet).
 | |
| 		 *    This implies that the PIN verify is still to be done.
 | |
| 		 * 2) we are in a very clean state after installing the app, and
 | |
| 		 *    want to use a NO CLOUD setup. The intial repo has no initial
 | |
| 		 *    commit in it, so its master branch does not yet exist. We do not
 | |
| 		 *    care about this, as the very first commit of dive data to the
 | |
| 		 *    no cloud repo solves this.
 | |
| 		 */
 | |
| 		auto credStatus = qPrefCloudStorage::cloud_verification_status();
 | |
| 		if (credStatus != qPrefCloudStorage::CS_NOCLOUD) {
 | |
| 			appendTextToLog(QStringLiteral("loading dives from cache failed %1").arg(error));
 | |
| 			setNotificationText(tr("Opening local data file failed"));
 | |
| 			if (credStatus != qPrefCloudStorage::CS_INCORRECT_USER_PASSWD) {
 | |
| 				qPrefCloudStorage::set_cloud_verification_status(qPrefCloudStorage::CS_NEED_TO_VERIFY);
 | |
| 				emit passwordStateChanged();
 | |
| 			}
 | |
| 		}
 | |
| 	} else {
 | |
| 		// if we can load from the cache, we know that we have a valid cloud account
 | |
| 		// and we know that there was at least one successful sync with the cloud when
 | |
| 		// that local cache was created - so there is a common ancestor
 | |
| 		setLoadFromCloud(true);
 | |
| 		if (qPrefCloudStorage::cloud_verification_status() == qPrefCloudStorage::CS_UNKNOWN) {
 | |
| 			qPrefCloudStorage::set_cloud_verification_status(qPrefCloudStorage::CS_VERIFIED);
 | |
| 			emit passwordStateChanged();
 | |
| 		}
 | |
| 		qPrefUnits::set_unit_system(git_prefs.unit_system);
 | |
| 		qPrefTechnicalDetails::set_tankbar(git_prefs.tankbar);
 | |
| 		qPrefTechnicalDetails::set_show_ccr_setpoint(git_prefs.show_ccr_setpoint);
 | |
| 		qPrefTechnicalDetails::set_show_ccr_sensors(git_prefs.show_ccr_sensors);
 | |
| 		qPrefPartialPressureGas::set_po2(git_prefs.pp_graphs.po2);
 | |
| 		// the following steps can take a long time, so provide updates
 | |
| 		setNotificationText(tr("Processing %1 dives").arg(divelog.dives->nr));
 | |
| 		process_loaded_dives();
 | |
| 		setNotificationText(tr("%1 dives loaded from local dive data file").arg(divelog.dives->nr));
 | |
| 	}
 | |
| 	if (qPrefCloudStorage::cloud_verification_status() == qPrefCloudStorage::CS_NEED_TO_VERIFY) {
 | |
| 		appendTextToLog(QStringLiteral("have cloud credentials, but still needs PIN"));
 | |
| 	}
 | |
| 	if (qPrefCloudStorage::cloud_verification_status() == qPrefCloudStorage::CS_INCORRECT_USER_PASSWD) {
 | |
| 		appendTextToLog(QStringLiteral("incorrect password for cloud credentials"));
 | |
| 		setNotificationText(tr("Incorrect cloud credentials"));
 | |
| 	}
 | |
| 	if (m_oldStatus == qPrefCloudStorage::CS_NOCLOUD) {
 | |
| 		// if we switch to credentials from CS_NOCLOUD, we take things online temporarily
 | |
| 		git_local_only = false;
 | |
| 		appendTextToLog(QStringLiteral("taking things online to be able to switch to cloud account"));
 | |
| 	}
 | |
| 	set_filename(encodedFilename.constData());
 | |
| 	if (git_local_only && qPrefCloudStorage::cloud_verification_status() != qPrefCloudStorage::CS_NOCLOUD)
 | |
| 		appendTextToLog(QStringLiteral("have cloud credentials, but user asked not to connect to network"));
 | |
| 
 | |
| 	// if we were unable to sync with the remote, we assume that there are local changes
 | |
| 	updateHaveLocalChanges(!git_remote_sync_successful);
 | |
| 	updateAllGlobalLists();
 | |
| 	setDiveListProcessing(false);
 | |
| 	// this could have added a new local cache directory
 | |
| 	emit cloudCacheListChanged();
 | |
| }
 | |
| 
 | |
| // Convenience function to accesss dive directly via its row.
 | |
| static struct dive *diveInRow(const QAbstractItemModel *model, int row)
 | |
| {
 | |
| 	QModelIndex index = model->index(row, 0, QModelIndex());
 | |
| 	return index.isValid() ?  model->data(index, DiveTripModelBase::DIVE_ROLE).value<struct dive *>() : nullptr;
 | |
| }
 | |
| 
 | |
| void QMLManager::selectRow(int row)
 | |
| {
 | |
| 	dive *d = diveInRow(MobileModels::instance()->listModel(), row);
 | |
| 	select_single_dive(d);
 | |
| }
 | |
| 
 | |
| void QMLManager::selectSwipeRow(int row)
 | |
| {
 | |
| 	dive *d = diveInRow(MobileModels::instance()->swipeModel(), row);
 | |
| 	select_single_dive(d);
 | |
| }
 | |
| 
 | |
| void QMLManager::updateAllGlobalLists()
 | |
| {
 | |
| 	emit buddyListChanged();
 | |
| 	emit suitListChanged();
 | |
| 	emit diveguideListChanged();
 | |
| 	// TODO: It would be nice if we could export the list of locations via model/view instead of a Q_PROPERTY
 | |
| 	emit locationListChanged();
 | |
| }
 | |
| 
 | |
| static QString nocloud_localstorage()
 | |
| {
 | |
| 	return QString(system_default_directory()) + "/cloudstorage/localrepo[master]";
 | |
| }
 | |
| 
 | |
| void QMLManager::mergeLocalRepo()
 | |
| {
 | |
| 	struct divelog log;
 | |
| 	parse_file(qPrintable(nocloud_localstorage()), &log);
 | |
| 	add_imported_dives(&log, IMPORT_MERGE_ALL_TRIPS);
 | |
| 	mark_divelist_changed(true);
 | |
| }
 | |
| 
 | |
| void QMLManager::copyAppLogToClipboard()
 | |
| {
 | |
| 	// The About page offers a button to copy logs so they can be pasted elsewhere
 | |
| 	QApplication::clipboard()->setText(getCombinedLogs(), QClipboard::Clipboard);
 | |
| }
 | |
| 
 | |
| bool QMLManager::createSupportEmail()
 | |
| {
 | |
| 	QString messageBody = "Please describe your issue here and keep the logs below:\n\n\n\n";
 | |
| #if defined(Q_OS_ANDROID)
 | |
| 	// let's use our nifty Java shareFile function
 | |
| 	QAndroidJniObject activity = QtAndroid::androidActivity();
 | |
| 	if (activity.isValid()) {
 | |
| 		QAndroidJniObject applogfilepath = QAndroidJniObject::fromString(appLogFileName);
 | |
| 		QAndroidJniObject libdcfilepath = QAndroidJniObject::fromString(logfile_name);
 | |
| 		bool success = activity.callMethod<jboolean>("supportEmail",
 | |
| 					"(Ljava/lang/String;Ljava/lang/String;)Z", // two string arguments, return bool
 | |
| 					applogfilepath.object<jstring>(), libdcfilepath.object<jstring>());
 | |
| 		qDebug() << __FUNCTION__ << "supportEmail" << (success ? "succeeded" : "failed");
 | |
| 		if (success)
 | |
| 			return true;
 | |
| 	}
 | |
| 	qDebug() << __FUNCTION__ << "failed to share the logFiles via intent, use the fall-back mail body method";
 | |
| #elif defined(Q_OS_IOS)
 | |
| 	// call into objC++ code to share on iOS
 | |
| 	QString libdcLogFileName(logfile_name);
 | |
| 	iosshare.supportEmail(appLogFileName, libdcLogFileName);
 | |
| 	// Unfortunately I haven't been able to figure out how to wait until the mail was sent
 | |
| 	// so that this could tell us whether this was successful or not
 | |
| 	// We always assume it worked and return to the caller.
 | |
| 	return true;
 | |
| #endif
 | |
| 	// fallback code that tries to copy the logs into the message body and uses the Qt send email method
 | |
| 	QString mailToLink = "mailto:in-app-support@subsurface-divelog.org?subject=Subsurface-mobile support request";
 | |
| 	mailToLink += "&body=";
 | |
| 	mailToLink += messageBody;
 | |
| 	mailToLink += getCombinedLogs();
 | |
| 	if (QDesktopServices::openUrl(QUrl(mailToLink))) {
 | |
| 		appendTextToLog("OS accepted support email");
 | |
| 		return true;
 | |
| 	}
 | |
| 	appendTextToLog("failed to create support email");
 | |
| 	return false;
 | |
| }
 | |
| 
 | |
| // useful for support requests
 | |
| QString QMLManager::getCombinedLogs()
 | |
| {
 | |
| 	// Add heading and append subsurface.log
 | |
| 	QString copyString = "\n---------- subsurface.log ----------\n";
 | |
| 	copyString += MessageHandlerModel::self()->logAsString();
 | |
| 
 | |
| 	// Add heading and append libdivecomputer.log
 | |
| 	QFile f(logfile_name);
 | |
| 	if (f.open(QFile::ReadOnly | QFile::Text)) {
 | |
| 		copyString += "\n\n\n---------- libdivecomputer.log ----------\n";
 | |
| 
 | |
| 		QTextStream in(&f);
 | |
| 		copyString += in.readAll();
 | |
| 	}
 | |
| 
 | |
| 	copyString += "---------- finish ----------\n";
 | |
| 
 | |
| #if defined(Q_OS_ANDROID)
 | |
| 	// on Android, the clipboard is effectively limited in size, but there is no
 | |
| 	// predefined hard limit. All remote procedure calls use a shared Binder
 | |
| 	// transaction buffer that is limited to 1MB. To work around this let's truncate
 | |
| 	// the log once it is more than half a million characters. Qt doesn't tell us if
 | |
| 	// the clipboard transaction fails, hopefully this will typically leave enough
 | |
| 	// margin of error.
 | |
| 	if (copyString.size() > 500000) {
 | |
| 		copyString.truncate(500000);
 | |
| 		copyString += "\n\n---------- truncated ----------\n";
 | |
| 	}
 | |
| #endif
 | |
| 	return copyString;
 | |
| }
 | |
| 
 | |
| void QMLManager::finishSetup()
 | |
| {
 | |
| 	appendTextToLog("finishSetup called");
 | |
| 	// Initialize cloud credentials.
 | |
| 	git_local_only = !prefs.cloud_auto_sync;
 | |
| 
 | |
| 	QString url;
 | |
| 	if (!qPrefCloudStorage::cloud_storage_email().isEmpty() &&
 | |
| 	    !qPrefCloudStorage::cloud_storage_password().isEmpty() &&
 | |
| 	    getCloudURL(url) == 0) {
 | |
| 		openLocalThenRemote(url);
 | |
| 	} else if (!empty_string(existing_filename) &&
 | |
| 		   qPrefCloudStorage::cloud_verification_status() != qPrefCloudStorage::CS_UNKNOWN) {
 | |
| 		rememberOldStatus();
 | |
| 		set_filename(qPrintable(nocloud_localstorage()));
 | |
| 		qPrefCloudStorage::set_cloud_verification_status(qPrefCloudStorage::CS_NOCLOUD);
 | |
| 		emit passwordStateChanged();
 | |
| 		saveCloudCredentials(qPrefCloudStorage::cloud_storage_email(), qPrefCloudStorage::cloud_storage_password(), qPrefCloudStorage::cloud_storage_pin());
 | |
| 		appendTextToLog(tr("working in no-cloud mode"));
 | |
| 		int error = parse_file(existing_filename, &divelog);
 | |
| 		if (error) {
 | |
| 			// we got an error loading the local file
 | |
| 			setNotificationText(tr("Error parsing local storage, giving up"));
 | |
| 			set_filename(NULL);
 | |
| 		} else {
 | |
| 			// successfully opened the local file, now add thigs to the dive list
 | |
| 			consumeFinishedLoad();
 | |
| 			updateHaveLocalChanges(true);
 | |
| 			appendTextToLog(QString("working in no-cloud mode, finished loading %1 dives from %2").arg(divelog.dives->nr).arg(existing_filename));
 | |
| 		}
 | |
| 	} else {
 | |
| 		qPrefCloudStorage::set_cloud_verification_status(qPrefCloudStorage::CS_UNKNOWN);
 | |
| 		emit passwordStateChanged();
 | |
| 		appendTextToLog(tr("no cloud credentials"));
 | |
| 		setStartPageText(RED_FONT + tr("Please enter valid cloud credentials.") + END_FONT);
 | |
| 	}
 | |
| 	m_initialized = true;
 | |
| 	emit initializedChanged();
 | |
| 	// this could have brought in new cache directories, so make sure QML
 | |
| 	// calls our getter function again and doesn't show us outdated information
 | |
| 	emit cloudCacheListChanged();
 | |
| }
 | |
| 
 | |
| QMLManager::~QMLManager()
 | |
| {
 | |
| #if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
 | |
| 	if (appLogFileOpen)
 | |
| 		appLogFile.close();
 | |
| #endif
 | |
| 	m_instance = NULL;
 | |
| }
 | |
| 
 | |
| QMLManager *QMLManager::instance()
 | |
| {
 | |
| 	return m_instance;
 | |
| }
 | |
| 
 | |
| #define CLOUDURL QString(prefs.cloud_base_url)
 | |
| #define CLOUDREDIRECTURL CLOUDURL + "/cgi-bin/redirect.pl"
 | |
| 
 | |
| void QMLManager::saveCloudCredentials(const QString &newEmail, const QString &newPassword, const QString &pin)
 | |
| {
 | |
| 	bool cloudCredentialsChanged = false;
 | |
| 	bool noCloud = qPrefCloudStorage::cloud_verification_status() == qPrefCloudStorage::CS_NOCLOUD;
 | |
| 
 | |
| 	// email address MUST be lower case or bad things happen
 | |
| 	QString email = newEmail.toLower();
 | |
| 
 | |
| 	// make sure we only have letters, numbers, and +-_. in password and email address
 | |
| 	QRegularExpression regExp("^[a-zA-Z0-9@.+_-]+$");
 | |
| 	if (!noCloud) {
 | |
| 		// in case of NO_CLOUD, the email address + passwd do not care, so do not check it.
 | |
| 		if (newPassword.isEmpty() ||
 | |
| 			!regExp.match(newPassword).hasMatch() ||
 | |
| 			!regExp.match(email).hasMatch()) {
 | |
| 			setStartPageText(RED_FONT + tr("Cloud storage email and password can only consist of letters, numbers, and '.', '-', '_', and '+'.") + END_FONT);
 | |
| 			return;
 | |
| 		}
 | |
| 		// use the same simplistic regex as the backend to check email addresses
 | |
| 		regExp = QRegularExpression("^[a-zA-Z0-9.+_-]+@[a-zA-Z0-9.+_-]+\\.[a-zA-Z0-9]+");
 | |
| 		if (!regExp.match(email).hasMatch()) {
 | |
| 			setStartPageText(RED_FONT + tr("Invalid format for email address") + END_FONT);
 | |
| 			return;
 | |
| 		}
 | |
| 	}
 | |
| 	if (!same_string(prefs.cloud_storage_email, qPrintable(email))) {
 | |
| 		cloudCredentialsChanged = true;
 | |
| 	}
 | |
| 
 | |
| 	if (!same_string(prefs.cloud_storage_password, qPrintable(newPassword))) {
 | |
| 		cloudCredentialsChanged = true;
 | |
| 	}
 | |
| 
 | |
| 	if (qPrefCloudStorage::cloud_verification_status() != qPrefCloudStorage::CS_NOCLOUD &&
 | |
| 		!cloudCredentialsChanged) {
 | |
| 		// just go back to the dive list
 | |
| 		qPrefCloudStorage::set_cloud_verification_status(m_oldStatus);
 | |
| 		emit passwordStateChanged();
 | |
| 	}
 | |
| 
 | |
| 	if (!noCloud && !verifyCredentials(email, newPassword, pin)) {
 | |
| 		appendTextToLog("saveCloudCredentials: given cloud credentials didn't verify");
 | |
| 		return;
 | |
| 	}
 | |
| 	qPrefCloudStorage::set_cloud_storage_email(email);
 | |
| 	qPrefCloudStorage::set_cloud_storage_password(newPassword);
 | |
| 
 | |
| 	if (m_oldStatus == qPrefCloudStorage::CS_NOCLOUD && cloudCredentialsChanged && divelog.dives->nr) {
 | |
| 		// we came from NOCLOUD and are connecting to a cloud account;
 | |
| 		// since we already have dives in the table, let's remember that so we can keep them
 | |
| 		noCloudToCloud = true;
 | |
| 		appendTextToLog("transitioning from no-cloud to cloud and have dives");
 | |
| 	}
 | |
| 	if (qPrefCloudStorage::cloud_storage_email().isEmpty() ||
 | |
| 		qPrefCloudStorage::cloud_storage_password().isEmpty()) {
 | |
| 		setStartPageText(RED_FONT + tr("Please enter valid cloud credentials.") + END_FONT);
 | |
| 	} else if (cloudCredentialsChanged) {
 | |
| 		// let's make sure there are no unsaved changes
 | |
| 		saveChangesLocal();
 | |
| 		syncLoadFromCloud();
 | |
| 		manager()->clearAccessCache(); // remove any chached credentials
 | |
| 		clear_git_id(); // invalidate our remembered GIT SHA
 | |
| 		clear_dive_file_data();
 | |
| 		setStartPageText(tr("Attempting to open cloud storage with new credentials"));
 | |
| 		// since we changed credentials, we need to try to connect to the cloud, regardless
 | |
| 		// of whether we're in offline mode or not, to make sure the repository is synced
 | |
| 		currentGitLocalOnly = git_local_only;
 | |
| 		git_local_only = false;
 | |
| 		loadDivesWithValidCredentials();
 | |
| 	}
 | |
| 	rememberOldStatus();
 | |
| }
 | |
| 
 | |
| bool QMLManager::verifyCredentials(QString email, QString password, QString pin)
 | |
| {
 | |
| 	setStartPageText(tr("Testing cloud credentials"));
 | |
| 	if (pin.isEmpty())
 | |
| 		appendTextToLog(QStringLiteral("verify credentials for email %1 (no PIN)").arg(email));
 | |
| 	else
 | |
| 		appendTextToLog(QStringLiteral("verify credentials for email %1 PIN %2").arg(email, pin));
 | |
| 	CloudStorageAuthenticate *csa = new CloudStorageAuthenticate(this);
 | |
| 	csa->backend(email, password, pin);
 | |
| 	// let's wait here for the signal to avoid too many more nested functions
 | |
| 	QTimer myTimer;
 | |
| 	myTimer.setSingleShot(true);
 | |
| 	QEventLoop loop;
 | |
| 	connect(csa, &CloudStorageAuthenticate::finishedAuthenticate, &loop, &QEventLoop::quit);
 | |
| 	connect(&myTimer, &QTimer::timeout, &loop, &QEventLoop::quit);
 | |
| 	myTimer.start(prefs.cloud_timeout * 1000);
 | |
| 	loop.exec();
 | |
| 	if (!myTimer.isActive()) {
 | |
| 		// got no response from the server
 | |
| 		setStartPageText(RED_FONT + tr("No response from cloud server to validate the credentials") + END_FONT);
 | |
| 		return false;
 | |
| 	}
 | |
| 	myTimer.stop();
 | |
| 	if (prefs.cloud_verification_status == qPrefCloudStorage::CS_INCORRECT_USER_PASSWD) {
 | |
| 		appendTextToLog(QStringLiteral("Incorrect email / password combination"));
 | |
| 		setStartPageText(RED_FONT + tr("Incorrect email / password combination") + END_FONT);
 | |
| 		return false;
 | |
| 	} else if (prefs.cloud_verification_status == qPrefCloudStorage::CS_NEED_TO_VERIFY) {
 | |
| 		if (pin.isEmpty()) {
 | |
| 			appendTextToLog(QStringLiteral("Cloud credentials require PIN entry"));
 | |
| 			setStartPageText(RED_FONT + tr("Cloud credentials require verification PIN") + END_FONT);
 | |
| 		} else {
 | |
| 			appendTextToLog(QStringLiteral("PIN provided but not accepted"));
 | |
| 			setStartPageText(RED_FONT + tr("Incorrect PIN, please try again") + END_FONT);
 | |
| 		}
 | |
| 		return false;
 | |
| 	} else if (prefs.cloud_verification_status == qPrefCloudStorage::CS_VERIFIED) {
 | |
| 		appendTextToLog(QStringLiteral("PIN accepted"));
 | |
| 		setStartPageText(RED_FONT + tr("PIN accepted, credentials verified") + END_FONT);
 | |
| 	}
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| void QMLManager::deleteAccount()
 | |
| {
 | |
| 	QString email(prefs.cloud_storage_email);
 | |
| 	QString passwd(prefs.cloud_storage_password);
 | |
| 	if (email.isEmpty() || passwd.isEmpty())
 | |
| 		return;
 | |
| 
 | |
| 	setStartPageText(tr("Deleting cloud account..."));
 | |
| 	appendTextToLog(QStringLiteral("user requested that we delete cloud account for email %1").arg(email));
 | |
| 	CloudStorageAuthenticate *csa = new CloudStorageAuthenticate(this);
 | |
| 	csa->deleteAccount(email, passwd);
 | |
| 	// let's wait here for the signal to avoid too many more nested functions
 | |
| 	QTimer myTimer;
 | |
| 	myTimer.setSingleShot(true);
 | |
| 	QEventLoop loop;
 | |
| 	connect(csa, &CloudStorageAuthenticate::finishedDelete, &loop, &QEventLoop::quit);
 | |
| 	connect(&myTimer, &QTimer::timeout, &loop, &QEventLoop::quit);
 | |
| 	myTimer.start(prefs.cloud_timeout * 3 * 1000); // give it extra time
 | |
| 	loop.exec();
 | |
| 	if (!myTimer.isActive()) {
 | |
| 		// got no response from the server
 | |
| 		setStartPageText(RED_FONT + tr("No response from cloud server to delete account") + END_FONT);
 | |
| 		appendTextToLog(QStringLiteral("no response from cloud server to delete account"));
 | |
| 		return;
 | |
| 	}
 | |
| 	myTimer.stop();
 | |
| 	appendTextToLog(QStringLiteral("deleted the account"));
 | |
| 	qPrefCloudStorage::set_cloud_storage_email("");
 | |
| 	qPrefCloudStorage::set_cloud_storage_email_encoded("");
 | |
| 	qPrefCloudStorage::set_cloud_storage_password("");
 | |
| 	qPrefCloudStorage::set_cloud_verification_status(qPrefCloudStorage::CS_NOCLOUD);
 | |
| 	emit passwordStateChanged();
 | |
| 	set_filename(qPrintable(nocloud_localstorage()));
 | |
| 	setStartPageText(tr("Cloud storage account deleted."));
 | |
| 	return;
 | |
| }
 | |
| 
 | |
| void QMLManager::loadDivesWithValidCredentials()
 | |
| {
 | |
| 	QString url;
 | |
| 	if (getCloudURL(url)) {
 | |
| 		setStartPageText(RED_FONT + tr("Cloud storage error: %1").arg(consumeError()) + END_FONT);
 | |
| 		revertToNoCloudIfNeeded();
 | |
| 		return;
 | |
| 	}
 | |
| 	QByteArray fileNamePrt = QFile::encodeName(url);
 | |
| 	struct git_info info;
 | |
| 	int error;
 | |
| 
 | |
| 	if (remote_repo_uptodate(fileNamePrt.data(), &info)) {
 | |
| 		appendTextToLog("Cloud sync shows local cache was current");
 | |
| 	} else {
 | |
| 		appendTextToLog("Cloud sync brought newer data, reloading the dive list");
 | |
| 		setDiveListProcessing(true);
 | |
| 		// if we aren't switching from no-cloud mode, let's clear the dive data
 | |
| 		if (!noCloudToCloud) {
 | |
| 			appendTextToLog("Clear out in memory dive data");
 | |
| 			clear_dive_file_data();
 | |
| 		} else {
 | |
| 			appendTextToLog("Switching from no cloud mode; keep in memory dive data");
 | |
| 		}
 | |
| 		if (info.repo) {
 | |
| 			appendTextToLog(QString("have repository and branch %1").arg(info.branch));
 | |
| 			error = git_load_dives(&info, &divelog);
 | |
| 		} else {
 | |
| 			appendTextToLog(QString("didn't receive valid git repo, try again"));
 | |
| 			error = parse_file(fileNamePrt.data(), &divelog);
 | |
| 		}
 | |
| 		setDiveListProcessing(false);
 | |
| 		if (!error) {
 | |
| 			report_error("filename is now %s", fileNamePrt.data());
 | |
| 			set_filename(fileNamePrt.data());
 | |
| 		} else {
 | |
| 			report_error("failed to open file %s", fileNamePrt.data());
 | |
| 			setNotificationText(consumeError());
 | |
| 			revertToNoCloudIfNeeded();
 | |
| 			set_filename(NULL);
 | |
| 			return;
 | |
| 		}
 | |
| 		consumeFinishedLoad();
 | |
| 	}
 | |
| 	cleanup_git_info(&info);
 | |
| 
 | |
| 	setLoadFromCloud(true);
 | |
| 
 | |
| 	// if we came from local storage mode, let's merge the local data into the local cache
 | |
| 	// for the remote data - which then later gets merged with the remote data if necessary
 | |
| 	if (noCloudToCloud) {
 | |
| 		git_storage_update_progress(qPrintable(tr("Loading dives from local storage ('no cloud' mode)")));
 | |
| 		mergeLocalRepo();
 | |
| 		appendTextToLog(QStringLiteral("%1 dives loaded after importing nocloud local storage").arg(divelog.dives->nr));
 | |
| 		noCloudToCloud = false;
 | |
| 		mark_divelist_changed(true);
 | |
| 		emit syncStateChanged();
 | |
| 		saveChangesLocal();
 | |
| 		if (git_local_only == false) {
 | |
| 			appendTextToLog(QStringLiteral("taking things back offline now that storage is synced"));
 | |
| 			git_local_only = true;
 | |
| 		}
 | |
| 	}
 | |
| 	// if we successfully synced with the cloud, update that status
 | |
| 	updateHaveLocalChanges(!git_remote_sync_successful);
 | |
| 	// if we got here just for an initial connection to the cloud, reset to offline
 | |
| 	if (currentGitLocalOnly) {
 | |
| 		currentGitLocalOnly = false;
 | |
| 		git_local_only = true;
 | |
| 	}
 | |
| 	return;
 | |
| }
 | |
| 
 | |
| void QMLManager::revertToNoCloudIfNeeded()
 | |
| {
 | |
| 	if (currentGitLocalOnly) {
 | |
| 		// we tried to connect to the cloud for the first time and that failed
 | |
| 		currentGitLocalOnly = false;
 | |
| 		git_local_only = true;
 | |
| 	}
 | |
| 	if (m_oldStatus == qPrefCloudStorage::CS_NOCLOUD) {
 | |
| 		// we tried to switch to a cloud account and had previously used local data,
 | |
| 		// but connecting to the cloud account (and subsequently merging the local
 | |
| 		// and cloud data) failed - so let's delete the cloud credentials and go
 | |
| 		// back to CS_NOCLOUD mode in order to prevent us from losing the locally stored
 | |
| 		// dives
 | |
| 		if (git_local_only == true) {
 | |
| 			appendTextToLog(QStringLiteral("taking things back offline since sync with cloud failed"));
 | |
| 			git_local_only = false;
 | |
| 		}
 | |
| 		free((void *)prefs.cloud_storage_email);
 | |
| 		prefs.cloud_storage_email = NULL;
 | |
| 		free((void *)prefs.cloud_storage_password);
 | |
| 		prefs.cloud_storage_password = NULL;
 | |
| 		qPrefCloudStorage::set_cloud_storage_email("");
 | |
| 		qPrefCloudStorage::set_cloud_storage_password("");
 | |
| 		rememberOldStatus();
 | |
| 		qPrefCloudStorage::set_cloud_verification_status(qPrefCloudStorage::CS_NOCLOUD);
 | |
| 		emit passwordStateChanged();
 | |
| 		set_filename(qPrintable(nocloud_localstorage()));
 | |
| 		setStartPageText(RED_FONT + tr("Failed to connect to cloud server, reverting to no cloud status") + END_FONT);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void QMLManager::consumeFinishedLoad()
 | |
| {
 | |
| 	prefs.unit_system = git_prefs.unit_system;
 | |
| 	if (git_prefs.unit_system == IMPERIAL)
 | |
| 		git_prefs.units = IMPERIAL_units;
 | |
| 	else if (git_prefs.unit_system == METRIC)
 | |
| 		git_prefs.units = SI_units;
 | |
| 	prefs.units = git_prefs.units;
 | |
| 	prefs.tankbar = git_prefs.tankbar;
 | |
| 	prefs.show_ccr_setpoint = git_prefs.show_ccr_setpoint;
 | |
| 	prefs.show_ccr_sensors = git_prefs.show_ccr_sensors;
 | |
| 	prefs.pp_graphs.po2 = git_prefs.pp_graphs.po2;
 | |
| 	process_loaded_dives();
 | |
| 	appendTextToLog(QStringLiteral("%1 dives loaded").arg(divelog.dives->nr));
 | |
| 	if (divelog.dives->nr == 0)
 | |
| 		setStartPageText(tr("Cloud storage open successfully. No dives in dive list."));
 | |
| }
 | |
| 
 | |
| void QMLManager::refreshDiveList()
 | |
| {
 | |
| 	MobileModels::instance()->invalidate();
 | |
| }
 | |
| 
 | |
| // Ouch. Editing a dive might create a dive site or change an existing dive site.
 | |
| // The following structure describes such a change caused by a dive edit.
 | |
| // Hopefully, we can remove this in due course by using finer-grained undo-commands.
 | |
| struct DiveSiteChange {
 | |
| 	OwningDiveSitePtr createdDs; // not-null if we created a dive site.
 | |
| 
 | |
| 	dive_site *editDs = nullptr; // not-null if we are supposed to edit an existing dive site.
 | |
| 	location_t location = zero_location; // new value of the location if we edit an existing dive site.
 | |
| 
 | |
| 	bool changed = false; // true if either a dive site or the dive was changed.
 | |
| };
 | |
| 
 | |
| static void setupDivesite(DiveSiteChange &res, struct dive *d, struct dive_site *ds, double lat, double lon, const char *locationtext)
 | |
| {
 | |
| 	location_t location = create_location(lat, lon);
 | |
| 	if (ds) {
 | |
| 		res.editDs = ds;
 | |
| 		res.location = location;
 | |
| 	} else {
 | |
| 		res.createdDs.reset(alloc_dive_site_with_name(locationtext));
 | |
| 		res.createdDs->location = location;
 | |
| 		d->dive_site = res.createdDs.get();
 | |
| 	}
 | |
| 	res.changed = true;
 | |
| }
 | |
| 
 | |
| bool QMLManager::checkDate(struct dive *d, QString date)
 | |
| {
 | |
| 	QString oldDate = formatDiveDateTime(d);
 | |
| 	if (date != oldDate) {
 | |
| 		QDateTime newDate;
 | |
| 		// what a pain - Qt will not parse dates if the day of the week is incorrect
 | |
| 		// so if the user changed the date but didn't update the day of the week (most likely behavior, actually),
 | |
| 		// we need to make sure we don't try to parse that
 | |
| 		QString format(QString(prefs.date_format_short) + QChar(' ') + prefs.time_format);
 | |
| 		if (format.contains(QLatin1String("ddd")) || format.contains(QLatin1String("dddd"))) {
 | |
| 			QString dateFormatToDrop = format.contains(QLatin1String("ddd")) ? QStringLiteral("ddd") : QStringLiteral("dddd");
 | |
| 			QDateTime ts;
 | |
| 			QLocale loc = getLocale();
 | |
| 			ts.setMSecsSinceEpoch(d->when * 1000L);
 | |
| 			QString drop = loc.toString(ts.toUTC(), dateFormatToDrop);
 | |
| 			format.replace(dateFormatToDrop, "");
 | |
| 			date.replace(drop, "");
 | |
| 		}
 | |
| 		// set date from string and make sure it's treated as UTC (like all our time stamps)
 | |
| 		newDate = QDateTime::fromString(date, format);
 | |
| 		newDate.setTimeSpec(Qt::UTC);
 | |
| 		if (!newDate.isValid()) {
 | |
| 			appendTextToLog("unable to parse date " + date + " with the given format " + format);
 | |
| 			QRegularExpression isoDate("\\d+-\\d+-\\d+[^\\d]+\\d+:\\d+");
 | |
| 			if (date.contains(isoDate)) {
 | |
| 				newDate = QDateTime::fromString(date, "yyyy-M-d h:m:s");
 | |
| 				if (newDate.isValid())
 | |
| 					goto parsed;
 | |
| 				newDate = QDateTime::fromString(date, "yy-M-d h:m:s");
 | |
| 				if (newDate.isValid())
 | |
| 					goto parsed;
 | |
| 			}
 | |
| 			QRegularExpression isoDateNoSecs("\\d+-\\d+-\\d+[^\\d]+\\d+");
 | |
| 			if (date.contains(isoDateNoSecs)) {
 | |
| 				newDate = QDateTime::fromString(date, "yyyy-M-d h:m");
 | |
| 				if (newDate.isValid())
 | |
| 					goto parsed;
 | |
| 				newDate = QDateTime::fromString(date, "yy-M-d h:m");
 | |
| 				if (newDate.isValid())
 | |
| 					goto parsed;
 | |
| 			}
 | |
| 			QRegularExpression usDate("\\d+/\\d+/\\d+[^\\d]+\\d+:\\d+:\\d+");
 | |
| 			if (date.contains(usDate)) {
 | |
| 				newDate = QDateTime::fromString(date, "M/d/yyyy h:m:s");
 | |
| 				if (newDate.isValid())
 | |
| 					goto parsed;
 | |
| 				newDate = QDateTime::fromString(date, "M/d/yy h:m:s");
 | |
| 				if (newDate.isValid())
 | |
| 					goto parsed;
 | |
| 				newDate = QDateTime::fromString(date.toLower(), "M/d/yyyy h:m:sap");
 | |
| 				if (newDate.isValid())
 | |
| 					goto parsed;
 | |
| 				newDate = QDateTime::fromString(date.toLower(), "M/d/yy h:m:sap");
 | |
| 				if (newDate.isValid())
 | |
| 					goto parsed;
 | |
| 			}
 | |
| 			QRegularExpression usDateNoSecs("\\d+/\\d+/\\d+[^\\d]+\\d+:\\d+");
 | |
| 			if (date.contains(usDateNoSecs)) {
 | |
| 				newDate = QDateTime::fromString(date, "M/d/yyyy h:m");
 | |
| 				if (newDate.isValid())
 | |
| 					goto parsed;
 | |
| 				newDate = QDateTime::fromString(date, "M/d/yy h:m");
 | |
| 				if (newDate.isValid())
 | |
| 					goto parsed;
 | |
| 				newDate = QDateTime::fromString(date.toLower(), "M/d/yyyy h:map");
 | |
| 				if (newDate.isValid())
 | |
| 					goto parsed;
 | |
| 				newDate = QDateTime::fromString(date.toLower(), "M/d/yy h:map");
 | |
| 				if (newDate.isValid())
 | |
| 					goto parsed;
 | |
| 			}
 | |
| 			QRegularExpression leDate("\\d+\\.\\d+\\.\\d+[^\\d]+\\d+:\\d+:\\d+");
 | |
| 			if (date.contains(leDate)) {
 | |
| 				newDate = QDateTime::fromString(date, "d.M.yyyy h:m:s");
 | |
| 				if (newDate.isValid())
 | |
| 					goto parsed;
 | |
| 				newDate = QDateTime::fromString(date, "d.M.yy h:m:s");
 | |
| 				if (newDate.isValid())
 | |
| 					goto parsed;
 | |
| 			}
 | |
| 			QRegularExpression leDateNoSecs("\\d+\\.\\d+\\.\\d+[^\\d]+\\d+:\\d+");
 | |
| 			if (date.contains(leDateNoSecs)) {
 | |
| 				newDate = QDateTime::fromString(date, "d.M.yyyy h:m");
 | |
| 				if (newDate.isValid())
 | |
| 					goto parsed;
 | |
| 				newDate = QDateTime::fromString(date, "d.M.yy h:m");
 | |
| 				if (newDate.isValid())
 | |
| 					goto parsed;
 | |
| 			}
 | |
| 			// if everything else failed - maybe the user is entering dives where they
 | |
| 			// don't recall the time? So let's try date only patterns...
 | |
| 			QRegularExpression usDateOnly("\\d+/\\d+/\\d+");
 | |
| 			if (date.contains(usDateOnly)) {
 | |
| 				newDate = QDateTime::fromString(date, "M/d/yy");
 | |
| 				if (newDate.isValid())
 | |
| 					goto parsed;
 | |
| 				newDate = QDateTime::fromString(date, "M/d/yyyy");
 | |
| 				if (newDate.isValid())
 | |
| 					goto parsed;
 | |
| 			}
 | |
| 			QRegularExpression leDateOnly("\\d+\\.\\d+\\.\\d+");
 | |
| 			if (date.contains(usDateOnly)) {
 | |
| 				newDate = QDateTime::fromString(date, "d.M.yy");
 | |
| 				if (newDate.isValid())
 | |
| 					goto parsed;
 | |
| 				newDate = QDateTime::fromString(date, "d.M.yyyy");
 | |
| 				if (newDate.isValid())
 | |
| 					goto parsed;
 | |
| 			}
 | |
| 			QRegularExpression isoDateOnly("\\d+-\\d+-\\d+");
 | |
| 			if (date.contains(usDateOnly)) {
 | |
| 				newDate = QDateTime::fromString(date, "yy-M-d");
 | |
| 				if (newDate.isValid())
 | |
| 					goto parsed;
 | |
| 				newDate = QDateTime::fromString(date, "yyyy-M-d");
 | |
| 				if (newDate.isValid())
 | |
| 					goto parsed;
 | |
| 			}
 | |
| 		}
 | |
| parsed:
 | |
| 		if (newDate.isValid()) {
 | |
| 			// stupid Qt... two digit years are always 19xx - WTF???
 | |
| 			// so if adding a hundred years gets you into something before a year from now...
 | |
| 			// add a hundred years.
 | |
| 			if (newDate.addYears(100) < QDateTime::currentDateTime().addYears(1))
 | |
| 				newDate = newDate.addYears(100);
 | |
| 			d->dc.when = d->when = dateTimeToTimestamp(newDate);
 | |
| 			return true;
 | |
| 		}
 | |
| 		appendTextToLog("none of our parsing attempts worked for the date string");
 | |
| 	}
 | |
| 	return false;
 | |
| }
 | |
| 
 | |
| bool QMLManager::checkLocation(DiveSiteChange &res, struct dive *d, QString location, QString gps)
 | |
| {
 | |
| 	struct dive_site *ds = get_dive_site_for_dive(d);
 | |
| 	bool changed = false;
 | |
| 	QString oldLocation = get_dive_location(d);
 | |
| 	if (oldLocation != location) {
 | |
| 		ds = get_dive_site_by_name(qPrintable(location), divelog.sites);
 | |
| 		if (!ds && !location.isEmpty()) {
 | |
| 			res.createdDs.reset(alloc_dive_site_with_name(qPrintable(location)));
 | |
| 			res.changed = true;
 | |
| 			ds = res.createdDs.get();
 | |
| 		}
 | |
| 		d->dive_site = ds;
 | |
| 		changed = true;
 | |
| 	}
 | |
| 	// now make sure that the GPS coordinates match - if the user changed the name but not
 | |
| 	// the GPS coordinates, this still does the right thing as the now new dive site will
 | |
| 	// have no coordinates, so the coordinates from the edit screen will get added
 | |
| 	if (formatDiveGPS(d) != gps) {
 | |
| 		double lat, lon;
 | |
| 		if (parseGpsText(gps, &lat, &lon)) {
 | |
| 			if (location.isEmpty())
 | |
| 				location = gps;
 | |
| 			if (verbose)
 | |
| 				qDebug() << "parsed GPS" << gps << ", using it for dive site" << location;
 | |
| 			// there are valid GPS coordinates - just use them
 | |
| 			setupDivesite(res, d, ds, lat, lon, qPrintable(location));
 | |
| 		} else {
 | |
| 			// just something we can't parse, so tell the user
 | |
| 			appendTextToLog(QString("wasn't able to parse gps string '%1'").arg(gps));
 | |
| 		}
 | |
| 	}
 | |
| 	return changed | res.changed;
 | |
| }
 | |
| 
 | |
| bool QMLManager::checkDuration(struct dive *d, QString duration)
 | |
| {
 | |
| 	if (formatDiveDuration(d) != duration) {
 | |
| 		int h = 0, m = 0, s = 0;
 | |
| 		QRegularExpression r1(QStringLiteral("(\\d*)\\s*%1[\\s,:]*(\\d*)\\s*%2[\\s,:]*(\\d*)\\s*%3").arg(tr("h")).arg(tr("min")).arg(tr("sec")), QRegularExpression::CaseInsensitiveOption);
 | |
| 		QRegularExpressionMatch m1 = r1.match(duration);
 | |
| 		QRegularExpression r2(QStringLiteral("(\\d*)\\s*%1[\\s,:]*(\\d*)\\s*%2").arg(tr("h")).arg(tr("min")), QRegularExpression::CaseInsensitiveOption);
 | |
| 		QRegularExpressionMatch m2 = r2.match(duration);
 | |
| 		QRegularExpression r3(QStringLiteral("(\\d*)\\s*%1").arg(tr("min")), QRegularExpression::CaseInsensitiveOption);
 | |
| 		QRegularExpressionMatch m3 = r3.match(duration);
 | |
| 		QRegularExpression r4(QStringLiteral("(\\d*):(\\d*):(\\d*)"));
 | |
| 		QRegularExpressionMatch m4 = r4.match(duration);
 | |
| 		QRegularExpression r5(QStringLiteral("(\\d*):(\\d*)"));
 | |
| 		QRegularExpressionMatch m5 = r5.match(duration);
 | |
| 		QRegularExpression r6(QStringLiteral("(\\d*)"));
 | |
| 		QRegularExpressionMatch m6 = r6.match(duration);
 | |
| 		if (m1.hasMatch()) {
 | |
| 			h = m1.captured(1).toInt();
 | |
| 			m = m1.captured(2).toInt();
 | |
| 			s = m1.captured(3).toInt();
 | |
| 		} else if (m2.hasMatch()) {
 | |
| 			h = m2.captured(1).toInt();
 | |
| 			m = m2.captured(2).toInt();
 | |
| 		} else if (m3.hasMatch()) {
 | |
| 			m = m3.captured(1).toInt();
 | |
| 		} else if (m4.hasMatch()) {
 | |
| 			h = m4.captured(1).toInt();
 | |
| 			m = m4.captured(2).toInt();
 | |
| 			s = m4.captured(3).toInt();
 | |
| 		} else if (m5.hasMatch()) {
 | |
| 			h = m5.captured(1).toInt();
 | |
| 			m = m5.captured(2).toInt();
 | |
| 		} else if (m6.hasMatch()) {
 | |
| 			m = m6.captured(1).toInt();
 | |
| 		}
 | |
| 		d->dc.duration.seconds = d->duration.seconds = h * 3600 + m * 60 + s;
 | |
| 		if (is_manually_added_dc(&d->dc))
 | |
| 			free_samples(&d->dc);
 | |
| 		else
 | |
| 			appendTextToLog("Cannot change the duration on a dive that wasn't manually added");
 | |
| 		return true;
 | |
| 	}
 | |
| 	return false;
 | |
| }
 | |
| 
 | |
| bool QMLManager::checkDepth(dive *d, QString depth)
 | |
| {
 | |
| 	if (get_depth_string(d->dc.maxdepth.mm, true, true) != depth) {
 | |
| 		int depthValue = parseLengthToMm(depth);
 | |
| 		// the QML code should stop negative depth, but massively huge depth can make
 | |
| 		// the profile extremely slow or even run out of memory and crash, so keep
 | |
| 		// the depth <= 500m
 | |
| 		if (0 <= depthValue && depthValue <= 500000) {
 | |
| 			d->maxdepth.mm = depthValue;
 | |
| 			if (is_manually_added_dc(&d->dc)) {
 | |
| 				d->dc.maxdepth.mm = d->maxdepth.mm;
 | |
| 				free_samples(&d->dc);
 | |
| 			}
 | |
| 			return true;
 | |
| 		}
 | |
| 	}
 | |
| 	return false;
 | |
| }
 | |
| 
 | |
| // update the dive and return the notes field, stripped of the HTML junk
 | |
| void QMLManager::commitChanges(QString diveId, QString number, QString date, QString location, QString gps, QString duration, QString depth,
 | |
| 			       QString airtemp, QString watertemp, QString suit, QString buddy, QString diveGuide, QString tags, QString weight, QString notes,
 | |
| 			       QStringList startpressure, QStringList endpressure, QStringList gasmix, QStringList usedCylinder, int rating, int visibility, QString state)
 | |
| {
 | |
| 	struct dive *orig = get_dive_by_uniq_id(diveId.toInt());
 | |
| 
 | |
| 	if (!orig) {
 | |
| 		appendTextToLog("cannot commit changes: no dive");
 | |
| 		return;
 | |
| 	}
 | |
| 	if (verbose) {
 | |
| 		qDebug().noquote() << QStringLiteral("diveId  :'%1'\n").arg(diveId) <<
 | |
| 				      QStringLiteral("number  :'%1'\n").arg(number) <<
 | |
| 				      QStringLiteral("date    :'%1'\n").arg(date) <<
 | |
| 				      QStringLiteral("location:'%1'\n").arg(location) <<
 | |
| 				      QStringLiteral("gps     :'%1'\n").arg(gps) <<
 | |
| 				      QStringLiteral("duration:'%1'\n").arg(duration) <<
 | |
| 				      QStringLiteral("depth   :'%1'\n").arg(depth) <<
 | |
| 				      QStringLiteral("airtemp :'%1'\n").arg(airtemp) <<
 | |
| 				      QStringLiteral("watertmp:'%1'\n").arg(watertemp) <<
 | |
| 				      QStringLiteral("suit    :'%1'\n").arg(suit) <<
 | |
| 				      QStringLiteral("buddy   :'%1'\n").arg(buddy) <<
 | |
| 				      QStringLiteral("diveGde :'%1'\n").arg(diveGuide) <<
 | |
| 				      QStringLiteral("tags    :'%1'\n").arg(tags) <<
 | |
| 				      QStringLiteral("weight  :'%1'\n").arg(weight) <<
 | |
| 				      QStringLiteral("state   :'%1'\n").arg(state);
 | |
| 	}
 | |
| 
 | |
| 	OwningDivePtr d_ptr(alloc_dive()); // Automatically delete dive if we exit early!
 | |
| 	dive *d = d_ptr.get();
 | |
| 	copy_dive(orig, d);
 | |
| 
 | |
| 	// notes comes back as rich text - let's convert this into plain text
 | |
| 	QTextDocument doc;
 | |
| 	doc.setHtml(notes);
 | |
| 	notes = doc.toPlainText();
 | |
| 
 | |
| 	bool diveChanged = false;
 | |
| 
 | |
| 	diveChanged = checkDate(d, date);
 | |
| 
 | |
| 	DiveSiteChange dsChange;
 | |
| 	diveChanged |= checkLocation(dsChange, d, location, gps);
 | |
| 
 | |
| 	diveChanged |= checkDuration(d, duration);
 | |
| 
 | |
| 	diveChanged |= checkDepth(d, depth);
 | |
| 
 | |
| 	if (QString::number(d->number) != number) {
 | |
| 		diveChanged = true;
 | |
| 		d->number = number.toInt();
 | |
| 	}
 | |
| 	if (get_temperature_string(d->airtemp, true) != airtemp) {
 | |
| 		diveChanged = true;
 | |
| 		d->airtemp.mkelvin = parseTemperatureToMkelvin(airtemp);
 | |
| 	}
 | |
| 	if (get_temperature_string(d->watertemp, true) != watertemp) {
 | |
| 		diveChanged = true;
 | |
| 		d->watertemp.mkelvin = parseTemperatureToMkelvin(watertemp);
 | |
| 	}
 | |
| 	if (formatSumWeight(d) != weight) {
 | |
| 		diveChanged = true;
 | |
| 		// not sure what we'd do if there was more than one weight system
 | |
| 		// defined - for now just ignore that case
 | |
| 		if (d->weightsystems.nr == 0) {
 | |
| 			weightsystem_t ws = { { parseWeightToGrams(weight) } , strdup(qPrintable(tr("weight"))), false };
 | |
| 			add_to_weightsystem_table(&d->weightsystems, 0, ws); // takes ownership of the string
 | |
| 		} else if (d->weightsystems.nr == 1) {
 | |
| 			d->weightsystems.weightsystems[0].weight.grams = parseWeightToGrams(weight);
 | |
| 		}
 | |
| 	}
 | |
| 	// start and end pressures
 | |
| 	// first, normalize the lists - QML gives us a list with just one empty string if nothing was entered
 | |
| 	if (startpressure == QStringList(QString()))
 | |
| 		startpressure = QStringList();
 | |
| 	if (endpressure == QStringList(QString()))
 | |
| 		endpressure = QStringList();
 | |
| 	if (formatStartPressure(d) != startpressure || formatEndPressure(d) != endpressure) {
 | |
| 		diveChanged = true;
 | |
| 		for ( int i = 0, j = 0 ; j < startpressure.length() && j < endpressure.length() ; i++ ) {
 | |
| 			if (state != "add" && !is_cylinder_used(d, i))
 | |
| 				continue;
 | |
| 
 | |
| 			cylinder_t *cyl = get_or_create_cylinder(d, i);
 | |
| 			cyl->start.mbar = parsePressureToMbar(startpressure[j]);
 | |
| 			cyl->end.mbar = parsePressureToMbar(endpressure[j]);
 | |
| 			if (cyl->end.mbar > cyl->start.mbar)
 | |
| 				cyl->end.mbar = cyl->start.mbar;
 | |
| 
 | |
| 			j++;
 | |
| 		}
 | |
| 	}
 | |
| 	// gasmix for first cylinder
 | |
| 	if (formatFirstGas(d) != gasmix) {
 | |
| 		for ( int i = 0, j = 0 ; j < gasmix.length() ; i++ ) {
 | |
| 			if (state != "add" && !is_cylinder_used(d, i))
 | |
| 				continue;
 | |
| 
 | |
| 			int o2 = parseGasMixO2(gasmix[j]);
 | |
| 			int he = parseGasMixHE(gasmix[j]);
 | |
| 			// the QML code SHOULD only accept valid gas mixes, but just to make sure
 | |
| 			if (o2 >= 0 && o2 <= 1000 &&
 | |
| 				he >= 0 && he <= 1000 &&
 | |
| 				o2 + he <= 1000) {
 | |
| 				diveChanged = true;
 | |
| 				get_or_create_cylinder(d, i)->gasmix.o2.permille = o2;
 | |
| 				get_cylinder(d, i)->gasmix.he.permille = he;
 | |
| 			}
 | |
| 			j++;
 | |
| 		}
 | |
| 	}
 | |
| 	// info for first cylinder
 | |
| 	if (formatGetCylinder(d) != usedCylinder) {
 | |
| 		diveChanged = true;
 | |
| 		int size = 0, wp = 0, j = 0, k = 0;
 | |
| 		for (j = 0; k < usedCylinder.length(); j++) {
 | |
| 			if (state != "add" && !is_cylinder_used(d, j))
 | |
| 				continue;
 | |
| 
 | |
| 			for (int i = 0; i < tank_info_table.nr; i++) {
 | |
| 				const tank_info &ti = tank_info_table.infos[i];
 | |
| 				if (ti.name == usedCylinder[k] ) {
 | |
| 					if (ti.ml > 0){
 | |
| 						size = ti.ml;
 | |
| 						wp = ti.bar * 1000;
 | |
| 					} else {
 | |
| 						size = (int) (cuft_to_l(ti.cuft) * 1000 / bar_to_atm(psi_to_bar(ti.psi)));
 | |
| 						wp = psi_to_mbar(ti.psi);
 | |
| 					}
 | |
| 					break;
 | |
| 				}
 | |
| 			}
 | |
| 			get_or_create_cylinder(d, j)->type.description = copy_qstring(usedCylinder[k]);
 | |
| 			get_cylinder(d, j)->type.size.mliter = size;
 | |
| 			get_cylinder(d, j)->type.workingpressure.mbar = wp;
 | |
| 			k++;
 | |
| 		}
 | |
| 	}
 | |
| 	if (d->suit != suit) {
 | |
| 		diveChanged = true;
 | |
| 		free(d->suit);
 | |
| 		d->suit = copy_qstring(suit);
 | |
| 	}
 | |
| 	if (d->buddy != buddy) {
 | |
| 		if (buddy.contains(",")){
 | |
| 			buddy = buddy.replace(QRegularExpression("\\s*,\\s*"), ", ");
 | |
| 		}
 | |
| 		diveChanged = true;
 | |
| 		free(d->buddy);
 | |
| 		d->buddy = copy_qstring(buddy);
 | |
| 	}
 | |
| 	if (d->diveguide != diveGuide) {
 | |
| 		if (diveGuide.contains(",")){
 | |
| 			diveGuide = diveGuide.replace(QRegularExpression("\\s*,\\s*"), ", ");
 | |
| 		}
 | |
| 		diveChanged = true;
 | |
| 		free(d->diveguide);
 | |
| 		d->diveguide = copy_qstring(diveGuide);
 | |
| 	}
 | |
| 	// normalize the tag list we have and the one we get from the UI
 | |
| 	// try hard to deal with accidental white space issues
 | |
| 	QStringList existingTagList = get_taglist_string(d->tag_list).split(",", SKIP_EMPTY);
 | |
| 	QStringList newTagList = tags.split(",", SKIP_EMPTY);
 | |
| 	QStringList newCleanTagList;
 | |
| 	for (QString s: newTagList) {
 | |
| 		if (!s.simplified().isEmpty())
 | |
| 			newCleanTagList << s.simplified();
 | |
| 	}
 | |
| 	newCleanTagList.sort();
 | |
| 	existingTagList.sort();
 | |
| 	if (newCleanTagList.join(",") != existingTagList.join(",")) {
 | |
| 		diveChanged = true;
 | |
| 		taglist_free(d->tag_list);
 | |
| 		d->tag_list = nullptr;
 | |
| 		for (QString tag: newCleanTagList)
 | |
| 			taglist_add_tag(&d->tag_list, qPrintable(tag));
 | |
| 	}
 | |
| 	if (d->rating != rating) {
 | |
| 		diveChanged = true;
 | |
| 		d->rating = rating;
 | |
| 	}
 | |
| 	if (d->visibility != visibility) {
 | |
| 		diveChanged = true;
 | |
| 		d->visibility = visibility;
 | |
| 	}
 | |
| 	if (formatNotes(d) != notes) {
 | |
| 		diveChanged = true;
 | |
| 		free(d->notes);
 | |
| 		d->notes = copy_qstring(notes);
 | |
| 	}
 | |
| 	// now that we have it all figured out, let's see what we need
 | |
| 	// to update
 | |
| 	if (diveChanged) {
 | |
| 		if (d->maxdepth.mm == d->dc.maxdepth.mm &&
 | |
| 		    d->maxdepth.mm > 0 &&
 | |
| 		    is_manually_added_dc(&d->dc) &&
 | |
| 		    d->dc.samples == 0) {
 | |
| 			// so we have depth > 0, a manually added dive and no samples
 | |
| 			// let's create an actual profile so the desktop version can work it
 | |
| 			// first clear out the mean depth (or the fake_dc() function tries
 | |
| 			// to be too clever)
 | |
| 			d->meandepth.mm = d->dc.meandepth.mm = 0;
 | |
| 			fake_dc(&d->dc);
 | |
| 		}
 | |
| 		fixup_dive(d);
 | |
| 		Command::editDive(orig, d_ptr.release(), dsChange.createdDs.release(), dsChange.editDs, dsChange.location); // With release() we're giving up ownership
 | |
| 		changesNeedSaving();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void QMLManager::updateTripDetails(QString tripIdString, QString tripLocation, QString tripNotes)
 | |
| {
 | |
| 	int tripId = tripIdString.toInt();
 | |
| 	dive_trip_t *trip = get_trip_by_uniq_id(tripId);
 | |
| 	if (!trip) {
 | |
| 		qDebug() << "updateTripData: cannot find trip for tripId" << tripIdString;
 | |
| 		return;
 | |
| 	}
 | |
| 	bool changed = false;
 | |
| 	if (tripLocation != trip->location) {
 | |
| 		changed = true;
 | |
| 		Command::editTripLocation(trip, tripLocation);
 | |
| 	}
 | |
| 	if (tripNotes != trip->notes) {
 | |
| 		changed = true;
 | |
| 		Command::editTripNotes(trip, tripNotes);
 | |
| 	}
 | |
| 	if (changed)
 | |
| 		changesNeedSaving();
 | |
| }
 | |
| 
 | |
| void QMLManager::removeDiveFromTrip(int id)
 | |
| {
 | |
| 	struct dive *d = get_dive_by_uniq_id(id);
 | |
| 	if (!d) {
 | |
| 		appendTextToLog(QString("Asked to remove non-existing dive with id %1 from its trip.").arg(id));
 | |
| 		return;
 | |
| 	}
 | |
| 	if (!d->divetrip) {
 | |
| 		appendTextToLog(QString("Asked to remove dive with id %1 from its trip (but it's not part of a trip).").arg(id));
 | |
| 		return;
 | |
| 	}
 | |
| 	QVector <dive *> dives;
 | |
| 	dives.append(d);
 | |
| 	Command::removeDivesFromTrip(dives);
 | |
| 	changesNeedSaving();
 | |
| }
 | |
| 
 | |
| void QMLManager::addTripForDive(int id)
 | |
| {
 | |
| 	struct dive *d = get_dive_by_uniq_id(id);
 | |
| 	if (!d) {
 | |
| 		appendTextToLog(QString("Asked to create trip for non-existing dive with id %1").arg(id));
 | |
| 		return;
 | |
| 	}
 | |
| 	if (d->divetrip) {
 | |
| 		appendTextToLog(QString("Asked to create trip for dive %1 with id %2 but it's already part of a trip with location %3.").arg(d->number).arg(id).arg(d->divetrip->location));
 | |
| 		return;
 | |
| 	}
 | |
| 	QVector <dive *> dives;
 | |
| 	dives.append(d);
 | |
| 	Command::createTrip(dives);
 | |
| 	changesNeedSaving();
 | |
| }
 | |
| 
 | |
| void QMLManager::addDiveToTrip(int id, int tripId)
 | |
| {
 | |
| 	struct dive *d = get_dive_by_uniq_id(id);
 | |
| 	if (!d) {
 | |
| 		appendTextToLog(QString("Asked to add non-existing dive with id %1 to trip %2.").arg(id).arg(tripId));
 | |
| 		return;
 | |
| 	}
 | |
| 	struct dive_trip *dt = get_trip_by_uniq_id(tripId);
 | |
| 	if (!dt) {
 | |
| 		appendTextToLog(QString("Asked to add dive with id %1 to trip with id %2 which cannot be found.").arg(id).arg(tripId));
 | |
| 		return;
 | |
| 	}
 | |
| 	QVector <dive *> dives;
 | |
| 	dives.append(d);
 | |
| 	Command::addDivesToTrip(dives, dt);
 | |
| 	changesNeedSaving();
 | |
| }
 | |
| 
 | |
| void QMLManager::changesNeedSaving(bool fromUndo)
 | |
| {
 | |
| 	// we no longer save right away on iOS because file access is so slow; on the other hand,
 | |
| 	// on Android the save as the user switches away doesn't seem to work... drat.
 | |
| 	// as a compromise for now we save just to local storage on Android right away (that appears
 | |
| 	// to be reasonably fast), but don't save at all (and only remember that we need to save things
 | |
| 	// on iOS
 | |
| 	// on all other platforms we just save the changes and be done with it
 | |
| 	mark_divelist_changed(true);
 | |
| 	emit syncStateChanged();
 | |
| #if defined(Q_OS_IOS)
 | |
| 	saveChangesLocal(fromUndo);
 | |
| #else
 | |
| 	saveChangesCloud(false, fromUndo);
 | |
| #endif
 | |
| 	updateAllGlobalLists();
 | |
| }
 | |
| 
 | |
| void QMLManager::openNoCloudRepo()
 | |
| {
 | |
| 	/*
 | |
| 	 * Open the No Cloud repo. In case this repo does not (yet)
 | |
| 	 * exists, create one first. When done, open the repo, which
 | |
| 	 * is obviously empty when just created.
 | |
| 	 */
 | |
| 	QString filename = nocloud_localstorage();
 | |
| 	struct git_info info;
 | |
| 
 | |
| 	appendTextToLog(QString("User asked not to connect to cloud, using %1 as repo.").arg(filename));
 | |
| 	if (is_git_repository(qPrintable(filename), &info) && !open_git_repository(&info)) {
 | |
| 		// repo doesn't exist, create it and write the empty dive list to it
 | |
| 		git_create_local_repo(qPrintable(filename));
 | |
| 		save_dives(qPrintable(filename));
 | |
| 		set_filename(qPrintable(filename));
 | |
| 		auto s = qPrefLog::instance();
 | |
| 		s->set_default_filename(qPrintable(filename));
 | |
| 		s->set_default_file_behavior(LOCAL_DEFAULT_FILE);
 | |
| 	}
 | |
| 	openLocalThenRemote(filename);
 | |
| }
 | |
| 
 | |
| void QMLManager::saveChangesLocal(bool fromUndo)
 | |
| {
 | |
| 	if (unsavedChanges()) {
 | |
| 		if (qPrefCloudStorage::cloud_verification_status() == qPrefCloudStorage::CS_NOCLOUD) {
 | |
| 			if (empty_string(existing_filename)) {
 | |
| 				QString filename = nocloud_localstorage();
 | |
| 				git_create_local_repo(qPrintable(filename));
 | |
| 				set_filename(qPrintable(filename));
 | |
| 				auto s = qPrefLog::instance();
 | |
| 				s->set_default_filename(qPrintable(filename));
 | |
| 				s->set_default_file_behavior(LOCAL_DEFAULT_FILE);
 | |
| 			}
 | |
| 		} else if (!m_loadFromCloud) {
 | |
| 			// this seems silly, but you need a common ancestor in the repository in
 | |
| 			// order to be able to merge che changes later
 | |
| 			appendTextToLog("Don't save dives without loading from the cloud, first.");
 | |
| 			return;
 | |
| 		}
 | |
| 		bool glo = git_local_only;
 | |
| 		git_local_only = true;
 | |
| 		int error = save_dives(existing_filename);
 | |
| 		git_local_only = glo;
 | |
| 		if (error) {
 | |
| 			setNotificationText(consumeError());
 | |
| 			set_filename(NULL);
 | |
| 			return;
 | |
| 		}
 | |
| 		mark_divelist_changed(false);
 | |
| 		Command::setClean();
 | |
| 		updateHaveLocalChanges(true);
 | |
| 		// provide a useful undo/redo notification
 | |
| 		// NOTE: the QML UI interprets a leading '[action]' (where only the two brackets are checked for)
 | |
| 		//       as an indication to use the text between those two brackets as the label of a button that
 | |
| 		//       can be used to open the context menu
 | |
| 		QString msgFormat = tr("[%1]Changes saved:'%2'.\n%1 possible via context menu");
 | |
| 		if (fromUndo)
 | |
| 			setNotificationText(msgFormat.arg(tr("Redo")).arg(tr("Undo: %1").arg(getRedoText())));
 | |
| 		else
 | |
| 			setNotificationText(msgFormat.arg(tr("Undo")).arg(getUndoText()));
 | |
| 	} else {
 | |
| 		appendTextToLog("local save requested with no unsaved changes");
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void QMLManager::saveChangesCloud(bool forceRemoteSync, bool fromUndo)
 | |
| {
 | |
| 	if (!unsavedChanges() && !forceRemoteSync) {
 | |
| 		appendTextToLog("asked to save changes but no unsaved changes");
 | |
| 		return;
 | |
| 	}
 | |
| 	// first we need to store any unsaved changes to the local repo
 | |
| 	gitProgressCB("Save changes to local cache");
 | |
| 	saveChangesLocal(fromUndo);
 | |
| 	// if the user asked not to push to the cloud we are done
 | |
| 	if (git_local_only && !forceRemoteSync)
 | |
| 		return;
 | |
| 
 | |
| 	if (!m_loadFromCloud) {
 | |
| 		setNotificationText(tr("Fatal error: cannot save data file. Please copy log file and report."));
 | |
| 		appendTextToLog("Don't save dives without loading from the cloud, first.");
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	bool glo = git_local_only;
 | |
| 	git_local_only = false;
 | |
| 	loadDivesWithValidCredentials();
 | |
| 	git_local_only = glo;
 | |
| }
 | |
| 
 | |
| void QMLManager::undo()
 | |
| {
 | |
| 	Command::getUndoStack()->undo();
 | |
| 	changesNeedSaving(true);
 | |
| }
 | |
| 
 | |
| void QMLManager::redo()
 | |
| {
 | |
| 	Command::getUndoStack()->redo();
 | |
| 	changesNeedSaving();
 | |
| }
 | |
| 
 | |
| void QMLManager::selectDive(int id)
 | |
| {
 | |
| 	int i;
 | |
| 	extern int amount_selected;
 | |
| 	struct dive *dive = NULL;
 | |
| 
 | |
| 	amount_selected = 0;
 | |
| 	for_each_dive (i, dive) {
 | |
| 		dive->selected = (dive->id == id);
 | |
| 		if (dive->selected)
 | |
| 			amount_selected++;
 | |
| 	}
 | |
| 	if (amount_selected == 0)
 | |
| 		qWarning("QManager::selectDive() called with unknown id %d",id);
 | |
| }
 | |
| 
 | |
| void QMLManager::deleteDive(int id)
 | |
| {
 | |
| 	struct dive *d = get_dive_by_uniq_id(id);
 | |
| 	if (!d) {
 | |
| 		appendTextToLog("trying to delete non-existing dive");
 | |
| 		return;
 | |
| 	}
 | |
| 	Command::deleteDive(QVector<dive *>{ d });
 | |
| 	changesNeedSaving();
 | |
| }
 | |
| 
 | |
| void QMLManager::toggleDiveInvalid(int id)
 | |
| {
 | |
| 	struct dive *d = get_dive_by_uniq_id(id);
 | |
| 	if (!d) {
 | |
| 		appendTextToLog("trying to toggle invalid flag of non-existing dive");
 | |
| 		return;
 | |
| 	}
 | |
| 	Command::editInvalid(!d->invalid, true);
 | |
| 	changesNeedSaving();
 | |
| }
 | |
| 
 | |
| bool QMLManager::toggleDiveSite(bool toggle)
 | |
| {
 | |
| 	if (toggle)
 | |
| 		what.divesite = what.divesite ? false : true;
 | |
| 
 | |
| 	return what.divesite;
 | |
| }
 | |
| 
 | |
| bool QMLManager::toggleNotes(bool toggle)
 | |
| {
 | |
| 	if (toggle)
 | |
| 		what.notes = what.notes ? false : true;
 | |
| 
 | |
| 	return what.notes;
 | |
| }
 | |
| 
 | |
| bool QMLManager::toggleDiveGuide(bool toggle)
 | |
| {
 | |
| 	if (toggle)
 | |
| 		what.diveguide = what.diveguide ? false : true;
 | |
| 
 | |
| 	return what.diveguide;
 | |
| }
 | |
| 
 | |
| bool QMLManager::toggleBuddy(bool toggle)
 | |
| {
 | |
| 	if (toggle)
 | |
| 		what.buddy = what.buddy ? false : true;
 | |
| 
 | |
| 	return what.buddy;
 | |
| }
 | |
| 
 | |
| bool QMLManager::toggleSuit(bool toggle)
 | |
| {
 | |
| 	if (toggle)
 | |
| 		what.suit = what.suit ? false : true;
 | |
| 
 | |
| 	return what.suit;
 | |
| }
 | |
| 
 | |
| bool QMLManager::toggleRating(bool toggle)
 | |
| {
 | |
| 	if (toggle)
 | |
| 		what.rating = what.rating ? false : true;
 | |
| 
 | |
| 	return what.rating;
 | |
| }
 | |
| 
 | |
| bool QMLManager::toggleVisibility(bool toggle)
 | |
| {
 | |
| 	if (toggle)
 | |
| 		what.visibility = what.visibility ? false : true;
 | |
| 
 | |
| 	return what.visibility;
 | |
| }
 | |
| 
 | |
| bool QMLManager::toggleTags(bool toggle)
 | |
| {
 | |
| 	if (toggle)
 | |
| 		what.tags = what.tags ? false : true;
 | |
| 
 | |
| 	return what.tags;
 | |
| }
 | |
| 
 | |
| bool QMLManager::toggleCylinders(bool toggle)
 | |
| {
 | |
| 	if (toggle)
 | |
| 		what.cylinders = what.cylinders ? false : true;
 | |
| 
 | |
| 	return what.cylinders;
 | |
| }
 | |
| 
 | |
| bool QMLManager::toggleWeights(bool toggle)
 | |
| {
 | |
| 	if (toggle)
 | |
| 		what.weights = what.weights ? false : true;
 | |
| 
 | |
| 	return what.weights;
 | |
| }
 | |
| 
 | |
| void QMLManager::copyDiveData(int id)
 | |
| {
 | |
| 	m_copyPasteDive = get_dive_by_uniq_id(id);
 | |
| 	if (!m_copyPasteDive) {
 | |
| 		appendTextToLog("trying to copy non-existing dive");
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	setNotificationText("Copy");
 | |
| }
 | |
| 
 | |
| void QMLManager::pasteDiveData(int id)
 | |
| {
 | |
| 	if (!m_copyPasteDive) {
 | |
| 		appendTextToLog("dive to paste is not selected");
 | |
| 		return;
 | |
| 	}
 | |
| 	Command::pasteDives(m_copyPasteDive, what);
 | |
| 	changesNeedSaving();
 | |
| }
 | |
| 
 | |
| void QMLManager::cancelDownloadDC()
 | |
| {
 | |
| 	import_thread_cancelled = true;
 | |
| }
 | |
| 
 | |
| int QMLManager::addDive()
 | |
| {
 | |
| 	// TODO: Duplicate code with desktop-widgets/mainwindow.cpp
 | |
| 	// create a dive an hour from now with a default depth (15m/45ft) and duration (40 minutes)
 | |
| 	// as a starting point for the user to edit
 | |
| 	struct dive d = { 0 };
 | |
| 	int diveId = d.id = dive_getUniqID();
 | |
| 	d.when = QDateTime::currentMSecsSinceEpoch() / 1000L + gettimezoneoffset() + 3600;
 | |
| 	d.dc.duration.seconds = 40 * 60;
 | |
| 	d.dc.maxdepth.mm = M_OR_FT(15, 45);
 | |
| 	d.dc.meandepth.mm = M_OR_FT(13, 39); // this creates a resonable looking safety stop
 | |
| 	make_manually_added_dc(&d.dc);
 | |
| 	fake_dc(&d.dc);
 | |
| 	fixup_dive(&d);
 | |
| 
 | |
| 	// addDive takes over the dive and clears out the structure passed in
 | |
| 	// we do NOT save the modified data at this stage because of the UI flow here... this will
 | |
| 	// be saved once the user finishes editing the newly added dive
 | |
| 	Command::addDive(&d, divelog.autogroup, true);
 | |
| 
 | |
| 	if (verbose)
 | |
| 		appendTextToLog(QString("Adding new dive with id '%1'").arg(diveId));
 | |
| 	// the QML UI uses the return value to set up the edit screen
 | |
| 	return diveId;
 | |
| }
 | |
| 
 | |
| void QMLManager::appendTextToLog(const QString &newText)
 | |
| {
 | |
| 	qDebug() << QString::number(timer.elapsed() / 1000.0,'f', 3) + ": " + newText;
 | |
| }
 | |
| 
 | |
| void QMLManager::setVerboseEnabled(bool verboseMode)
 | |
| {
 | |
| 	m_verboseEnabled = verboseMode;
 | |
| 	verbose = verboseMode;
 | |
| 	appendTextToLog(QStringLiteral("verbose is ") + (verbose ? QStringLiteral("on") : QStringLiteral("off")));
 | |
| 	emit verboseEnabledChanged();
 | |
| }
 | |
| 
 | |
| void QMLManager::syncLoadFromCloud()
 | |
| {
 | |
| 	QSettings s;
 | |
| 	QString cloudMarker = QLatin1String("loadFromCloud") + QString(prefs.cloud_storage_email);
 | |
| 	m_loadFromCloud = s.contains(cloudMarker) && s.value(cloudMarker).toBool();
 | |
| }
 | |
| 
 | |
| void QMLManager::setLoadFromCloud(bool done)
 | |
| {
 | |
| 	QSettings s;
 | |
| 	QString cloudMarker = QLatin1String("loadFromCloud") + QString(prefs.cloud_storage_email);
 | |
| 	s.setValue(cloudMarker, done);
 | |
| 	m_loadFromCloud = done;
 | |
| 	emit loadFromCloudChanged();
 | |
| }
 | |
| 
 | |
| void QMLManager::setStartPageText(const QString& text)
 | |
| {
 | |
| 	m_startPageText = text;
 | |
| 	emit startPageTextChanged();
 | |
| }
 | |
| 
 | |
| QString QMLManager::getNumber(const QString& diveId)
 | |
| {
 | |
| 	int dive_id = diveId.toInt();
 | |
| 	struct dive *d = get_dive_by_uniq_id(dive_id);
 | |
| 	QString number;
 | |
| 	if (d)
 | |
| 		number = QString::number(d->number);
 | |
| 	return number;
 | |
| }
 | |
| 
 | |
| QString QMLManager::getDate(const QString& diveId)
 | |
| {
 | |
| 	int dive_id = diveId.toInt();
 | |
| 	struct dive *d = get_dive_by_uniq_id(dive_id);
 | |
| 	QString datestring;
 | |
| 	if (d)
 | |
| 		datestring = get_short_dive_date_string(d->when);
 | |
| 	return datestring;
 | |
| }
 | |
| 
 | |
| QString QMLManager::getVersion() const
 | |
| {
 | |
| 	QRegularExpression versionRe(":([()\\.,\\d]+)");
 | |
| 	QRegularExpressionMatch match = versionRe.match(getUserAgent());
 | |
| 	if (!match.hasMatch())
 | |
| 		return QString();
 | |
| 
 | |
| 	return match.captured(1);
 | |
| }
 | |
| 
 | |
| QString QMLManager::getGpsFromSiteName(const QString &siteName)
 | |
| {
 | |
| 	struct dive_site *ds;
 | |
| 
 | |
| 	ds = get_dive_site_by_name(qPrintable(siteName), divelog.sites);
 | |
| 	if (!ds)
 | |
| 		return QString();
 | |
| 	return printGPSCoords(&ds->location);
 | |
| }
 | |
| 
 | |
| void QMLManager::setNotificationText(QString text)
 | |
| {
 | |
| 	appendTextToLog(QStringLiteral("showProgress: ") + text);
 | |
| 	m_notificationText = text;
 | |
| 	emit notificationTextChanged();
 | |
| 	// Once we're initialized, this may be called from signal context, notably when selecting an action from a context menu.
 | |
| 	// Processing events may now cause the menu to be deleted. Deleting a QML object which sent a signal causes QML to quit the application.
 | |
| 	// Therefore, don't process events once the application is started.
 | |
| 	// During startup this is needed so that the notifications are shown.
 | |
| 	if (!m_initialized)
 | |
| 		qApp->processEvents();
 | |
| }
 | |
| 
 | |
| qreal QMLManager::lastDevicePixelRatio()
 | |
| {
 | |
| 	return m_lastDevicePixelRatio;
 | |
| }
 | |
| 
 | |
| void QMLManager::setDevicePixelRatio(qreal dpr, QScreen *screen)
 | |
| {
 | |
| 	if (m_lastDevicePixelRatio != dpr) {
 | |
| 		m_lastDevicePixelRatio = dpr;
 | |
| 		emit sendScreenChanged(screen);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void QMLManager::screenChanged(QScreen *screen)
 | |
| {
 | |
| 	qDebug("QMLManager received screen changed notification (%d,%d)", screen->size().width(), screen->size().height());
 | |
| 	m_lastDevicePixelRatio = screen->devicePixelRatio();
 | |
| 	emit sendScreenChanged(screen);
 | |
| }
 | |
| 
 | |
| void QMLManager::quit()
 | |
| {
 | |
| 	if (unsavedChanges())
 | |
| 		saveChangesCloud(false);
 | |
| 	QApplication::quit();
 | |
| }
 | |
| 
 | |
| QStringList QMLManager::suitList() const
 | |
| {
 | |
| 	return suitModel.stringList();
 | |
| }
 | |
| 
 | |
| QStringList QMLManager::buddyList() const
 | |
| {
 | |
| 	return buddyModel.stringList();
 | |
| }
 | |
| 
 | |
| QStringList QMLManager::diveguideList() const
 | |
| {
 | |
| 	return diveguideModel.stringList();
 | |
| }
 | |
| 
 | |
| QStringList QMLManager::locationList() const
 | |
| {
 | |
| 	return locationModel.allSiteNames();
 | |
| }
 | |
| 
 | |
| // this is the cylinder list used when editing a dive
 | |
| QStringList QMLManager::cylinderListInit() const
 | |
| {
 | |
| 	return formatFullCylinderList();
 | |
| }
 | |
| 
 | |
| // this is the cylinder list used to pick your default cylinder
 | |
| // it starts with the (translated) "no default cylinder" in order to special-case this
 | |
| QStringList QMLManager::defaultCylinderListInit() const
 | |
| {
 | |
| 	QStringList cylinders = cylinderListInit();
 | |
| 	// now add fist one that indicates that the user wants no default cylinder
 | |
| 	cylinders.prepend(tr("no default cylinder"));
 | |
| 	return cylinders;
 | |
| }
 | |
| 
 | |
| void QMLManager::setProgressMessage(QString text)
 | |
| {
 | |
| 	m_progressMessage = text;
 | |
| 	emit progressMessageChanged();
 | |
| }
 | |
| 
 | |
| void QMLManager::setBtEnabled(bool value)
 | |
| {
 | |
| 	m_btEnabled = value;
 | |
| }
 | |
| 
 | |
| #if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
 | |
| 
 | |
| void writeToAppLogFile(QString logText)
 | |
| {
 | |
| 	// write to storage and flush so that the data doesn't get lost
 | |
| 	logText.append("\n");
 | |
| 	QMLManager *self = QMLManager::instance();
 | |
| 	if (self) {
 | |
| 		self->writeToAppLogFile(logText);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void QMLManager::writeToAppLogFile(QString logText)
 | |
| {
 | |
| 	if (appLogFileOpen) {
 | |
| 		appLogFile.write(qPrintable(logText));
 | |
| 		appLogFile.flush();
 | |
| 	}
 | |
| }
 | |
| #endif
 | |
| 
 | |
| #if defined(Q_OS_ANDROID)
 | |
| //HACK to color the system bar on Android, use qtandroidextras and call the appropriate Java methods
 | |
| //this code is based on code in the Kirigami example app for Android (under LGPL-2) Copyright 2017 Marco Martin
 | |
| 
 | |
| // there doesn't appear to be an include that defines these in an easily accessible way
 | |
| // WindowManager.LayoutParams
 | |
| #define FLAG_TRANSLUCENT_STATUS 0x04000000
 | |
| #define FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS 0x80000000
 | |
| // View
 | |
| #define SYSTEM_UI_FLAG_LIGHT_STATUS_BAR 0x00002000
 | |
| 
 | |
| void QMLManager::setStatusbarColor(QColor color)
 | |
| {
 | |
| 	QtAndroid::runOnAndroidThread([color]() {
 | |
| 		QAndroidJniObject window = QtAndroid::androidActivity().callObjectMethod("getWindow", "()Landroid/view/Window;");
 | |
| 		window.callMethod<void>("addFlags", "(I)V", FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
 | |
| 		window.callMethod<void>("clearFlags", "(I)V", FLAG_TRANSLUCENT_STATUS);
 | |
| 		window.callMethod<void>("setStatusBarColor", "(I)V", color.rgba());
 | |
| 		window.callMethod<void>("setNavigationBarColor", "(I)V", color.rgba());
 | |
| 	});
 | |
| }
 | |
| #else
 | |
| void QMLManager::setStatusbarColor(QColor)
 | |
| {
 | |
| 	// noop
 | |
| }
 | |
| 
 | |
| #endif
 | |
| 
 | |
| void QMLManager::retrieveBluetoothName()
 | |
| {
 | |
| 	QString name = DC_devName();
 | |
| 	const QList<BTDiscovery::btVendorProduct> btDCs = BTDiscovery::instance()->getBtDcs();
 | |
| 	for (BTDiscovery::btVendorProduct btDC: btDCs) {
 | |
| 		qDebug() << "compare" <<name << btDC.btpdi.address;
 | |
| 		if (name.contains(btDC.btpdi.address))
 | |
| 			DC_setDevBluetoothName(btDC.btpdi.name);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| QString QMLManager::DC_vendor() const
 | |
| {
 | |
| 	return DCDeviceData::instance()->vendor();
 | |
| }
 | |
| 
 | |
| QString QMLManager::DC_product() const
 | |
| {
 | |
| 	return DCDeviceData::instance()->product();
 | |
| }
 | |
| 
 | |
| QString QMLManager::DC_devName() const
 | |
| {
 | |
| 	return DCDeviceData::instance()->devName();
 | |
| }
 | |
| 
 | |
| QString QMLManager::DC_devBluetoothName() const
 | |
| {
 | |
| 	return DCDeviceData::instance()->devBluetoothName();
 | |
| }
 | |
| 
 | |
| QString QMLManager::DC_descriptor() const
 | |
| {
 | |
| 	return DCDeviceData::instance()->descriptor();
 | |
| }
 | |
| 
 | |
| bool QMLManager::DC_forceDownload() const
 | |
| {
 | |
| 	return DCDeviceData::instance()->forceDownload();
 | |
| }
 | |
| 
 | |
| bool QMLManager::DC_bluetoothMode() const
 | |
| {
 | |
| 	return DCDeviceData::instance()->bluetoothMode();
 | |
| }
 | |
| 
 | |
| bool QMLManager::DC_saveDump() const
 | |
| {
 | |
| 	return DCDeviceData::instance()->saveDump();
 | |
| }
 | |
| 
 | |
| void QMLManager::DC_setVendor(const QString& vendor)
 | |
| {
 | |
| 	DCDeviceData::instance()->setVendor(vendor);
 | |
| }
 | |
| 
 | |
| void QMLManager::DC_setProduct(const QString& product)
 | |
| {
 | |
| 	DCDeviceData::instance()->setProduct(product);
 | |
| }
 | |
| 
 | |
| void QMLManager::DC_setDevName(const QString& devName)
 | |
| {
 | |
| 	DCDeviceData::instance()->setDevName(devName);
 | |
| #if defined(Q_OS_ANDROID)
 | |
| 	// get the currently valid list of devices and set up the USB device descriptor
 | |
| 	// if the connection string matches a connection in that list
 | |
| 	androidSerialDevices = serial_usb_android_get_devices();
 | |
| 	std::string connection = devName.toStdString();
 | |
| 	for (unsigned int i = 0; i < androidSerialDevices.size(); i++) {
 | |
| 		if (androidSerialDevices[i].uiRepresentation == connection) {
 | |
| 			appendTextToLog(QString("setDevName matches USB device %1").arg(i));
 | |
| 			DCDeviceData::instance()->setUsbDevice(androidSerialDevices[i]);
 | |
| 		}
 | |
| 	}
 | |
| #endif
 | |
| }
 | |
| 
 | |
| void QMLManager::DC_setDevBluetoothName(const QString& devBluetoothName)
 | |
| {
 | |
| 	DCDeviceData::instance()->setDevBluetoothName(devBluetoothName);
 | |
| }
 | |
| 
 | |
| void QMLManager::DC_setBluetoothMode(bool mode)
 | |
| {
 | |
| 	DCDeviceData::instance()->setBluetoothMode(mode);
 | |
| }
 | |
| 
 | |
| void QMLManager::DC_setForceDownload(bool force)
 | |
| {
 | |
| 	DCDeviceData::instance()->setForceDownload(force);
 | |
| 	DC_ForceDownloadChanged();
 | |
| }
 | |
| 
 | |
| void QMLManager::DC_setSaveDump(bool dumpMode)
 | |
| {
 | |
| 	DCDeviceData::instance()->setSaveDump(dumpMode);
 | |
| }
 | |
| 
 | |
| QStringList QMLManager::getProductListFromVendor(const QString &vendor)
 | |
| {
 | |
| 	return DCDeviceData::instance()->getProductListFromVendor(vendor);
 | |
| }
 | |
| 
 | |
| int QMLManager::getMatchingAddress(const QString &vendor, const QString &product)
 | |
| {
 | |
| 	return DCDeviceData::instance()->getMatchingAddress(vendor, product);
 | |
| }
 | |
| 
 | |
| int QMLManager::getDetectedVendorIndex()
 | |
| {
 | |
| 	return DCDeviceData::instance()->getDetectedVendorIndex();
 | |
| }
 | |
| 
 | |
| int QMLManager::getDetectedProductIndex(const QString ¤tVendorText)
 | |
| {
 | |
| 	return DCDeviceData::instance()->getDetectedProductIndex(currentVendorText);
 | |
| }
 | |
| 
 | |
| int QMLManager::getConnectionIndex(const QString &deviceSubstr)
 | |
| {
 | |
| 	return connectionListModel.indexOf(deviceSubstr);
 | |
| }
 | |
| 
 | |
| void QMLManager::setGitLocalOnly(const bool &value)
 | |
| {
 | |
| 	git_local_only = value;
 | |
| }
 | |
| 
 | |
| #if defined(Q_OS_ANDROID)
 | |
| // try to guess which dive computer was plugged into the USB port
 | |
| QString QMLManager::getProductVendorConnectionIdx(android_usb_serial_device_descriptor descriptor)
 | |
| {
 | |
| 	// convert the information we get from the Android USB layer into indices for the three
 | |
| 	// combo boxes for vendor, product, and connection in the QML UI
 | |
| 	// for each of these values '-1' means that no entry should be pre-selected
 | |
| 	QString uiString = QString::fromStdString(descriptor.uiRepresentation);
 | |
| 	QString product = QString::fromStdString(descriptor.usbProduct);
 | |
| 	int connIdx = connectionListModel.indexOf(uiString);
 | |
| 	int vendIdx = -1;
 | |
| 	int prodIdx = -1;
 | |
| 
 | |
| 	if (!descriptor.manufacturer.empty())
 | |
| 		vendIdx = vendorList.indexOf(QString::fromStdString(descriptor.manufacturer));
 | |
| 	if (!descriptor.product.empty())
 | |
| 		prodIdx = productList[vendorList[vendIdx]].indexOf(QString::fromStdString(descriptor.product));
 | |
| 
 | |
| 	// the rest of them simply will get the default -1:-1:index of the uiString
 | |
| 	return QString("%1;%2;%3").arg(vendIdx).arg(prodIdx).arg(connIdx);
 | |
| }
 | |
| 
 | |
| void QMLManager::androidUsbPopoulateConnections()
 | |
| {
 | |
| 	// let's understand what devices are available
 | |
| 	androidSerialDevices = serial_usb_android_get_devices();
 | |
| 	appendTextToLog(QString("rescanning USB: %1 devices reported").arg(androidSerialDevices.size()));
 | |
| 
 | |
| 	// list all USB devices, this will include the one that triggered the intent
 | |
| 	for (unsigned int i = 0; i < androidSerialDevices.size(); i++) {
 | |
| 		QString uiString = QString::fromStdString(androidSerialDevices[i].uiRepresentation);
 | |
| 		appendTextToLog(QString("found USB device with ui string %1").arg(uiString));
 | |
| 		connectionListModel.addAddress(uiString);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void QMLManager::showDownloadPage(QAndroidJniObject usbDevice)
 | |
| {
 | |
| 	if (!usbDevice.isValid()) {
 | |
| 		// this really shouldn't happen anymore, but just in case...
 | |
| 		m_pluggedInDeviceName = "";
 | |
| 	} else {
 | |
| 		// repopulate the connection list
 | |
| 		rescanConnections();
 | |
| 
 | |
| 		// parse the usbDevice
 | |
| 		android_usb_serial_device_descriptor usbDeviceDescriptor = getDescriptor(usbDevice);
 | |
| 
 | |
| 		// inform the QML UI that it should show the download page
 | |
| 		m_pluggedInDeviceName = getProductVendorConnectionIdx(usbDeviceDescriptor);
 | |
| 	}
 | |
| 	emit pluggedInDeviceNameChanged();
 | |
| }
 | |
| 
 | |
| void QMLManager::restartDownload(QAndroidJniObject usbDevice)
 | |
| {
 | |
| 	// this gets called if we received a permission intent after
 | |
| 	// already trying to download from USB
 | |
| 	if (!usbDevice.isValid()) {
 | |
| 		appendTextToLog("permission intent with invalid UsbDevice - ignoring");
 | |
| 	} else {
 | |
| 		// inform that QML code that it should retry downloading
 | |
| 		// we get the usbDevice again and could verify that this is
 | |
| 		// still the same - but I don't see how this could change while we are waiting
 | |
| 		// for permission
 | |
| 		android_usb_serial_device_descriptor usbDeviceDescriptor = getDescriptor(usbDevice);
 | |
| 		appendTextToLog(QString("got permission from Android, restarting download for %1")
 | |
| 				.arg(QString::fromStdString(usbDeviceDescriptor.uiRepresentation)));
 | |
| 		emit restartDownloadSignal();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| #endif
 | |
| 
 | |
| static filter_constraint make_filter_constraint(filter_constraint_type type, const QString &s)
 | |
| {
 | |
| 	filter_constraint res(type);
 | |
| 	filter_constraint_set_stringlist(res, s);
 | |
| 	return res;
 | |
| }
 | |
| 
 | |
| void QMLManager::setFilter(const QString filterText, int index)
 | |
| {
 | |
| 	QString f = filterText.trimmed();
 | |
| 	FilterData data;
 | |
| 	if (!f.isEmpty()) {
 | |
| 		// This is ugly - the indices of the mode are hardcoded!
 | |
| 		switch(index) {
 | |
| 		default:
 | |
| 		case 0:
 | |
| 			data.fullText = f;
 | |
| 			break;
 | |
| 		case 1:
 | |
| 			data.constraints.push_back(make_filter_constraint(FILTER_CONSTRAINT_PEOPLE, f));
 | |
| 			break;
 | |
| 		case 2:
 | |
| 			data.constraints.push_back(make_filter_constraint(FILTER_CONSTRAINT_TAGS, f));
 | |
| 			break;
 | |
| 		}
 | |
| 	}
 | |
| 	DiveFilter::instance()->setFilter(data);
 | |
| }
 | |
| 
 | |
| void QMLManager::setShowNonDiveComputers(bool show)
 | |
| {
 | |
| 	m_showNonDiveComputers = show;
 | |
| 	BTDiscovery::instance()->showNonDiveComputers(show);
 | |
| }
 | |
| 
 | |
| #if defined(Q_OS_ANDROID)
 | |
| // implemented in core/android.cpp
 | |
| void checkPendingIntents();
 | |
| #endif
 | |
| 
 | |
| void QMLManager::appInitialized()
 | |
| {
 | |
| #if defined(Q_OS_ANDROID)
 | |
| 	checkPendingIntents();
 | |
| #endif
 | |
| }
 | |
| 
 | |
| #if !defined(Q_OS_ANDROID)
 | |
| void QMLManager::exportToFile(export_types type, QString dir, bool anonymize)
 | |
| {
 | |
| 	// dir starts with "file://" e.g. "file:///tmp"
 | |
| 	// remove prefix and add standard filenamel
 | |
| 	QString fileName = dir.right(dir.size() - 7) + "/Subsurface_export";
 | |
| 
 | |
| 	switch (type)
 | |
| 	{
 | |
| 		case EX_DIVES_XML:
 | |
| 			save_dives_logic(qPrintable(fileName + ".ssrf"), false, anonymize);
 | |
| 			break;
 | |
| 		case EX_DIVE_SITES_XML:
 | |
| 			{
 | |
| 				std::vector<const dive_site *> sites = getDiveSitesToExport(false);
 | |
| 				save_dive_sites_logic(qPrintable(fileName + ".xml"), sites.data(), (int)sites.size(), anonymize);
 | |
| 				break;
 | |
| 			}
 | |
| 		default:
 | |
| 			qDebug() << "export to unknown type " << type << " using " << dir << " remove names " << anonymize;
 | |
| 			break;
 | |
| 	}
 | |
| }
 | |
| #endif
 | |
| 
 | |
| void QMLManager::exportToWEB(export_types type, QString userId, QString password, bool anonymize)
 | |
| {
 | |
| 	switch (type)
 | |
| 	{
 | |
| 		case EX_DIVELOGS_DE:
 | |
| 			uploadDiveLogsDE::instance()->doUpload(false, userId, password);
 | |
| 			break;
 | |
| 		case EX_DIVESHARE:
 | |
| 			uploadDiveShare::instance()->doUpload(false, userId, anonymize);
 | |
| 			break;
 | |
| 		default:
 | |
| 			qDebug() << "upload to unknown type " << type << " using " << userId << "/" <<  password << " remove names " << anonymize;
 | |
| 			break;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void QMLManager::shareViaEmail(export_types type, bool anonymize)
 | |
| {
 | |
| #if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
 | |
| 	QString fileName = appLogFileName;
 | |
| #else
 | |
| 	QString fileName = system_default_directory();
 | |
| #endif
 | |
| 	QString body;
 | |
| 	switch (type) {
 | |
| 	case EX_DIVES_XML:
 | |
| 		fileName.replace("subsurface.log", "subsurface.ssrf");
 | |
| 		if (save_dives_logic(qPrintable(fileName), false, anonymize) == 0) {
 | |
| 			// ok, we have a file, let's send it
 | |
| 			body = "Subsurface dive log data";
 | |
| 		} else {
 | |
| 			appendTextToLog("failure to save dive log, aborting attempt to send via email");
 | |
| 		}
 | |
| 		break;
 | |
| 	case EX_DIVE_SITES_XML:
 | |
| 		fileName.replace("subsurface.log", "subsurface_divesites.xml");
 | |
| 		{ // need a block so the compiler doesn't complain about creating the sites variable here
 | |
| 			std::vector<const dive_site *> sites = getDiveSitesToExport(false);
 | |
| 			if (save_dive_sites_logic(qPrintable(fileName), sites.data(), (int)sites.size(), anonymize) == 0) {
 | |
| 				// ok, we have a file, let's send it
 | |
| 				body = "Subsurface dive site data";
 | |
| 			} else {
 | |
| 				appendTextToLog("failure to save dive site data, aborting attempt to send via email");
 | |
| 			}
 | |
| 		}
 | |
| 		break;
 | |
| 	default:
 | |
| 		qDebug() << "cannot export type " << type << " via email";
 | |
| 		return;
 | |
| 	}
 | |
| #if defined(Q_OS_ANDROID)
 | |
| 	// let's use our nifty Java shareViaEmail function
 | |
| 	QAndroidJniObject activity = QtAndroid::androidActivity();
 | |
| 	if (activity.isValid()) {
 | |
| 		QAndroidJniObject attachmentPath = QAndroidJniObject::fromString(fileName);
 | |
| 		QAndroidJniObject subject = QAndroidJniObject::fromString("Subsurface export");
 | |
| 		QAndroidJniObject bodyString = QAndroidJniObject::fromString(body);
 | |
| 		QAndroidJniObject emptyString = QAndroidJniObject::fromString("");
 | |
| 		bool success = activity.callMethod<jboolean>("shareViaEmail",
 | |
| 					"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Z", // five string arguments, return bool
 | |
| 					subject.object<jstring>(), emptyString.object<jstring>(), bodyString.object<jstring>(),
 | |
| 							     attachmentPath.object<jstring>(), emptyString.object<jstring>());
 | |
| 		qDebug() << __FUNCTION__ << "shareViaEmail" << (success ? "succeeded" : "failed");
 | |
| 	}
 | |
| #elif defined(Q_OS_IOS)
 | |
| 	// call into objC++ code to share on iOS
 | |
| 	QString subject("Subsurface export");
 | |
| 	QString emptyString("");
 | |
| 	iosshare.shareViaEmail(subject, emptyString, body, fileName, emptyString);
 | |
| #else
 | |
| 	appendTextToLog("on a mobile platform this would send" + fileName + "via email with body" + body);
 | |
| #endif
 | |
| }
 | |
| void QMLManager::uploadFinishSlot(bool success, const QString &text, const QByteArray &html)
 | |
| {
 | |
| 	emit uploadFinish(success, text);
 | |
| }
 | |
| 
 | |
| qPrefCloudStorage::cloud_status QMLManager::oldStatus() const
 | |
| {
 | |
| 	return m_oldStatus;
 | |
| }
 | |
| 
 | |
| void QMLManager::setOldStatus(const qPrefCloudStorage::cloud_status value)
 | |
| {
 | |
| 	if (m_oldStatus != value) {
 | |
| 		m_oldStatus = value;
 | |
| 		emit oldStatusChanged();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void QMLManager::rememberOldStatus()
 | |
| {
 | |
| 	setOldStatus((qPrefCloudStorage::cloud_status)qPrefCloudStorage::cloud_verification_status());
 | |
| }
 | |
| 
 | |
| void QMLManager::divesChanged(const QVector<dive *> &dives, DiveField field)
 | |
| {
 | |
| 	Q_UNUSED(field)
 | |
| 	for (struct dive *d: dives) {
 | |
| 		qDebug() << "dive #" << d->number << "changed, cache is" << (dive_cache_is_valid(d) ? "valid" : "invalidated");
 | |
| 		// a brute force way to deal with that would of course be to call
 | |
| 		// invalidate_dive_cache(d);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| QString QMLManager::getUndoText() const
 | |
| {
 | |
| 	QString undoText = Command::getUndoStack()->undoText();
 | |
| 	return undoText;
 | |
| }
 | |
| 
 | |
| QString QMLManager::getRedoText() const
 | |
| {
 | |
| 	QString redoText = Command::getUndoStack()->redoText();
 | |
| 	return redoText;
 | |
| }
 | |
| 
 | |
| void QMLManager::setDiveListProcessing(bool value)
 | |
| {
 | |
| 	if (m_diveListProcessing != value) {
 | |
| 		m_diveListProcessing = value;
 | |
| 		emit diveListProcessingChanged();
 | |
| 	}
 | |
| 
 | |
| }
 | |
| 
 | |
| void QMLManager::importCacheRepo(QString repo)
 | |
| {
 | |
| 	struct divelog log;
 | |
| 	QString repoPath = QString("%1/cloudstorage/%2").arg(system_default_directory()).arg(repo);
 | |
| 	appendTextToLog(QString("importing %1").arg(repoPath));
 | |
| 	parse_file(qPrintable(repoPath), &log);
 | |
| 	add_imported_dives(&log, IMPORT_MERGE_ALL_TRIPS);
 | |
| 	changesNeedSaving();
 | |
| }
 | |
| 
 | |
| QStringList QMLManager::cloudCacheList() const
 | |
| {
 | |
| 	QDir localCacheDir(QString("%1/cloudstorage/").arg(system_default_directory()));
 | |
| 	QStringList dirs = localCacheDir.entryList();
 | |
| 	QStringList result;
 | |
| 	foreach(QString dir, dirs) {
 | |
| 		QString originsDir = QString("%1/cloudstorage/%2/.git/refs/remotes/origin/").arg(system_default_directory()).arg(dir);
 | |
| 		QDir remote(originsDir);
 | |
| 		if (dir == "localrepo") {
 | |
| 			result << QString("localrepo[master]");
 | |
| 		} else {
 | |
| 			foreach(QString branch, remote.entryList().filter(QRegularExpression("...+")))
 | |
| 				result << QString("%1[%2]").arg(dir).arg(branch);
 | |
| 		}
 | |
| 	}
 | |
| 	return result;
 | |
| }
 | |
| 
 | |
| void QMLManager::updateHaveLocalChanges(bool status)
 | |
| {
 | |
| 	localChanges = status;
 | |
| 	emit syncStateChanged();
 | |
| }
 | |
| 
 | |
| // show the state of the dive list
 | |
| QString QMLManager::getSyncState() const
 | |
| {
 | |
| 	if (unsavedChanges())
 | |
| 		return tr("(unsaved changes in memory)");
 | |
| 	if (localChanges)
 | |
| 		return tr("(changes synced locally)");
 | |
| 	return tr("(synced with cloud)");
 | |
| }
 | |
| 
 | |
| // show the state of the cloud login
 | |
| QString QMLManager::getPasswordState() const
 | |
| {
 | |
| 	auto credStatus = qPrefCloudStorage::cloud_verification_status();
 | |
| 	if (credStatus == qPrefCloudStorage::CS_INCORRECT_USER_PASSWD)
 | |
| 		return tr("(incorrect cloud email or password)");
 | |
| 	return QString();
 | |
| }
 |