diff --git a/approot/admin-database.xml b/approot/admin-database.xml
index 4e01c81b..91e9347b 100644
--- a/approot/admin-database.xml
+++ b/approot/admin-database.xml
@@ -60,7 +60,7 @@
@@ -69,42 +69,7 @@
-
-
${tr:Lms.Admin.Database.Status.status}
-
- ${status}
-
-
-
-
-
-
-
-
diff --git a/approot/admin-scannercontroller.xml b/approot/admin-scannercontroller.xml
new file mode 100644
index 00000000..234e298c
--- /dev/null
+++ b/approot/admin-scannercontroller.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
${tr:Lms.Admin.ScannerController.scanner}
+
+
+
+
+
diff --git a/approot/messages.xml b/approot/messages.xml
index ac96275f..eb64a2f4 100644
--- a/approot/messages.xml
+++ b/approot/messages.xml
@@ -54,22 +54,25 @@
Update start time
Weekly
-Cannot get track duration
-Cannot parse file
-Cannot read file
-
-
-Get report
-Last scan
-Not available
-Scanned {1} files in {2} on {3} ({4} errors, {5} duplicates)
-No audio track
-Duplicated file hash
-Duplicated MBID
-Status
-Not scheduled
-Scheduled on {1}
-Scanning {1} of {2} files ({3} %)
+Cannot get track duration
+Cannot parse file
+Cannot read file
+
+
+Force scan now
+Get report
+Last scan
+Not available
+Scanned {1} files in {2} on {3} ({4} errors, {5} duplicates)
+No audio track
+Duplicated file hash
+Duplicated MBID
+Scan now
+Scanner
+Status
+Not scheduled
+Scheduled on {1}
+Scanning {1} of {2} files ({3} %)
New user
diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml
index d28c0b35..fa3ffb37 100644
--- a/approot/messages_fr.xml
+++ b/approot/messages_fr.xml
@@ -54,22 +54,25 @@
Heure de départ de la mise à jour
Toutes les semaines
-Impossible de récupérer la durée de la piste
-Impossible d'analyser le fichier
-Impossible de lire le fichier
-
-
-Rapport
-Dernier scan
-Non disponible
-{1} fichiers scannés en {2} le {3} ({4} erreurs, {5} duplicatas)
-Pas de piste audio
-Hash dupliqué
-MBID dupliqué
-Statut
-Non planifié
-Planifié le {1}
-Scan de {1} fichiers sur {2} ({3} %)
+Impossible de récupérer la durée de la piste
+Impossible d'analyser le fichier
+Impossible de lire le fichier
+
+
+Lancer un scan forcé
+Rapport
+Dernier scan
+Non disponible
+{1} fichiers scannés en {2} le {3} ({4} erreurs, {5} duplicatas)
+Pas de piste audio
+Hash dupliqué
+MBID dupliqué
+Lancer un scan
+Scanner
+Statut
+Non planifié
+Planifié le {1}
+Scan de {1} fichiers sur {2} ({3} %)
Ajouter
@@ -132,7 +135,7 @@
Artistes d'album
-Albums similaires
+Albums similaires
Copyright
Disque {1}
diff --git a/src/libs/scanner/impl/MediaScanner.cpp b/src/libs/scanner/impl/MediaScanner.cpp
index 0529ff4a..ccc83e34 100644
--- a/src/libs/scanner/impl/MediaScanner.cpp
+++ b/src/libs/scanner/impl/MediaScanner.cpp
@@ -257,49 +257,51 @@ MediaScanner::MediaScanner(Database::Db& db)
}
MediaScanner::~MediaScanner()
-{
- if (_running)
- stop();
-}
-
-void
-MediaScanner::restart(void)
{
stop();
- start();
}
void
-MediaScanner::start(void)
+MediaScanner::start()
{
- _running = true;
-
scheduleNextScan();
_ioService.start();
}
void
-MediaScanner::stop(void)
+MediaScanner::stop()
{
- _running = false;
+ _abortScan = true;
_scheduleTimer.cancel();
-
_ioService.stop();
}
void
-MediaScanner::requestImmediateScan()
+MediaScanner::abortScan()
{
+ _abortScan = true;
+
+ _scheduleTimer.cancel();
+ _ioService.stop();
+
+ _abortScan = false;
+ _ioService.start();
+}
+
+void
+MediaScanner::requestImmediateScan(bool force)
+{
+ abortScan();
_ioService.post([=]()
{
- scheduleScan();
+ scheduleScan(force);
});
}
void
-MediaScanner::requestReschedule()
+MediaScanner::requestReload()
{
_ioService.post([=]()
{
@@ -308,7 +310,7 @@ MediaScanner::requestReschedule()
}
MediaScanner::Status
-MediaScanner::getStatus()
+MediaScanner::getStatus() const
{
Status res;
@@ -365,7 +367,7 @@ MediaScanner::scheduleNextScan()
if (nextScanDate.isValid())
{
nextScanDateTime = Wt::WDateTime {nextScanDate, _startTime};
- scheduleScan(nextScanDateTime);
+ scheduleScan(false, nextScanDateTime);
}
{
@@ -384,7 +386,7 @@ MediaScanner::countAllFiles(ScanStats& stats)
exploreFilesRecursive(_mediaDirectory, [&](std::error_code ec, const std::filesystem::path& path)
{
- if (!_running)
+ if (_abortScan)
return false;
if (!ec && isFileSupported(path, _fileExtensions))
@@ -398,13 +400,21 @@ MediaScanner::countAllFiles(ScanStats& stats)
}
void
-MediaScanner::scheduleScan(const Wt::WDateTime& dateTime)
+MediaScanner::scheduleScan(bool force, const Wt::WDateTime& dateTime)
{
+ auto cb {[=](boost::system::error_code ec)
+ {
+ if (ec)
+ return;
+
+ scan(force);
+ }};
+
if (dateTime.isNull())
{
LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan right now";
- _scheduleTimer.expires_from_now(std::chrono::seconds(0));
- _scheduleTimer.async_wait(std::bind(&MediaScanner::scan, this, std::placeholders::_1));
+ _scheduleTimer.expires_from_now(std::chrono::seconds {0});
+ _scheduleTimer.async_wait(cb);
}
else
{
@@ -413,16 +423,13 @@ MediaScanner::scheduleScan(const Wt::WDateTime& dateTime)
LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan at " << std::string(std::ctime(&t));
_scheduleTimer.expires_at(timePoint);
- _scheduleTimer.async_wait(std::bind(&MediaScanner::scan, this, std::placeholders::_1));
+ _scheduleTimer.async_wait(cb);
}
}
void
-MediaScanner::scan(boost::system::error_code err)
+MediaScanner::scan(bool forceScan)
{
- if (err)
- return;
-
scanStarted().emit();
{
@@ -438,8 +445,6 @@ MediaScanner::scan(boost::system::error_code err)
refreshScanSettings();
- bool forceScan {false};
-
LMS_LOG(DBUPDATER, DEBUG) << "Counting files in media directory '" << _mediaDirectory.string() << "'...";
countAllFiles(stats);
LMS_LOG(DBUPDATER, DEBUG) << "-> Nb files = " << stats.filesToScan;
@@ -454,17 +459,17 @@ MediaScanner::scan(boost::system::error_code err)
removeOrphanEntries();
- if (_running)
+ if (!_abortScan)
checkDuplicatedAudioFiles(stats);
// Now update all the track features if needed
fetchTrackFeatures(stats);
- LMS_LOG(DBUPDATER, INFO) << "Scan " << (_running ? "complete" : "aborted") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errors.size() << "), features fetched = " << stats.featuresFetched << "/" << stats.featuresToFetch <<", duplicates = " << stats.duplicates.size();
+ LMS_LOG(DBUPDATER, INFO) << "Scan " << (_abortScan ? "aborted" : "complete") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errors.size() << "), features fetched = " << stats.featuresFetched << "/" << stats.featuresToFetch <<", duplicates = " << stats.duplicates.size();
_dbSession.optimize();
- if (_running)
+ if (!_abortScan)
{
stats.stopTime = Wt::WLocalDateTime::currentDateTime().toUTC();
{
@@ -546,7 +551,7 @@ MediaScanner::fetchTrackFeatures(ScanStats& stats)
for (const TrackInfo& trackToFetch : tracksToFetch)
{
- if (!_running)
+ if (_abortScan)
return;
if (fetchTrackFeatures(trackToFetch.id, trackToFetch.mbid))
@@ -774,7 +779,7 @@ MediaScanner::scanMediaDirectory(const std::filesystem::path& mediaDirectory, bo
{
exploreFilesRecursive(mediaDirectory, [&](std::error_code ec, const std::filesystem::path& path)
{
- if (!_running)
+ if (_abortScan)
return false;
if (ec)
@@ -861,7 +866,7 @@ MediaScanner::removeMissingTracks(ScanStats& stats)
for (const auto& [trackId, trackPath] : trackPaths)
{
- if (!_running)
+ if (_abortScan)
return;
if (!checkFile(trackPath, _mediaDirectory, _fileExtensions))
diff --git a/src/libs/scanner/impl/MediaScanner.hpp b/src/libs/scanner/impl/MediaScanner.hpp
index 8c19defb..3a2c737e 100644
--- a/src/libs/scanner/impl/MediaScanner.hpp
+++ b/src/libs/scanner/impl/MediaScanner.hpp
@@ -52,12 +52,10 @@ class MediaScanner : public IMediaScanner
void start() override;
void stop() override;
- void restart() override;
+ void requestReload() override;
+ void requestImmediateScan(bool force) override;
- void requestImmediateScan() override;
- void requestReschedule() override ;
-
- Status getStatus() override;
+ Status getStatus() const override;
Wt::Signal<>& scanStarted() override { return _sigScanStarted; }
Wt::Signal<>& scanComplete() override { return _sigScanComplete; }
@@ -68,10 +66,12 @@ class MediaScanner : public IMediaScanner
// Job handling
void scheduleNextScan();
- void scheduleScan(const Wt::WDateTime& dateTime = {});
+ void scheduleScan(bool force, const Wt::WDateTime& dateTime = {});
+
+ void abortScan();
// Update database (scheduled callback)
- void scan(boost::system::error_code ec);
+ void scan(bool force);
void scanMediaDirectory( const std::filesystem::path& mediaDirectory, bool forceScan, ScanStats& stats);
bool fetchTrackFeatures(Database::IdType trackId, const UUID& MBID);
@@ -89,7 +89,9 @@ class MediaScanner : public IMediaScanner
void notifyInProgressIfNeeded(const ScanStats& stats);
void notifyInProgress(const ScanStats& stats);
- bool _running {};
+ std::mutex _scanInProgress {};
+ std::atomic _abortScan {};
+
Wt::WIOService _ioService;
boost::asio::system_timer _scheduleTimer {_ioService};
Wt::Signal<> _sigScanStarted;
@@ -100,7 +102,7 @@ class MediaScanner : public IMediaScanner
Database::Session _dbSession;
std::unique_ptr _metadataParser;
- std::shared_mutex _statusMutex;
+ mutable std::shared_mutex _statusMutex;
State _curState {State::NotScheduled};
std::optional _lastCompleteScanStats;
std::optional _inProgressScanStats;
diff --git a/src/libs/scanner/include/scanner/IMediaScanner.hpp b/src/libs/scanner/include/scanner/IMediaScanner.hpp
index 1c0d3fdc..c70358c4 100644
--- a/src/libs/scanner/include/scanner/IMediaScanner.hpp
+++ b/src/libs/scanner/include/scanner/IMediaScanner.hpp
@@ -40,11 +40,10 @@ class IMediaScanner
virtual void start() = 0;
virtual void stop() = 0;
- virtual void restart() = 0;
// Async requests
- virtual void requestImmediateScan() = 0;
- virtual void requestReschedule() = 0;
+ virtual void requestReload() = 0;
+ virtual void requestImmediateScan(bool force) = 0;
enum class State
@@ -56,13 +55,13 @@ class IMediaScanner
struct Status
{
- State currentState {State::NotScheduled};
- Wt::WDateTime nextScheduledScan;
- std::optional lastCompleteScanStats;
+ State currentState {State::NotScheduled};
+ Wt::WDateTime nextScheduledScan;
+ std::optional lastCompleteScanStats;
std::optional inProgressScanStats;
};
- virtual Status getStatus() = 0;
+ virtual Status getStatus() const = 0;
// Called just after scan start
virtual Wt::Signal<>& scanStarted() = 0;
diff --git a/src/libs/subsonic/impl/Scan.cpp b/src/libs/subsonic/impl/Scan.cpp
index 70b2deb0..46231758 100644
--- a/src/libs/subsonic/impl/Scan.cpp
+++ b/src/libs/subsonic/impl/Scan.cpp
@@ -54,7 +54,7 @@ namespace API::Subsonic::Scan
Response
handleStartScan(RequestContext& context)
{
- ServiceProvider::get()->requestImmediateScan();
+ ServiceProvider::get()->requestImmediateScan(false);
Response response {Response::createOkResponse(context)};
response.addNode("scanStatus", createStatusResponseNode());
diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt
index beb8ac4a..269986c3 100644
--- a/src/lms/CMakeLists.txt
+++ b/src/lms/CMakeLists.txt
@@ -10,7 +10,7 @@ add_executable(lms
ui/SettingsView.cpp
ui/TrackStringUtils.cpp
ui/admin/DatabaseSettingsView.cpp
- ui/admin/DatabaseStatus.cpp
+ ui/admin/ScannerController.cpp
ui/admin/InitWizardView.cpp
ui/admin/UserView.cpp
ui/admin/UsersView.cpp
diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp
index 250ceaab..c9efc903 100644
--- a/src/lms/ui/LmsApplication.cpp
+++ b/src/lms/ui/LmsApplication.cpp
@@ -123,6 +123,7 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env,
// Add a resource bundle
messageResourceBundle().use(appRoot() + "admin-database");
messageResourceBundle().use(appRoot() + "admin-initwizard");
+ messageResourceBundle().use(appRoot() + "admin-scannercontroller");
messageResourceBundle().use(appRoot() + "admin-user");
messageResourceBundle().use(appRoot() + "admin-users");
messageResourceBundle().use(appRoot() + "artist");
diff --git a/src/lms/ui/admin/DatabaseSettingsView.cpp b/src/lms/ui/admin/DatabaseSettingsView.cpp
index 5b612ff6..3b734728 100644
--- a/src/lms/ui/admin/DatabaseSettingsView.cpp
+++ b/src/lms/ui/admin/DatabaseSettingsView.cpp
@@ -34,7 +34,7 @@
#include "common/Validators.hpp"
#include "common/ValueStringModel.hpp"
-#include "DatabaseStatus.hpp"
+#include "ScannerController.hpp"
#include "LmsApplication.hpp"
namespace UserInterface {
@@ -220,7 +220,7 @@ DatabaseSettingsView::refreshView()
Wt::WPushButton *discardBtn = t->bindWidget("discard-btn", std::make_unique(Wt::WString::tr("Lms.discard")));
Wt::WPushButton *immScanBtn = t->bindWidget("immediate-scan-btn", std::make_unique(Wt::WString::tr("Lms.Admin.Database.immediate-scan")));
- t->bindNew("status");
+ t->bindNew("scanner-controller");
saveBtn->clicked().connect([=] ()
{
@@ -230,7 +230,7 @@ DatabaseSettingsView::refreshView()
{
model->saveData();
- ServiceProvider::get()->requestReschedule();
+ ServiceProvider::get()->requestReload();
LmsApp->notifyMsg(MsgType::Success, Wt::WString::tr("Lms.Admin.Database.settings-saved"));
}
@@ -247,7 +247,7 @@ DatabaseSettingsView::refreshView()
immScanBtn->clicked().connect([=] ()
{
- ServiceProvider::get()->requestImmediateScan();
+ ServiceProvider::get()->requestImmediateScan(false);
});
t->updateView(model.get());
diff --git a/src/lms/ui/admin/DatabaseStatus.cpp b/src/lms/ui/admin/ScannerController.cpp
similarity index 68%
rename from src/lms/ui/admin/DatabaseStatus.cpp
rename to src/lms/ui/admin/ScannerController.cpp
index 1030c452..5bc6cb35 100644
--- a/src/lms/ui/admin/DatabaseStatus.cpp
+++ b/src/lms/ui/admin/ScannerController.cpp
@@ -17,14 +17,16 @@
* along with LMS. If not, see .
*/
-#include "DatabaseStatus.hpp"
+#include "ScannerController.hpp"
#include
#include
#include
+#include
#include
#include
+#include
#include "utils/Service.hpp"
#include "LmsApplication.hpp"
@@ -66,7 +68,7 @@ class ReportResource : public Wt::WResource
void handleRequest(const Wt::Http::Request&, Wt::Http::Response& response)
{
- response.out() << Wt::WString::tr("Lms.Admin.Database.Status.errors-header").arg(_stats.errors.size()).toUTF8() << std::endl;
+ response.out() << Wt::WString::tr("Lms.Admin.ScannerController.errors-header").arg(_stats.errors.size()).toUTF8() << std::endl;
for (const auto& error : _stats.errors)
{
@@ -78,7 +80,7 @@ class ReportResource : public Wt::WResource
response.out() << std::endl;
- response.out() << Wt::WString::tr("Lms.Admin.Database.Status.duplicates-header").arg(_stats.duplicates.size()).toUTF8() << std::endl;
+ response.out() << Wt::WString::tr("Lms.Admin.ScannerController.duplicates-header").arg(_stats.duplicates.size()).toUTF8() << std::endl;
for (const auto& duplicate : _stats.duplicates)
response.out() << duplicate.file.string() << " - " << duplicateReasonToWString(duplicate.reason).toUTF8() << std::endl;
@@ -90,10 +92,10 @@ class ReportResource : public Wt::WResource
{
switch (error)
{
- case Scanner::ScanErrorType::CannotReadFile: return Wt::WString::tr("Lms.Admin.Database.Status.cannot-read-file");
- case Scanner::ScanErrorType::CannotParseFile: return Wt::WString::tr("Lms.Admin.Database.Status.cannot-parse-file");
- case Scanner::ScanErrorType::NoAudioTrack: return Wt::WString::tr("Lms.Admin.Database.Status.no-audio-track");
- case Scanner::ScanErrorType::BadDuration: return Wt::WString::tr("Lms.Admin.Database.Status.bad-duration");
+ case Scanner::ScanErrorType::CannotReadFile: return Wt::WString::tr("Lms.Admin.ScannerController.cannot-read-file");
+ case Scanner::ScanErrorType::CannotParseFile: return Wt::WString::tr("Lms.Admin.ScannerController.cannot-parse-file");
+ case Scanner::ScanErrorType::NoAudioTrack: return Wt::WString::tr("Lms.Admin.ScannerController.no-audio-track");
+ case Scanner::ScanErrorType::BadDuration: return Wt::WString::tr("Lms.Admin.ScannerController.bad-duration");
}
return "?";
}
@@ -102,8 +104,8 @@ class ReportResource : public Wt::WResource
{
switch (reason)
{
- case Scanner::DuplicateReason::SameHash: return Wt::WString::tr("Lms.Admin.Database.Status.same-hash");
- case Scanner::DuplicateReason::SameMBID: return Wt::WString::tr("Lms.Admin.Database.Status.same-mbid");
+ case Scanner::DuplicateReason::SameHash: return Wt::WString::tr("Lms.Admin.ScannerController.same-hash");
+ case Scanner::DuplicateReason::SameMBID: return Wt::WString::tr("Lms.Admin.ScannerController.same-mbid");
}
return "?";
}
@@ -112,8 +114,8 @@ class ReportResource : public Wt::WResource
};
-DatabaseStatus::DatabaseStatus()
-: WTemplate {Wt::WString::tr("Lms.Admin.Database.Status.template")}
+ScannerController::ScannerController()
+: WTemplate {Wt::WString::tr("Lms.Admin.ScannerController.template")}
{
addFunction("tr", &Wt::WTemplate::Functions::tr);
@@ -133,16 +135,34 @@ DatabaseStatus::DatabaseStatus()
}
void
-DatabaseStatus::refreshContents()
+ScannerController::refreshContents()
{
using namespace Scanner;
- Wt::WPushButton* reportBtn {bindNew("btn-report", Wt::WString::tr("Lms.Admin.Database.Status.get-report"))};
+ Wt::WPushButton* reportBtn {bindNew("btn-report", Wt::WString::tr("Lms.Admin.ScannerController.get-report"))};
+
+ Wt::WSplitButton* actionBtn {bindNew("btn-action")};
+ actionBtn->actionButton()->addStyleClass("btn-primary");
+ actionBtn->actionButton()->setText(Wt::WString::tr("Lms.Admin.ScannerController.scan-now"));
+ actionBtn->actionButton()->clicked().connect([]
+ {
+ ServiceProvider::get()->requestImmediateScan(false);
+ });
+
+ auto popup = std::make_unique();
+ popup->addItem(Wt::WString::tr("Lms.Admin.ScannerController.force-scan-now"));
+ popup->itemSelected().connect([]
+ {
+ ServiceProvider::get()->requestImmediateScan(true);
+ });
+ actionBtn->dropDownButton()->setMenu(std::move(popup));
+ actionBtn->dropDownButton()->addStyleClass("btn-primary");
+
const IMediaScanner::Status status {ServiceProvider::get()->getStatus()};
if (status.lastCompleteScanStats)
{
- bindString("last-scan", Wt::WString::tr("Lms.Admin.Database.Status.last-scan-status")
+ bindString("last-scan", Wt::WString::tr("Lms.Admin.ScannerController.last-scan-status")
.arg(status.lastCompleteScanStats->nbFiles())
.arg(durationToString(status.lastCompleteScanStats->startTime, status.lastCompleteScanStats->stopTime))
.arg(status.lastCompleteScanStats->stopTime.toString())
@@ -156,7 +176,7 @@ DatabaseStatus::refreshContents()
}
else
{
- bindString("last-scan", Wt::WString::tr("Lms.Admin.Database.Status.last-scan-not-available"));
+ bindString("last-scan", Wt::WString::tr("Lms.Admin.ScannerController.last-scan-not-available"));
reportBtn->setEnabled(false);
}
@@ -164,16 +184,16 @@ DatabaseStatus::refreshContents()
switch (status.currentState)
{
case IMediaScanner::State::NotScheduled:
- bindString("status", Wt::WString::tr("Lms.Admin.Database.Status.status-not-scheduled"));
+ bindString("status", Wt::WString::tr("Lms.Admin.ScannerController.status-not-scheduled"));
break;
case IMediaScanner::State::Scheduled:
- bindString("status", Wt::WString::tr("Lms.Admin.Database.Status.status-scheduled")
+ bindString("status", Wt::WString::tr("Lms.Admin.ScannerController.status-scheduled")
.arg(status.nextScheduledScan.toString()));
break;
case IMediaScanner::State::InProgress:
{
std::ostringstream oss;
- bindString("status", Wt::WString::tr("Lms.Admin.Database.Status.status-in-progress")
+ bindString("status", Wt::WString::tr("Lms.Admin.ScannerController.status-in-progress")
.arg(status.inProgressScanStats->processedFiles)
.arg(status.inProgressScanStats->filesToScan)
.arg(status.inProgressScanStats->progress()));
diff --git a/src/lms/ui/admin/DatabaseStatus.hpp b/src/lms/ui/admin/ScannerController.hpp
similarity index 83%
rename from src/lms/ui/admin/DatabaseStatus.hpp
rename to src/lms/ui/admin/ScannerController.hpp
index d970ca36..498d28fe 100644
--- a/src/lms/ui/admin/DatabaseStatus.hpp
+++ b/src/lms/ui/admin/ScannerController.hpp
@@ -21,16 +21,17 @@
#include
-namespace UserInterface {
-
-class DatabaseStatus : public Wt::WTemplate
+namespace UserInterface
{
- public:
- DatabaseStatus();
- private:
- void refreshContents();
-};
+ class ScannerController : public Wt::WTemplate
+ {
+ public:
+ ScannerController();
+
+ private:
+ void refreshContents();
+ };
} // namespace DatabaseStatus