1 /* GUI_LocalLibrary.cpp */
2 
3 /* Copyright (C) 2011-2020 Michael Lugmair (Lucio Carreras)
4  *
5  * This file is part of sayonara player
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation, either version 3 of the License, or
10  * (at your option) any later version.
11 
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16 
17  * You should have received a copy of the GNU General Public License
18  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
19  */
20 
21 /*
22  * GUI_LocalLibrary.cpp
23  *
24  *  Created on: Apr 24, 2011
25  *      Author: Michael Lugmair (Lucio Carreras)
26  */
27 
28 #include "GUI_LocalLibrary.h"
29 #include "GUI_ImportDialog.h"
30 
31 #include "Gui/Library/Utils/GUI_DeleteDialog.h"
32 #include "Gui/Library/ui_GUI_LocalLibrary.h"
33 
34 #include "Gui/Library/CoverView/GUI_CoverView.h"
35 #include "Gui/Library/Utils/DirChooserDialog.h"
36 #include "Gui/Library/Utils/GUI_ReloadLibraryDialog.h"
37 #include "Gui/Library/Utils/GUI_LibraryInfoBox.h"
38 #include "Gui/Library/Utils/LocalLibraryMenu.h"
39 
40 #include "Gui/Utils/Icons.h"
41 #include "Gui/Utils/Style.h"
42 #include "Gui/Utils/PreferenceAction.h"
43 #include "Gui/Utils/Shortcuts/ShortcutHandler.h"
44 #include "Gui/Utils/Shortcuts/Shortcut.h"
45 
46 #include "Components/Library/LocalLibrary.h"
47 #include "Components/LibraryManagement/LibraryManager.h"
48 
49 #include "Utils/Utils.h"
50 #include "Utils/FileUtils.h"
51 #include "Utils/Set.h"
52 #include "Utils/Language/Language.h"
53 #include "Utils/Settings/Settings.h"
54 #include "Utils/Library/LibraryInfo.h"
55 
56 #include <QDir>
57 #include <QFileDialog>
58 #include <QStringList>
59 #include <QFileSystemWatcher>
60 
61 namespace
62 {
63 	// ui->swViewType
64 	enum AlbumViewIndex
65 	{
66 		ArtistAlbumTableView = 0,
67 		AlbumCoverView = 1,
68 		DirectoryView = 2
69 	};
70 
71 	// ui->swReload
72 	enum ReloadWidgetIndex
73 	{
74 		TableView = 0,
75 		ReloadView = 1,
76 		NoDirView = 2
77 	};
78 
79 	ReloadWidgetIndex getReloadWidgetIndex(const Library::Info& libraryInfo, bool isLibraryEmpty)
80 	{
81 		const auto pathExists = Util::File::exists(libraryInfo.path());
82 		if(!pathExists)
83 		{
84 			return ReloadWidgetIndex::NoDirView;
85 		}
86 
87 		else if(isLibraryEmpty)
88 		{
89 			return ReloadWidgetIndex::ReloadView;
90 		}
91 
92 		return ReloadWidgetIndex::TableView;
93 	}
94 }
95 
96 using namespace Library;
97 
98 struct GUI_LocalLibrary::Private
99 {
100 	LocalLibrary* library;
101 	LocalLibraryMenu* libraryMenu;
102 
103 	Private(LocalLibrary* localLibrary, GUI_LocalLibrary* parent) :
104 		library {localLibrary},
105 		libraryMenu {new LocalLibraryMenu(library->info().name(), library->info().path(), parent)} {}
106 };
107 
108 GUI_LocalLibrary::GUI_LocalLibrary(LibraryId id, Library::Manager* libraryManager, QWidget* parent) :
109 	GUI_AbstractLibrary(libraryManager->libraryInstance(id), parent)
110 {
111 	m = Pimpl::make<Private>(libraryManager->libraryInstance(id), this);
112 
113 	setupParent(this, &ui);
114 	this->setFocusProxy(ui->leSearch);
115 
116 	ui->directoryView->init(libraryManager, id);
117 
118 	connect(m->library, &LocalLibrary::sigReloadingLibrary, this, &GUI_LocalLibrary::progressChanged);
119 	connect(m->library, &LocalLibrary::sigReloadingLibraryFinished, this, &GUI_LocalLibrary::reloadFinished);
120 	connect(m->library, &LocalLibrary::sigReloadingLibraryFinished, ui->lvGenres, &GenreView::reloadGenres);
121 	connect(m->library, &LocalLibrary::sigAllTracksLoaded, this, &GUI_LocalLibrary::tracksLoaded);
122 	connect(m->library, &LocalLibrary::sigImportDialogRequested, this, &GUI_LocalLibrary::importDialogRequested);
123 	connect(m->library, &LocalLibrary::sigRenamed, this, &GUI_LocalLibrary::nameChanged);
124 	connect(m->library, &LocalLibrary::sigPathChanged, this, &GUI_LocalLibrary::pathChanged);
125 
126 	connect(ui->tvAlbums, &AlbumView::sigDiscPressed, m->library, &LocalLibrary::changeCurrentDisc);
127 	connect(ui->lvGenres, &GenreView::sigSelectedChanged, this, &GUI_LocalLibrary::genreSelectionChanged);
128 	connect(ui->lvGenres, &GenreView::sigInvalidGenreSelected, this, &GUI_LocalLibrary::invalidGenreSelected);
129 	connect(ui->lvGenres, &GenreView::sigProgress, this, &GUI_LocalLibrary::progressChanged);
130 
131 	connect(m->libraryMenu, &LocalLibraryMenu::sigPathChanged, m->library, &LocalLibrary::setLibraryPath);
132 	connect(m->libraryMenu, &LocalLibraryMenu::sigNameChanged, m->library, &LocalLibrary::setLibraryName);
133 	connect(m->libraryMenu, &LocalLibraryMenu::sigImportFile, this, &GUI_LocalLibrary::importFilesRequested);
134 	connect(m->libraryMenu, &LocalLibraryMenu::sigImportFolder, this, &GUI_LocalLibrary::importDirsRequested);
135 	connect(m->libraryMenu, &LocalLibraryMenu::sigInfo, this, &GUI_LocalLibrary::showInfoBox);
136 	connect(m->libraryMenu, &LocalLibraryMenu::sigReloadLibrary, this, &GUI_LocalLibrary::reloadLibraryRequested);
137 
138 	connect(ui->btnScanForFiles, &QPushButton::clicked, this, &GUI_LocalLibrary::reloadLibraryDeepRequested);
139 	connect(ui->btnImportDirectories, &QPushButton::clicked, this, &GUI_LocalLibrary::importDirsRequested);
140 
141 	connect(ui->splitterArtistAlbum, &QSplitter::splitterMoved, this, &GUI_LocalLibrary::splitterArtistMoved);
142 	connect(ui->splitterTracks, &QSplitter::splitterMoved, this, &GUI_LocalLibrary::splitterTracksMoved);
143 	connect(ui->splitterGenre, &QSplitter::splitterMoved, this, &GUI_LocalLibrary::splitterGenreMoved);
144 
145 	connect(ui->tvAlbums, &ItemView::sigReloadClicked, this, &GUI_LocalLibrary::reloadLibraryRequested);
146 	connect(ui->tvArtists, &ItemView::sigReloadClicked, this, &GUI_LocalLibrary::reloadLibraryRequested);
147 	connect(ui->tvTracks, &ItemView::sigReloadClicked, this, &GUI_LocalLibrary::reloadLibraryRequested);
148 
149 	ui->extensionBar->init(m->library);
150 	ui->lvGenres->init(m->library);
151 	ui->btnView->setOverrideText(true);
152 
153 	ListenSetting(Set::Lib_ViewType, GUI_LocalLibrary::switchViewType);
154 	ListenSetting(Set::Lib_ShowFilterExtBar, GUI_LocalLibrary::tracksLoaded);
155 
156 	auto* shortcutHandler = ShortcutHandler::instance();
157 	auto shortcut = shortcutHandler->shortcut(ShortcutIdentifier::CoverView);
158 	shortcut.connect(this, [this]() {
159 		this->selectNextViewType();
160 	});
161 
162 	const auto libraryPath = m->library->info().path();
163 
164 	const auto paths = QStringList()
165 		<< libraryPath
166 		<< Util::File::getParentDirectory(libraryPath);
167 
168 	auto* action = new Gui::LibraryPreferenceAction(this);
169 	connect(ui->btnLibraryPreferences, &QPushButton::clicked, action, &QAction::trigger);
170 
171 	auto* fileSystemWatcher = new QFileSystemWatcher(this);
172 	fileSystemWatcher->addPaths(paths); // old qt versions don't have the new constructor
173 	connect(fileSystemWatcher, &QFileSystemWatcher::directoryChanged, this, [this]([[maybe_unused]] const auto& path) {
174 		this->checkMainSplitterStatus();
175 	});
176 }
177 
178 GUI_LocalLibrary::~GUI_LocalLibrary()
179 {
180 	delete ui;
181 	ui = nullptr;
182 }
183 
184 void GUI_LocalLibrary::checkViewState()
185 {
186 	if(this->isVisible())
187 	{
188 		checkMainSplitterStatus();
189 
190 		if(!m->library->isReloading())
191 		{
192 			checkFileExtensionBar();
193 		}
194 	}
195 }
196 
197 void GUI_LocalLibrary::checkMainSplitterStatus()
198 {
199 	const auto isLibraryEmpty = m->library->isEmpty();
200 	const auto libraryInfo = m->library->info();
201 
202 	const auto index = getReloadWidgetIndex(libraryInfo, isLibraryEmpty);
203 	ui->swReload->setCurrentIndex(static_cast<int>(index));
204 
205 	const auto inLibraryState = (index == ReloadWidgetIndex::TableView);
206 	ui->leSearch->setVisible(inLibraryState);
207 	ui->btnScanForFiles->setVisible(!inLibraryState);
208 	ui->btnImportDirectories->setVisible(!inLibraryState);
209 
210 	if(index == ReloadWidgetIndex::NoDirView)
211 	{
212 		ui->labDir->setText(libraryInfo.path());
213 	}
214 
215 	else
216 	{
217 		const auto isReloading = m->library->isReloading();
218 
219 		ui->pbProgress->setVisible(isReloading);
220 		ui->labProgress->setVisible(isReloading);
221 		ui->widgetReload->setVisible(isReloading || isLibraryEmpty);
222 		m->libraryMenu->setLibraryEmpty(isLibraryEmpty);
223 	}
224 }
225 
226 void GUI_LocalLibrary::checkFileExtensionBar()
227 {
228 	ui->extensionBar->refresh();
229 	ui->extensionBar->setVisible
230 		(
231 			GetSetting(Set::Lib_ShowFilterExtBar) &&
232 			ui->extensionBar->hasExtensions()
233 		);
234 }
235 
236 void GUI_LocalLibrary::tracksLoaded()
237 {
238 	checkViewState();
239 
240 	const auto info = m->library->info();
241 
242 	ui->labLibraryName->setText(info.name());
243 	ui->labPath->setText(Util::createLink(info.path(), Style::isDark()));
244 
245 	ui->btnScanForFiles->setIcon(Gui::Icons::icon(Gui::Icons::Refresh));
246 	ui->btnImportDirectories->setIcon(Gui::Icons::icon(Gui::Icons::Folder));
247 }
248 
249 void GUI_LocalLibrary::clearSelections()
250 {
251 	GUI_AbstractLibrary::clearSelections();
252 
253 	if(ui->coverView)
254 	{
255 		ui->coverView->clearSelections();
256 	}
257 
258 	ui->lvGenres->clearSelection();
259 }
260 
261 void GUI_LocalLibrary::invalidGenreSelected()
262 {
263 	ui->leSearch->setInvalidGenreMode(true);
264 	ui->leSearch->setCurrentMode(Filter::Genre);
265 	ui->leSearch->setText(GenreView::invalidGenreName());
266 
267 	searchTriggered();
268 
269 	ui->leSearch->setInvalidGenreMode(false);
270 }
271 
272 void GUI_LocalLibrary::genreSelectionChanged(const QStringList& genres)
273 {
274 	if(!genres.isEmpty())
275 	{
276 		ui->leSearch->setInvalidGenreMode(false);
277 		ui->leSearch->setCurrentMode(Filter::Genre);
278 		ui->leSearch->setText(genres.join(","));
279 
280 		searchTriggered();
281 	}
282 }
283 
284 TrackDeletionMode GUI_LocalLibrary::showDeleteDialog(int track_count)
285 {
286 	GUI_DeleteDialog dialog(track_count, this);
287 	dialog.exec();
288 
289 	return dialog.answer();
290 }
291 
292 void GUI_LocalLibrary::progressChanged(const QString& type, int progress)
293 {
294 	checkViewState();
295 
296 	ui->pbProgress->setMaximum((progress > 0) ? 100 : 0);
297 	ui->pbProgress->setValue(progress);
298 	ui->labProgress->setText
299 		(
300 			this->fontMetrics().elidedText(type, Qt::ElideRight, ui->widgetReload->width() / 2)
301 		);
302 }
303 
304 void GUI_LocalLibrary::reloadLibraryRequested()
305 {
306 	reloadLibraryRequestedWithQuality(ReloadQuality::Unknown);
307 }
308 
309 void GUI_LocalLibrary::reloadLibraryDeepRequested()
310 {
311 	reloadLibraryRequestedWithQuality(ReloadQuality::Accurate);
312 }
313 
314 void GUI_LocalLibrary::reloadLibraryRequestedWithQuality(ReloadQuality quality)
315 {
316 	if(quality == ReloadQuality::Unknown)
317 	{
318 		auto* dialog = new GUI_LibraryReloadDialog(m->library->info().name(), this);
319 		connect(dialog, &GUI_LibraryReloadDialog::sigAccepted, this, &GUI_LocalLibrary::reloadLibraryAccepted);
320 
321 		dialog->setQuality(quality);
322 		dialog->show();
323 	}
324 
325 	else
326 	{
327 		reloadLibrary(quality);
328 	}
329 }
330 
331 void GUI_LocalLibrary::reloadLibraryAccepted(ReloadQuality quality)
332 {
333 	if(sender())
334 	{
335 		sender()->deleteLater();
336 	}
337 
338 	reloadLibrary(quality);
339 }
340 
341 void GUI_LocalLibrary::reloadLibrary(ReloadQuality quality)
342 {
343 	m->libraryMenu->setLibraryBusy(true);
344 	m->library->reloadLibrary(false, quality);
345 }
346 
347 void GUI_LocalLibrary::reloadFinished()
348 {
349 	m->libraryMenu->setLibraryBusy(false);
350 	checkViewState();
351 }
352 
353 void GUI_LocalLibrary::showInfoBox()
354 {
355 	const auto info = m->library->info();
356 	GUI_LibraryInfoBox infoBox(info, this);
357 	infoBox.exec();
358 }
359 
360 void GUI_LocalLibrary::importDirsRequested()
361 {
362 	DirChooserDialog dialog(this);
363 
364 	if(dialog.exec() == QFileDialog::Accepted)
365 	{
366 		const auto dirs = dialog.selectedFiles();
367 		m->library->importFiles(dirs);
368 	}
369 }
370 
371 void GUI_LocalLibrary::importFilesRequested()
372 {
373 	const auto files = QFileDialog::getOpenFileNames
374 		(
375 			nullptr,
376 			Lang::get(Lang::ImportFiles),
377 			QDir::homePath(),
378 			Util::getFileFilter(Util::Extensions(Util::Extension::Soundfile), tr("Audio files"))
379 		);
380 
381 	m->library->importFiles(files);
382 }
383 
384 void GUI_LocalLibrary::nameChanged(const QString& newName)
385 {
386 	m->libraryMenu->refreshName(newName);
387 	ui->labLibraryName->setText(newName);
388 }
389 
390 void GUI_LocalLibrary::pathChanged(const QString& newPath)
391 {
392 	m->libraryMenu->refreshPath(newPath);
393 
394 	if(this->isVisible())
395 	{
396 		reloadLibraryRequestedWithQuality(ReloadQuality::Accurate);
397 		ui->labPath->setText(newPath);
398 	}
399 }
400 
401 void GUI_LocalLibrary::importDialogRequested(const QString& targetDirectory)
402 {
403 	if(this->isVisible())
404 	{
405 		auto* uiImporter = new GUI_ImportDialog(m->library, true, this);
406 		uiImporter->setTargetDirectory(targetDirectory);
407 
408 		connect(uiImporter, &Gui::Dialog::sigClosed, uiImporter, &QObject::deleteLater);
409 		uiImporter->show();
410 	}
411 }
412 
413 void GUI_LocalLibrary::splitterArtistMoved([[maybe_unused]] int pos, [[maybe_unused]] int idx)
414 {
415 	const auto data = ui->splitterArtistAlbum->saveState();
416 	SetSetting(Set::Lib_SplitterStateArtist, data);
417 }
418 
419 void GUI_LocalLibrary::splitterTracksMoved([[maybe_unused]] int pos, [[maybe_unused]] int idx)
420 {
421 	const auto data = ui->splitterTracks->saveState();
422 	SetSetting(Set::Lib_SplitterStateTrack, data);
423 }
424 
425 void GUI_LocalLibrary::splitterGenreMoved([[maybe_unused]] int pos, [[maybe_unused]] int idx)
426 {
427 	const auto data = ui->splitterGenre->saveState();
428 	SetSetting(Set::Lib_SplitterStateGenre, data);
429 }
430 
431 void GUI_LocalLibrary::switchViewType()
432 {
433 	const auto viewType = GetSetting(Set::Lib_ViewType);
434 	switch(viewType)
435 	{
436 		case Library::ViewType::CoverView:
437 			if(!ui->coverView->isInitialized())
438 			{
439 				ui->coverView->init(m->library);
440 				connect(ui->coverView, &GUI_CoverView::sigDeleteClicked, this, &GUI_LocalLibrary::itemDeleteClicked);
441 				connect(ui->coverView,
442 				        &GUI_CoverView::sigReloadClicked,
443 				        this,
444 				        &GUI_LocalLibrary::reloadLibraryRequested);
445 			}
446 
447 			if(m->library->isLoaded() && (!m->library->selectedArtists().isEmpty()))
448 			{
449 				m->library->selectedArtistsChanged(IndexSet());
450 			}
451 
452 			ui->swViewType->setCurrentIndex(AlbumViewIndex::AlbumCoverView);
453 			break;
454 
455 		case Library::ViewType::FileView:
456 			ui->swViewType->setCurrentIndex(AlbumViewIndex::DirectoryView);
457 			break;
458 
459 		case Library::ViewType::Standard:
460 		default:
461 			ui->swViewType->setCurrentIndex(AlbumViewIndex::ArtistAlbumTableView);
462 			break;
463 	}
464 
465 	ui->swViewType->setFocus();
466 }
467 
468 void GUI_LocalLibrary::selectNextViewType()
469 {
470 	auto viewType = static_cast<int>(GetSetting(Set::Lib_ViewType));
471 	viewType = (viewType + 1) % 3;
472 	SetSetting(Set::Lib_ViewType, ViewType(viewType));
473 }
474 
475 bool GUI_LocalLibrary::hasSelections() const
476 {
477 	return GUI_AbstractLibrary::hasSelections() ||
478 	       (!ui->lvGenres->selectedItems().isEmpty()) ||
479 	       (!ui->coverView->selectedItems().isEmpty());
480 }
481 
482 QList<Filter::Mode> GUI_LocalLibrary::searchOptions() const
483 {
484 	return {Filter::Fulltext, Filter::Filename, Filter::Genre};
485 }
486 
487 void GUI_LocalLibrary::queryLibrary()
488 {
489 	GUI_AbstractLibrary::queryLibrary();
490 	ui->directoryView->setFilterTerm(m->library->filter().filtertext(false).join(""));
491 }
492 
493 void GUI_LocalLibrary::showEvent(QShowEvent* e)
494 {
495 	GUI_AbstractLibrary::showEvent(e);
496 
497 	const QMap<QSplitter*, QByteArray> splitters
498 		{
499 			{ui->splitterArtistAlbum, GetSetting(Set::Lib_SplitterStateArtist)},
500 			{ui->splitterTracks,      GetSetting(Set::Lib_SplitterStateTrack)},
501 			{ui->splitterGenre,       GetSetting(Set::Lib_SplitterStateGenre)}
502 		};
503 
504 	for(auto it = splitters.begin(); it != splitters.end(); it++)
505 	{
506 		if(!it.value().isEmpty())
507 		{
508 			it.key()->restoreState(it.value());
509 		}
510 	}
511 
512 	checkViewState();
513 	m->library->load();
514 }
515 
516 void GUI_LocalLibrary::languageChanged()
517 {
518 	ui->retranslateUi(this);
519 
520 	ui->btnLibraryPreferences->setText(Lang::get(Lang::Preferences));
521 	ui->labGenres->setText(Lang::get(Lang::Genres));
522 	ui->btnScanForFiles->setText(Lang::get(Lang::ScanForFiles));
523 	ui->btnImportDirectories->setText(Lang::get(Lang::ImportDir));
524 
525 	GUI_AbstractLibrary::languageChanged();
526 }
527 
528 void GUI_LocalLibrary::skinChanged()
529 {
530 	GUI_AbstractLibrary::skinChanged();
531 
532 	checkViewState();
533 }
534 
535 // GUI_AbstractLibrary
536 Library::TableView* GUI_LocalLibrary::lvArtist() const { return ui->tvArtists; }
537 
538 Library::TableView* GUI_LocalLibrary::lvAlbum() const { return ui->tvAlbums; }
539 
540 Library::TableView* GUI_LocalLibrary::lvTracks() const { return ui->tvTracks; }
541 
542 QList<QAbstractItemView*> GUI_LocalLibrary::allViews() const
543 {
544 	return {ui->lvGenres, ui->tvAlbums, ui->tvArtists, ui->tvTracks};
545 }
546 
547 Library::SearchBar* GUI_LocalLibrary::leSearch() const { return ui->leSearch; }
548 
549 // LocalLibraryContainer
550 QMenu* GUI_LocalLibrary::menu() const { return m->libraryMenu; }
551 
552 QFrame* GUI_LocalLibrary::headerFrame() const { return ui->headerFrame; }
553