mirror of
https://github.com/Chatterino/chatterino2.git
synced 2026-08-24 10:04:53 -05:00
feat: Show restore dialog when settings fail to load (#6662)
Co-authored-by: pajlada <rasmus.karlsson@pajlada.com> Tested-by: pajlada <rasmus.karlsson@pajlada.com> Tested-by: Mm2PL <mm2pl+gh@kotmisia.pl> Reported-by: w3bprinz Reported-by: cqmpact Reviewed-by: pajlada <rasmus.karlsson@pajlada.com> Reviewed-by: Mm2PL <mm2pl+gh@kotmisia.pl>
This commit is contained in:
@@ -22,6 +22,7 @@
|
||||
- Minor: Added broadcaster-only `/poll`, `/cancelpoll`, and `/endpoll` commands. (#6583, #6605)
|
||||
- Minor: Added broadcaster-only `/prediction`, `/cancelprediction`, `/lockprediction`, and `/completeprediction` commands. (#6583, #6612, #6632, #6749)
|
||||
- Minor: Added support for BetterTTV Pro subscriber badges. (#6625, #6724)
|
||||
- Minor: Added backup restore dialog if settings fail to load. (#6662)
|
||||
- Minor: Added `debug.traceback` for plugins. (#6652)
|
||||
- Minor: Added title and duration options for `/clip` command. (#6669)
|
||||
- Minor: Added the ability to filter on messages by the author's external badges (example: `author.external_badges contains "chatterino:Top Donator"` or `author.external_badges contains "frankerfacez:bot"`). (#6709)
|
||||
|
||||
@@ -20,7 +20,7 @@ class BaseApplication : public EmptyApplication
|
||||
{
|
||||
public:
|
||||
BaseApplication()
|
||||
: settings(this->args, this->settingsDir.path())
|
||||
: settings(this->args, this->settingsDir.path(), /*isTest=*/true)
|
||||
, updates(this->paths_, this->settings)
|
||||
, theme(this->paths_)
|
||||
, fonts(this->settings)
|
||||
@@ -29,7 +29,7 @@ public:
|
||||
|
||||
explicit BaseApplication(const QString &settingsData)
|
||||
: EmptyApplication(settingsData)
|
||||
, settings(this->args, this->settingsDir.path())
|
||||
, settings(this->args, this->settingsDir.path(), /*isTest=*/true)
|
||||
, updates(this->paths_, this->settings)
|
||||
, theme(this->paths_)
|
||||
, fonts(this->settings)
|
||||
|
||||
@@ -541,6 +541,8 @@ set(SOURCE_FILES
|
||||
util/AbandonObject.hpp
|
||||
util/AttachToConsole.cpp
|
||||
util/AttachToConsole.hpp
|
||||
util/Backup.cpp
|
||||
util/Backup.hpp
|
||||
util/BadgeRegistry.cpp
|
||||
util/BadgeRegistry.hpp
|
||||
util/CancellationToken.hpp
|
||||
@@ -707,6 +709,8 @@ set(SOURCE_FILES
|
||||
widgets/dialogs/QualityPopup.hpp
|
||||
widgets/dialogs/ReplyThreadPopup.cpp
|
||||
widgets/dialogs/ReplyThreadPopup.hpp
|
||||
widgets/dialogs/RestoreBackupsDialog.cpp
|
||||
widgets/dialogs/RestoreBackupsDialog.hpp
|
||||
widgets/dialogs/SelectChannelDialog.cpp
|
||||
widgets/dialogs/SelectChannelDialog.hpp
|
||||
widgets/dialogs/SelectChannelFiltersDialog.cpp
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "controllers/nicknames/Nickname.hpp"
|
||||
#include "debug/Benchmark.hpp"
|
||||
#include "pajlada/settings/signalargs.hpp"
|
||||
#include "util/Backup.hpp"
|
||||
#include "util/WindowsHelper.hpp"
|
||||
|
||||
#include <pajlada/signals/scoped-connection.hpp>
|
||||
@@ -22,6 +23,7 @@
|
||||
namespace {
|
||||
|
||||
using namespace chatterino;
|
||||
using namespace Qt::Literals;
|
||||
|
||||
template <typename T>
|
||||
void initializeSignalVector(pajlada::Signals::SignalHolder &signalHolder,
|
||||
@@ -148,7 +150,8 @@ bool Settings::toggleMutedChannel(const QString &channelName)
|
||||
|
||||
Settings *Settings::instance_ = nullptr;
|
||||
|
||||
Settings::Settings(const Args &args, const QString &settingsDirectory)
|
||||
Settings::Settings(const Args &args, const QString &settingsDirectory,
|
||||
bool isTest)
|
||||
: prevInstance_(Settings::instance_)
|
||||
, disableSaving(args.dontSaveSettings)
|
||||
{
|
||||
@@ -157,7 +160,43 @@ Settings::Settings(const Args &args, const QString &settingsDirectory)
|
||||
// get global instance of the settings library
|
||||
auto settingsInstance = pajlada::Settings::SettingManager::getInstance();
|
||||
|
||||
settingsInstance->load(qPrintable(settingsPath));
|
||||
if (isTest)
|
||||
{
|
||||
settingsInstance->load(qPrintable(settingsPath));
|
||||
}
|
||||
else
|
||||
{
|
||||
backup::loadWithBackups(
|
||||
backup::FileData{
|
||||
.fileName = u"settings.json"_s,
|
||||
.directory = settingsDirectory,
|
||||
.fileKind = u"Settings"_s,
|
||||
.fileDescription =
|
||||
u"This file contains the main application settings such as accounts and hotkeys."_s,
|
||||
},
|
||||
[&]() -> ExpectedStr<void> {
|
||||
using LoadError = pajlada::Settings::SettingManager::LoadError;
|
||||
auto err = settingsInstance->load(qPrintable(settingsPath));
|
||||
switch (err)
|
||||
{
|
||||
case LoadError::NoError:
|
||||
return {}; // ok
|
||||
case LoadError::CannotOpenFile:
|
||||
return makeUnexpected(u"Failed to open '" %
|
||||
settingsPath % '\'');
|
||||
case LoadError::FileHandleError:
|
||||
return makeUnexpected("File handle error");
|
||||
case LoadError::FileReadError:
|
||||
return makeUnexpected("Failed to read file");
|
||||
case LoadError::FileSeekError:
|
||||
return makeUnexpected("Failed to seek in file");
|
||||
case LoadError::JSONParseError:
|
||||
return makeUnexpected("File contained malformed JSON");
|
||||
}
|
||||
assert(false);
|
||||
return makeUnexpected("Unknown error");
|
||||
});
|
||||
}
|
||||
|
||||
settingsInstance->setBackupEnabled(true);
|
||||
settingsInstance->setBackupSlots(9);
|
||||
|
||||
@@ -127,7 +127,8 @@ class Settings
|
||||
bool disableSaving;
|
||||
|
||||
public:
|
||||
Settings(const Args &args, const QString &settingsDirectory);
|
||||
Settings(const Args &args, const QString &settingsDirectory,
|
||||
bool isTest = false);
|
||||
~Settings();
|
||||
|
||||
static Settings &instance();
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
// SPDX-FileCopyrightText: 2026 Contributors to Chatterino <https://chatterino.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "util/Backup.hpp"
|
||||
|
||||
#include "common/QLogging.hpp"
|
||||
#include "util/Expected.hpp"
|
||||
#include "util/FilesystemHelpers.hpp"
|
||||
#include "widgets/dialogs/RestoreBackupsDialog.hpp"
|
||||
|
||||
#include <pajlada/settings/settingmanager.hpp>
|
||||
#include <QDir>
|
||||
#include <QRegularExpression>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace {
|
||||
|
||||
QRegularExpression regexForFile(const QString &file)
|
||||
{
|
||||
return QRegularExpression(
|
||||
QStringView(u"^%1\\.bkp-\\d+$").arg(QRegularExpression::escape(file)));
|
||||
}
|
||||
|
||||
bool anyBackupsOf(const QString &directory, const QString &filename)
|
||||
{
|
||||
QDir fileDir(directory);
|
||||
if (!fileDir.exists())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
auto regex = regexForFile(filename);
|
||||
return std::ranges::any_of(fileDir.entryList(QDir::Files),
|
||||
[&](const auto &entry) {
|
||||
return regex.match(entry).hasMatch();
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino::backup {
|
||||
|
||||
std::vector<BackupFile> findBackupsFor(const QString &directory,
|
||||
const QString &filename)
|
||||
{
|
||||
QDir fileDir(directory);
|
||||
if (!fileDir.exists())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
auto dst = qStringToStdPath(fileDir.filePath(filename));
|
||||
|
||||
auto regex = regexForFile(filename);
|
||||
std::vector<BackupFile> backups;
|
||||
const auto entries = fileDir.entryInfoList(QDir::Files, QDir::Time);
|
||||
auto testSM = pajlada::Settings::SettingManager();
|
||||
testSM.saveMethod =
|
||||
pajlada::Settings::SettingManager::SaveMethod::SaveManually;
|
||||
|
||||
for (const auto &entry : entries)
|
||||
{
|
||||
if (!regex.match(entry.fileName()).hasMatch())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
auto canonicalPath = entry.filesystemCanonicalFilePath();
|
||||
|
||||
BackupState state = BackupState::UnableToRead;
|
||||
using LoadError = pajlada::Settings::SettingManager::LoadError;
|
||||
|
||||
auto res = testSM.loadFrom(canonicalPath);
|
||||
switch (res)
|
||||
{
|
||||
case LoadError::NoError:
|
||||
state = BackupState::Ok;
|
||||
break;
|
||||
|
||||
case LoadError::CannotOpenFile:
|
||||
case LoadError::FileHandleError:
|
||||
case LoadError::FileReadError:
|
||||
case LoadError::FileSeekError:
|
||||
state = BackupState::UnableToRead;
|
||||
break;
|
||||
|
||||
case LoadError::JSONParseError:
|
||||
state = BackupState::BadContents;
|
||||
break;
|
||||
|
||||
case LoadError::SavingFromTemporaryFileFailed:
|
||||
// should never happen, temporary file loading/saving is not enabled
|
||||
assert(false);
|
||||
break;
|
||||
}
|
||||
|
||||
backups.emplace_back(BackupFile{
|
||||
.path = canonicalPath,
|
||||
.dstPath = dst,
|
||||
.lastModified = entry.lastModified(),
|
||||
.fileSize = entry.size(),
|
||||
.state = state,
|
||||
});
|
||||
}
|
||||
|
||||
return backups;
|
||||
}
|
||||
|
||||
void loadWithBackups(const FileData &fileData,
|
||||
const std::function<ExpectedStr<void>()> &load)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
auto loadResult = load();
|
||||
if (loadResult)
|
||||
{
|
||||
return;
|
||||
}
|
||||
qCDebug(chatterinoSettings)
|
||||
<< fileData.fileKind << "failed to load:" << loadResult.error();
|
||||
|
||||
if (!anyBackupsOf(fileData.directory, fileData.fileName))
|
||||
{
|
||||
qCDebug(chatterinoSettings)
|
||||
<< "No backups for" << fileData.fileKind;
|
||||
return;
|
||||
}
|
||||
|
||||
auto *diag = new RestoreBackupsDialog(fileData, loadResult.error());
|
||||
auto ret = diag->exec(); // we need to use exec here to block
|
||||
if (ret != QDialog::Accepted)
|
||||
{
|
||||
return; // rejected -> don't retry
|
||||
}
|
||||
|
||||
qCDebug(chatterinoSettings) << "Retrying to load" << fileData.fileKind;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino::backup
|
||||
@@ -0,0 +1,64 @@
|
||||
// SPDX-FileCopyrightText: 2026 Contributors to Chatterino <https://chatterino.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "util/Expected.hpp"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QString>
|
||||
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
|
||||
class QJsonValue;
|
||||
|
||||
namespace chatterino {
|
||||
class Paths;
|
||||
} // namespace chatterino
|
||||
|
||||
namespace chatterino::backup {
|
||||
|
||||
enum class BackupState : uint8_t {
|
||||
/// The backup contains valid JSON
|
||||
Ok,
|
||||
/// The backup could not be read (e.g. invalid file permissions)
|
||||
UnableToRead,
|
||||
/// The backup contains invalid JSON
|
||||
BadContents,
|
||||
};
|
||||
|
||||
/// A backup file (e.g. `settings.json.bkp-7`) and its state.
|
||||
struct BackupFile {
|
||||
std::filesystem::path path;
|
||||
std::filesystem::path dstPath;
|
||||
QDateTime lastModified;
|
||||
qint64 fileSize = 0;
|
||||
BackupState state = BackupState::Ok;
|
||||
};
|
||||
|
||||
/// Specifies where to load the file from and descriptions about the file and its contents.
|
||||
struct FileData {
|
||||
/// "settings.json", "window-layout.json"
|
||||
QString fileName;
|
||||
QString directory;
|
||||
/// "Settings", "Window layout" etc.
|
||||
QString fileKind;
|
||||
/// "This file stores..."
|
||||
QString fileDescription;
|
||||
};
|
||||
|
||||
/// Find a list of backups for the given `filename` in the given `directory`.
|
||||
std::vector<BackupFile> findBackupsFor(const QString &directory,
|
||||
const QString &filename);
|
||||
|
||||
/// Attempt to load the file described in `fileData` using the `load` param.
|
||||
///
|
||||
/// If the load fails and any backups are available, spawn a restore backups dialog.
|
||||
void loadWithBackups(const FileData &fileData,
|
||||
const std::function<ExpectedStr<void>()> &load);
|
||||
|
||||
} // namespace chatterino::backup
|
||||
|
||||
Q_DECLARE_METATYPE(chatterino::backup::BackupFile);
|
||||
@@ -0,0 +1,227 @@
|
||||
// SPDX-FileCopyrightText: 2026 Contributors to Chatterino <https://chatterino.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "widgets/dialogs/RestoreBackupsDialog.hpp"
|
||||
|
||||
#include "common/QLogging.hpp"
|
||||
#include "util/Backup.hpp"
|
||||
#include "util/FilesystemHelpers.hpp"
|
||||
#include "util/FormatTime.hpp"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QComboBox>
|
||||
#include <QDesktopServices>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QKeyEvent>
|
||||
#include <QLabel>
|
||||
#include <QLocale>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QString>
|
||||
#include <QStringBuilder>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
|
||||
using namespace Qt::Literals;
|
||||
|
||||
namespace {
|
||||
|
||||
void closeApp()
|
||||
{
|
||||
// Using a force exit over QApplication::exit, because we're currently in
|
||||
// the initialization. QApplication::exit only tells the eventloops to exit,
|
||||
// but it returns to the caller. If we return, we'd continue with the
|
||||
// initialization, which could cause settings to be loaded/overwritten.
|
||||
_Exit(1);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
RestoreBackupsDialog::RestoreBackupsDialog(backup::FileData fileData,
|
||||
const QString &prevError,
|
||||
QWidget *parent)
|
||||
: QDialog(parent,
|
||||
QFlags{
|
||||
// same as QMessageBox
|
||||
Qt::Dialog,
|
||||
Qt::MSWindowsFixedSizeDialogHint,
|
||||
// Disable default style
|
||||
Qt::CustomizeWindowHint,
|
||||
// Show window title
|
||||
Qt::WindowTitleHint,
|
||||
// Show minimize button
|
||||
Qt::WindowMinimizeButtonHint,
|
||||
})
|
||||
, fileData(std::move(fileData))
|
||||
, backupCombo(new QComboBox)
|
||||
, showButton(u"Show"_s)
|
||||
, corruptedBackupsWarning(
|
||||
u"Some backups are damaged or otherwise unreadable."_s)
|
||||
{
|
||||
this->setAttribute(Qt::WA_DeleteOnClose);
|
||||
this->setWindowTitle(u"Chatterino - Restore Backup of " %
|
||||
this->fileData.fileKind % '?');
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
|
||||
auto *description =
|
||||
new QLabel(u"Chatterino " % this->fileData.fileKind.toLower() %
|
||||
u" failed to load: " % prevError % u"<p>" %
|
||||
this->fileData.fileDescription %
|
||||
u"<p>There are backups of this file.<br>Do you want to "
|
||||
u"restore the selected backup?");
|
||||
layout->addWidget(description);
|
||||
|
||||
auto *hbox = new QHBoxLayout;
|
||||
hbox->addWidget(&this->backupCombo, 1);
|
||||
hbox->addWidget(&this->showButton);
|
||||
layout->addLayout(hbox);
|
||||
|
||||
this->corruptedBackupsWarning.hide();
|
||||
layout->addWidget(&this->corruptedBackupsWarning);
|
||||
|
||||
layout->addSpacing(10);
|
||||
|
||||
auto *buttons = new QDialogButtonBox;
|
||||
layout->addWidget(buttons);
|
||||
auto *restoreButton = buttons->addButton(QDialogButtonBox::Yes);
|
||||
restoreButton->setText(u"Restore Backup"_s);
|
||||
auto *ignoreBtn = buttons->addButton(QDialogButtonBox::No);
|
||||
// Qt has StandardButton::Ignore, but that button has the AcceptRole
|
||||
ignoreBtn->setText(u"Ignore"_s);
|
||||
auto *abortBtn = buttons->addButton(QDialogButtonBox::Abort);
|
||||
|
||||
QObject::connect(restoreButton, &QAbstractButton::clicked, this, [this] {
|
||||
auto selected = this->backupCombo.currentData();
|
||||
auto *data = get_if<backup::BackupFile>(&selected);
|
||||
if (!data)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool retry = true;
|
||||
while (retry)
|
||||
{
|
||||
retry = false;
|
||||
std::error_code ec;
|
||||
qCDebug(chatterinoSettings)
|
||||
<< "Copying" << stdPathToQString(data->path) << "to"
|
||||
<< stdPathToQString(data->dstPath);
|
||||
if (!std::filesystem::copy_file(
|
||||
data->path, data->dstPath,
|
||||
std::filesystem::copy_options::overwrite_existing, ec))
|
||||
{
|
||||
retry = QMessageBox::critical(
|
||||
this, "Failed to restore file",
|
||||
u"Failed to copy '%1' to '%2': %3"_s.arg(
|
||||
stdPathToQString(data->path),
|
||||
stdPathToQString(data->dstPath),
|
||||
QString::fromStdString(ec.message())),
|
||||
QMessageBox::Retry | QMessageBox::Ok) ==
|
||||
QMessageBox::Retry;
|
||||
}
|
||||
}
|
||||
|
||||
this->hasChosenAnything = true;
|
||||
this->accept();
|
||||
});
|
||||
QObject::connect(ignoreBtn, &QAbstractButton::clicked, this, [this] {
|
||||
auto res = QMessageBox::question(
|
||||
this, u"Chatterino - Discard Backup?"_s,
|
||||
u"Are you sure you want to discard the backup? Doing so will "_s
|
||||
"overwrite and discard any previous settings.");
|
||||
if (res == QMessageBox::Yes)
|
||||
{
|
||||
this->hasChosenAnything = true;
|
||||
this->reject();
|
||||
}
|
||||
});
|
||||
QObject::connect(abortBtn, &QAbstractButton::clicked, this, &closeApp);
|
||||
|
||||
QObject::connect(&this->showButton, &QPushButton::clicked, this, [this] {
|
||||
auto selected = this->backupCombo.currentData();
|
||||
auto *data = get_if<backup::BackupFile>(&selected);
|
||||
if (!data)
|
||||
{
|
||||
return;
|
||||
}
|
||||
auto url = QUrl::fromLocalFile(stdPathToQString(data->path));
|
||||
QDesktopServices::openUrl(url);
|
||||
});
|
||||
|
||||
this->refreshBackups();
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
// Needed for Sway to make the dialog floating. See
|
||||
// https://github.com/swaywm/sway/issues/3095
|
||||
this->layout()->activate();
|
||||
this->setFixedSize(this->layout()->totalMinimumSize());
|
||||
#endif
|
||||
}
|
||||
|
||||
void RestoreBackupsDialog::closeEvent(QCloseEvent * /*event*/)
|
||||
{
|
||||
if (!this->hasChosenAnything)
|
||||
{
|
||||
closeApp();
|
||||
}
|
||||
}
|
||||
|
||||
void RestoreBackupsDialog::keyPressEvent(QKeyEvent *event)
|
||||
{
|
||||
event->ignore();
|
||||
// Don't call QDialog here, as it would handle QKeySequence::Cancel, Enter,
|
||||
// and Return.
|
||||
}
|
||||
|
||||
void RestoreBackupsDialog::refreshBackups()
|
||||
{
|
||||
this->backupCombo.clear();
|
||||
|
||||
auto availableBackups = backup::findBackupsFor(this->fileData.directory,
|
||||
this->fileData.fileName);
|
||||
auto dtf = QLocale::system().dateTimeFormat(QLocale::ShortFormat);
|
||||
auto now = QDateTime::currentDateTime();
|
||||
|
||||
bool anyCorrupt = false;
|
||||
for (const auto &backup : availableBackups)
|
||||
{
|
||||
if (backup.state != backup::BackupState::Ok)
|
||||
{
|
||||
anyCorrupt = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
QString itemStr = stdPathToQString(backup.path.filename());
|
||||
itemStr += u" (";
|
||||
itemStr += backup.lastModified.toString(dtf);
|
||||
auto timeDiff = std::chrono::duration_cast<std::chrono::seconds>(
|
||||
now - backup.lastModified);
|
||||
if (timeDiff.count() > 0)
|
||||
{
|
||||
itemStr += u" - ";
|
||||
itemStr += formatTime(timeDiff);
|
||||
itemStr += u" ago";
|
||||
}
|
||||
itemStr += ')';
|
||||
|
||||
this->backupCombo.addItem(itemStr, QVariant::fromValue(backup));
|
||||
}
|
||||
|
||||
this->corruptedBackupsWarning.setVisible(anyCorrupt);
|
||||
|
||||
bool anyBackups = this->backupCombo.count() > 0;
|
||||
auto *okButton = this->dialogButtons.button(QDialogButtonBox::Ok);
|
||||
if (okButton)
|
||||
{
|
||||
okButton->setEnabled(anyBackups);
|
||||
}
|
||||
this->showButton.setEnabled(anyBackups);
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,45 @@
|
||||
// SPDX-FileCopyrightText: 2026 Contributors to Chatterino <https://chatterino.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "util/Backup.hpp"
|
||||
#include "util/Expected.hpp"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDialog>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class Paths;
|
||||
|
||||
class RestoreBackupsDialog : public QDialog
|
||||
{
|
||||
public:
|
||||
RestoreBackupsDialog(backup::FileData fileData, const QString &prevError,
|
||||
QWidget *parent = nullptr);
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent * /*event*/) override;
|
||||
void keyPressEvent(QKeyEvent *event) override;
|
||||
|
||||
private:
|
||||
void refreshBackups();
|
||||
|
||||
backup::FileData *selectedFileData() const;
|
||||
|
||||
backup::FileData fileData;
|
||||
|
||||
QComboBox backupCombo;
|
||||
QPushButton showButton;
|
||||
QDialogButtonBox dialogButtons;
|
||||
QLabel corruptedBackupsWarning;
|
||||
|
||||
bool hasChosenAnything = false;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
Reference in New Issue
Block a user