Drag and Drop Images onto a ride

.. drag and dropping images into a ride will store them
   in the media folder and add the filename to the "Images"
   metadata tag which contains a list separated by newlines.

   The metadata does not include the full path since we may
   change the path in future releases
This commit is contained in:
Mark Liversedge
2024-01-30 13:59:40 +00:00
parent 50c305b7f2
commit b3df633787
6 changed files with 152 additions and 2 deletions

View File

@@ -1628,3 +1628,99 @@ RideItem::xdataMatch(QString name, QString series, QString &mname, QString &mser
}
return false;
}
bool
RideItem::addImage(QString filename)
{
// get list of images
QStringList list=images();
// filename should be full path since we need to copy into the media folder
// we will rename it with a number at the front that cycles
QFileInfo fi(filename);
if (!fi.exists() || !fi.isReadable() || !fi.isFile()) return false;
// lets generate a target filename
bool isduplicate=true;
QString targetname;
for(int prefix=1; isduplicate; prefix++) {
targetname=QString("%1-%2").arg(prefix).arg(fi.fileName());
isduplicate = list.contains(targetname);
if (!isduplicate) {
// lets check it doesn't already exist on disk
QFileInfo ti(context->athlete->home->media().canonicalPath() + "/" + targetname);
isduplicate = ti.exists();
}
}
// lets copy from source full path, to media folder
if (QFile::copy(filename, QString("%1/%2").arg(context->athlete->home->media().canonicalPath()).arg(targetname))) {
// success !
list << targetname;
ride_->setTag("Images", list.join("\n"));
// make sure it gets saved !
setDirty(true);
return true;
}
return false;
}
bool
RideItem::removeImage(QString filename)
{
// if it really exists then zap it!
QFileInfo fi(context->athlete->home->media().canonicalPath() + "/" + filename);
if (fi.exists() && fi.isReadable() && fi.isFile()) {
QFile::remove(fi.absoluteFilePath());
// remove from metadata
QStringList list=images();
int index= list.indexOf(filename);
if (index != -1) list.removeAt(index);
ride_->setTag("Images", list.join("\n"));
// set dirty
setDirty(true);
return true;
}
return false;
}
// read from the metadata but also check they actually exist
QStringList
RideItem::images() const
{
QStringList exist;
foreach(QString filename, ride_->getTag("Images", "").split("\n")) {
QFileInfo fi(context->athlete->home->media().canonicalPath() + "/" + filename);
if (fi.exists() && fi.isReadable() && fi.isFile()) exist << filename;
}
return exist;
}
// we hide the implementation of directory here since we may change our
// minds later and store in sub-folders etc
QStringList RideItem::imagePaths() const
{
QStringList paths;
foreach(QString filename, images())
paths << context->athlete->home->media().canonicalPath() + "/" + filename;
return paths;
}
int
RideItem::importImages(QStringList files)
{
int count=0;
foreach(QString file, files) {
if (addImage(file) == true) count++;
}
return count;
}

View File

@@ -129,6 +129,18 @@ class RideItem : public QObject
// as a well formatted string
QString getStringForSymbol(QString name, bool useMetricUnits=true);
// add an image to the ride- store it in the media folder and add metadata for it
// note that in all cases the return list is the filename only, the location they
// are stored is managed here (callers should not implement filepath)
bool addImage(QString filename);
bool removeImage(QString filename);
QStringList images() const;
// these two are path based
QStringList imagePaths() const;
int importImages(QStringList files);
// access the metadata
QString getText(QString name, QString fallback) const;
bool hasText(QString name) const;

View File

@@ -623,6 +623,21 @@ heatcolor(double value)
return returning;
}
// setup list of image extensions we will support
static QVector<QString> imageexts;
static bool initextensions() { imageexts << ".png" << ".gif" << ".jpeg" << ".jpg" << ".bmp"; return true; }
static bool initexts = initextensions();
// is the file an image?
bool isImage(QString filename)
{
QString lowername = filename.toLower();
foreach(QString ext, imageexts) {
if (lowername.endsWith(ext)) return true;
}
return false;
}
// used std::sort, std::lower_bound et al
bool doubledescend(const double &s1, const double &s2) { return s1 > s2; }

View File

@@ -62,6 +62,9 @@ namespace Utils
double heat(double min, double max, double value); // return value normalised between 0-1 for min/max
QColor heatcolor(double value); // return color hear for value between 0 and 1
// media
bool isImage(QString);
// used std::sort, std::lower_bound et al
struct comparedouble { bool operator()(const double p1, const double p2) { return p1 < p2; } };
struct compareqstring { bool operator()(const QString p1, const QString p2) { return p1 < p2; } };

View File

@@ -1600,11 +1600,11 @@ MainWindow::dropEvent(QDropEvent *event)
// is this a chart file ?
QStringList filenames;
QList<LTMSettings> imported;
QStringList list, workouts;
QStringList list, workouts, images;
for(int i=0; i<urls.count(); i++) {
QString filename = QFileInfo(urls.value(i).toLocalFile()).absoluteFilePath();
fprintf(stderr, "%s\n", filename.toStdString().c_str()); fflush(stderr);
//fprintf(stderr, "%s\n", filename.toStdString().c_str()); fflush(stderr);
if (filename.endsWith(".gchart", Qt::CaseInsensitive)) {
// add to the list of charts to import
@@ -1625,6 +1625,9 @@ MainWindow::dropEvent(QDropEvent *event)
xmlReader.parse(source);
imported += handler.getSettings();
} else if (Utils::isImage(filename)) {
images << filename;
// Look for Workout files only in Train view
} else if (currentAthleteTab->currentView() == 3 && ErgFile::isWorkout(filename)) {
workouts << filename;
@@ -1658,6 +1661,9 @@ MainWindow::dropEvent(QDropEvent *event)
// import workouts
if (workouts.count()) Library::importFiles(currentAthleteTab->context, workouts, true);
// import images (these will be attached to the current ride)
if (images.count()) importImages(images);
// if there is anything left, process based upon view...
if (filenames.count()) {
@@ -1668,6 +1674,21 @@ MainWindow::dropEvent(QDropEvent *event)
return;
}
void
MainWindow::importImages(QStringList list)
{
// we need to be on activities view and with a current
// ride otherwise we just ignore the list
if (currentAthleteTab->currentView() != 1 || currentAthleteTab->context->ride == NULL) {
QMessageBox::critical(this, tr("Import Images Failed"), tr("You can only import images on the activities view with an activity selected."));
return;
}
// lets import them
int count = currentAthleteTab->context->ride->importImages(list);
QMessageBox::information(this, tr("Import Images to Activity"), QString(tr("%1 images imported.")).arg(count));
}
void
MainWindow::importCharts(QStringList list)
{

View File

@@ -157,6 +157,9 @@ class MainWindow : public QMainWindow
// chart importing
void importCharts(QStringList);
// import images into the current ride
void importImages(QStringList);
// open and closing windows and tabs
void closeWindow();