Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(fix UX) Library: add feedback to directory operations (add, remove, relink) #12436

Merged
merged 10 commits into from
May 14, 2024
11 changes: 5 additions & 6 deletions src/coreservices.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ void CoreServices::initialize(QApplication* pApp) {
// the uninitialized singleton instance!
m_pPlayerManager->bindToLibrary(m_pLibrary.get());

bool hasChanged_MusicDir = false;
bool musicDirAdded = false;

if (m_pTrackCollectionManager->internalCollection()->loadRootDirs().isEmpty()) {
// TODO(XXX) this needs to be smarter, we can't distinguish between an empty
Expand All @@ -368,10 +368,9 @@ void CoreServices::initialize(QApplication* pApp) {
tr("Choose music library directory"),
QStandardPaths::writableLocation(
QStandardPaths::MusicLocation));
if (!fd.isEmpty()) {
// adds Folder to database.
m_pLibrary->slotRequestAddDir(fd);
hasChanged_MusicDir = true;
// request to add directory to database.
if (!fd.isEmpty() && m_pLibrary->requestAddDir(fd)) {
musicDirAdded = true;
}
}

Expand Down Expand Up @@ -422,7 +421,7 @@ void CoreServices::initialize(QApplication* pApp) {

// Scan the library directory. Do this after the skinloader has
// loaded a skin, see issue #6625
if (rescan || hasChanged_MusicDir || m_pSettingsManager->shouldRescanLibrary()) {
if (rescan || musicDirAdded || m_pSettingsManager->shouldRescanLibrary()) {
m_pTrackCollectionManager->startLibraryScan();
}

Expand Down
8 changes: 3 additions & 5 deletions src/library/browse/browsefeature.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,6 @@ BrowseFeature::BrowseFeature(
m_proxyModel(&m_browseModel, true),
m_pSidebarModel(new FolderTreeModel(this)),
m_pLastRightClickedItem(nullptr) {
connect(this,
&BrowseFeature::requestAddDir,
pLibrary,
&Library::slotRequestAddDir);
connect(&m_browseModel,
&BrowseTableModel::restoreModelState,
this,
Expand Down Expand Up @@ -165,7 +161,9 @@ void BrowseFeature::slotAddToLibrary() {
return;
}
QString spath = m_pLastRightClickedItem->getData().toString();
emit requestAddDir(spath);
if (!m_pLibrary->requestAddDir(spath)) {
return;
}

QMessageBox msgBox;
msgBox.setIcon(QMessageBox::Warning);
Expand Down
50 changes: 39 additions & 11 deletions src/library/dao/directorydao.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,17 @@ DirectoryDAO::AddResult DirectoryDAO::addDirectory(
if (!newDir.exists() || !newDir.isDir()) {
kLogger.warning()
<< "Failed to add"
<< newDir
<< newDir.location()
<< ": Directory does not exist or is inaccessible";
return AddResult::InvalidOrMissingDirectory;
}
if (!newDir.isReadable()) {
kLogger.warning()
<< "Aborting to to add"
<< newDir.location()
<< ": Directory can not be read";
return AddResult::UnreadableDirectory;
}
const auto newCanonicalLocation = newDir.canonicalLocation();
DEBUG_ASSERT(!newCanonicalLocation.isEmpty());
QList<mixxx::FileInfo> obsoleteChildDirs;
Expand All @@ -67,11 +74,11 @@ DirectoryDAO::AddResult DirectoryDAO::addDirectory(
// Abort to prevent inconsistencies in the database
kLogger.warning()
<< "Aborting to add"
<< newDir
<< newDir.location()
<< ": Loaded directory"
<< oldDir
<< oldDir.location()
<< "does not exist or is inaccessible";
return AddResult::InvalidOrMissingDirectory;
continue;
}
const auto oldCanonicalLocation = oldDir.canonicalLocation();
DEBUG_ASSERT(!oldCanonicalLocation.isEmpty());
Expand Down Expand Up @@ -106,7 +113,7 @@ DirectoryDAO::AddResult DirectoryDAO::addDirectory(
if (RemoveResult::Ok != removeDirectory(oldDir)) {
kLogger.warning()
<< "Failed to remove obsolete child directory"
<< oldDir;
<< oldDir.location();
DEBUG_ASSERT(!"removeDirectory failed");
continue;
}
Expand Down Expand Up @@ -141,11 +148,32 @@ DirectoryDAO::RemoveResult DirectoryDAO::removeDirectory(
return RemoveResult::Ok;
}

QList<RelocatedTrack> DirectoryDAO::relocateDirectory(
std::pair<DirectoryDAO::RelocateResult, QList<RelocatedTrack>> DirectoryDAO::relocateDirectory(
const QString& oldDirectory,
const QString& newDirectory) const {
const mixxx::FileInfo newFileInfo(newDirectory);
DEBUG_ASSERT(oldDirectory == mixxx::FileInfo(oldDirectory).location());
DEBUG_ASSERT(newDirectory == mixxx::FileInfo(newDirectory).location());
DEBUG_ASSERT(newDirectory == newFileInfo.location());

if (!newFileInfo.exists() || !newFileInfo.isDir()) {
kLogger.warning()
<< "Aborting to relocate"
<< oldDirectory
<< ": "
<< newDirectory
<< "does not exist or is inaccessible";
return {RelocateResult::InvalidOrMissingDirectory, {}};
}
if (!newFileInfo.isReadable()) {
kLogger.warning()
<< "Aborting to relocate"
<< oldDirectory
<< ": "
<< newDirectory
<< "can not be read";
return {RelocateResult::UnreadableDirectory, {}};
}

// TODO(rryan): This method could use error reporting. It can fail in
// mysterious ways for example if a track in the oldDirectory also has a zombie
// track location in newDirectory then the replace query will fail because the
Expand All @@ -158,7 +186,7 @@ QList<RelocatedTrack> DirectoryDAO::relocateDirectory(
if (!query.exec()) {
LOG_FAILED_QUERY(query) << "could not relocate directory"
<< oldDirectory << "to" << newDirectory;
return {};
return {RelocateResult::SqlError, {}};
}

// Appending '/' is required to disambiguate files from parent
Expand All @@ -174,7 +202,7 @@ QList<RelocatedTrack> DirectoryDAO::relocateDirectory(
query.bindValue(":oldDirectoryPrefix", oldDirectoryPrefix);
if (!query.exec()) {
LOG_FAILED_QUERY(query) << "could not relocate path of tracks";
return {};
return {RelocateResult::SqlError, {}};
}

QList<DbId> loc_ids;
Expand Down Expand Up @@ -208,10 +236,10 @@ QList<RelocatedTrack> DirectoryDAO::relocateDirectory(
query.bindValue(QStringLiteral(":id"), loc_ids.at(i).toVariant());
if (!query.exec()) {
LOG_FAILED_QUERY(query) << "could not relocate path of track";
return {};
return {RelocateResult::SqlError, relocatedTracks};
}
}

qDebug() << "Relocated tracks:" << relocatedTracks.size();
return relocatedTracks;
return {RelocateResult::Ok, relocatedTracks};
}
9 changes: 8 additions & 1 deletion src/library/dao/directorydao.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class DirectoryDAO : public DAO {
Ok,
AlreadyWatching,
InvalidOrMissingDirectory,
UnreadableDirectory,
SqlError,
};
AddResult addDirectory(const mixxx::FileInfo& newDir) const;
Expand All @@ -28,8 +29,14 @@ class DirectoryDAO : public DAO {
};
RemoveResult removeDirectory(const mixxx::FileInfo& oldDir) const;

enum class RelocateResult {
Ok,
InvalidOrMissingDirectory,
UnreadableDirectory,
SqlError,
};
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To reduce tr strings for the dialogs we could merge all ~Result types and forward them to a dialog function in Library.

// TODO: Move this function out of the DAO
QList<RelocatedTrack> relocateDirectory(
std::pair<RelocateResult, QList<RelocatedTrack>> relocateDirectory(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could also be a struct to make it more compact, but that appearantly that requires some extras to make it work with std::tie in TrackCollection.
Didn't test it.

struct RelocateResult {
    Result result;
    QList<RelocatedTrack> trackList;
    template<typename T, typename U>
    operator std::tuple<T, U>();
};

const QString& oldDirectory,
const QString& newDirectory) const;
};
111 changes: 75 additions & 36 deletions src/library/library.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -567,7 +567,7 @@ void Library::onSkinLoadFinished() {
m_pSidebarModel->activateDefaultSelection();
}

void Library::slotRequestAddDir(const QString& dir) {
bool Library::requestAddDir(const QString& dir) {
// We only call this method if the user has picked a new directory via a
// file dialog. This means the system sandboxer (if we are sandboxed) has
// granted us permission to this folder. Create a security bookmark while we
Expand All @@ -577,24 +577,59 @@ void Library::slotRequestAddDir(const QString& dir) {
QDir directory(dir);
Sandbox::createSecurityTokenForDir(directory);

if (!m_pTrackCollectionManager->addDirectory(mixxx::FileInfo(dir))) {
QMessageBox::information(nullptr,
tr("Add Directory to Library"),
tr("Could not add the directory to your library. Either this "
"directory is already in your library or you are currently "
"rescanning your library."));
DirectoryDAO::AddResult result =
m_pTrackCollectionManager->addDirectory(mixxx::FileInfo(dir));
QString error;
switch (result) {
case DirectoryDAO::AddResult::Ok:
break;
case DirectoryDAO::AddResult::AlreadyWatching:
error = tr("This or a parent directory is already in your library.");
break;
case DirectoryDAO::AddResult::InvalidOrMissingDirectory:
error = tr(
"This or a listed directory does not exist or is inaccessible.\n"
"Aborting the operation to avoid library inconsistencies");
break;
case DirectoryDAO::AddResult::UnreadableDirectory:
error = tr(
"This directory can not be read.");
break;
case DirectoryDAO::AddResult::SqlError:
error = tr(
"An unknown error occurred.\n"
"Aborting the operation to avoid library inconsistencies");
break;
default:
return false;
}
// set at least one directory in the config file so that it will be possible
// to downgrade from 1.12
if (m_pConfig->getValueString(kLegacyDirectoryConfigKey).length() < 1) {
m_pConfig->set(kLegacyDirectoryConfigKey, dir);
if (!error.isEmpty()) {
QMessageBox::information(nullptr,
tr("Can't add Directory to Library"),
tr("Could not add <b>%1</b> to your library.\n\n%2")
.arg(directory.absolutePath(), error));
return false;
}

return true;
}

void Library::slotRequestRemoveDir(const QString& dir, LibraryRemovalType removalType) {
bool Library::requestRemoveDir(const QString& dir, LibraryRemovalType removalType) {
// Remove the directory from the directory list.
if (!m_pTrackCollectionManager->removeDirectory(mixxx::FileInfo(dir))) {
return;
DirectoryDAO::RemoveResult result =
m_pTrackCollectionManager->removeDirectory(mixxx::FileInfo(dir));
if (result != DirectoryDAO::RemoveResult::Ok) {
switch (result) {
case DirectoryDAO::RemoveResult::NotFound:
case DirectoryDAO::RemoveResult::SqlError:
QMessageBox::information(nullptr,
tr("Can't remove Directory from Library"),
tr("An unknown error occurred."));
break;
default:
DEBUG_ASSERT(!"unreachable");
}
return false;
}

switch (removalType) {
Expand All @@ -613,32 +648,36 @@ void Library::slotRequestRemoveDir(const QString& dir, LibraryRemovalType remova
DEBUG_ASSERT(!"unreachable");
}

// Also update the config file if necessary so that downgrading is still
// possible.
QString confDir = m_pConfig->getValueString(kLegacyDirectoryConfigKey);

if (QDir(dir) == QDir(confDir)) {
const QList<mixxx::FileInfo> dirList =
m_pTrackCollectionManager->internalCollection()->loadRootDirs();
if (dirList.isEmpty()) {
// Save empty string so that an old version of mixxx knows it has to
// ask for a new directory.
m_pConfig->set(kLegacyDirectoryConfigKey, QString());
} else {
m_pConfig->set(kLegacyDirectoryConfigKey, dirList.first().location());
}
}
return true;
}

void Library::slotRequestRelocateDir(const QString& oldDir, const QString& newDir) {
m_pTrackCollectionManager->relocateDirectory(oldDir, newDir);
bool Library::requestRelocateDir(const QString& oldDir, const QString& newDir) {
DirectoryDAO::RelocateResult result =
m_pTrackCollectionManager->relocateDirectory(oldDir, newDir);
if (result == DirectoryDAO::RelocateResult::Ok) {
return true;
}

// also update the config file if necessary so that downgrading is still
// possible
QString conDir = m_pConfig->getValueString(kLegacyDirectoryConfigKey);
if (oldDir == conDir) {
m_pConfig->set(kLegacyDirectoryConfigKey, newDir);
QString error;
switch (result) {
case DirectoryDAO::RelocateResult::InvalidOrMissingDirectory:
error = tr(
"This directory does not exist or is inaccessible.");
break;
case DirectoryDAO::RelocateResult::UnreadableDirectory:
error = tr(
"This directory can not be read.");
break;
default:
DEBUG_ASSERT(!"unreachable");
}
if (!error.isEmpty()) {
QMessageBox::information(nullptr,
tr("Relink Directory"),
tr("Could not relink <b>%1</b> to <b>%2</b>.\n\n%3")
.arg(oldDir, newDir, error));
}
return false;
}

void Library::setFont(const QFont& font) {
Expand Down
7 changes: 4 additions & 3 deletions src/library/library.h
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ class Library: public QObject {
/// and shows the results by switching the view.
void searchTracksInCollection(const QString& query);

bool requestAddDir(const QString& directory);
bool requestRemoveDir(const QString& directory, LibraryRemovalType removalType);
bool requestRelocateDir(const QString& previousDirectory, const QString& newDirectory);

#ifdef __ENGINEPRIME__
std::unique_ptr<mixxx::LibraryExporter> makeLibraryExporter(QWidget* parent);
#endif
Expand All @@ -111,9 +115,6 @@ class Library: public QObject {
void slotRefreshLibraryModels();
void slotCreatePlaylist();
void slotCreateCrate();
void slotRequestAddDir(const QString& directory);
void slotRequestRemoveDir(const QString& directory, LibraryRemovalType removalType);
void slotRequestRelocateDir(const QString& previousDirectory, const QString& newDirectory);
void onSkinLoadFinished();

signals:
Expand Down
Loading