Save export settings to local storage.

This commit is contained in:
John Preston 2018-06-22 22:32:00 +01:00
parent ae18ece549
commit 844d030332
10 changed files with 312 additions and 79 deletions

View file

@ -87,7 +87,7 @@ public:
HiddenUrlClickHandler(QString url) : UrlClickHandler(url, false) {
}
QString copyToClipboardContextItemText() const override {
return url().isEmpty()
return (url().isEmpty() || url().startsWith(qstr("internal:")))
? QString()
: UrlClickHandler::copyToClipboardContextItemText();
}

View file

@ -0,0 +1,48 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "export/export_settings.h"
#include "export/output/export_output_abstract.h"
namespace Export {
namespace {
constexpr auto kMaxFileSize = 1500 * 1024 * 1024;
} // namespace
bool MediaSettings::validate() const {
if ((types | Type::AllMask) != Type::AllMask) {
return false;
} else if (sizeLimit < 0 || sizeLimit > kMaxFileSize) {
return false;
}
return true;
}
bool Settings::validate() const {
using Format = Output::Format;
const auto MustBeFull = Type::PersonalChats | Type::BotChats;
const auto MustNotBeFull = Type::PublicGroups | Type::PublicChannels;
if ((types | Type::AllMask) != Type::AllMask) {
return false;
} else if ((fullChats | Type::AllMask) != Type::AllMask) {
return false;
} else if ((fullChats & MustBeFull) != MustBeFull) {
return false;
} else if ((fullChats & MustNotBeFull) != 0) {
return false;
} else if (format != Format::Text && format != Format::Json) {
return false;
} else if (!media.validate()) {
return false;
}
return true;
};
} // namespace Export

View file

@ -16,6 +16,8 @@ enum class Format;
} // namespace Output
struct MediaSettings {
bool validate() const;
enum class Type {
Photo = 0x01,
Video = 0x02,
@ -24,6 +26,9 @@ struct MediaSettings {
Sticker = 0x10,
GIF = 0x20,
File = 0x40,
MediaMask = Photo | Video | VoiceMessage | VideoMessage,
AllMask = MediaMask | Sticker | GIF | File,
};
using Types = base::flags<Type>;
friend inline constexpr auto is_flag_type(Type) { return true; };
@ -38,6 +43,8 @@ struct MediaSettings {
};
struct Settings {
bool validate() const;
enum class Type {
PersonalInfo = 0x001,
Userpics = 0x002,
@ -55,6 +62,8 @@ struct Settings {
GroupsChannelsMask = GroupsMask | ChannelsMask,
NonChannelChatsMask = PersonalChats | BotChats | PrivateGroups,
AnyChatsMask = PersonalChats | BotChats | GroupsChannelsMask,
NonChatsMask = PersonalInfo | Userpics | Contacts | Sessions,
AllMask = NonChatsMask | AnyChatsMask,
};
using Types = base::flags<Type>;
friend inline constexpr auto is_flag_type(Type) { return true; };

View file

@ -14,7 +14,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/wrap/padding_wrap.h"
#include "boxes/confirm_box.h"
#include "lang/lang_keys.h"
#include "storage/localstorage.h"
#include "core/file_utilities.h"
#include "platform/platform_specific.h"
#include "styles/style_export.h"
#include "styles/style_boxes.h"
@ -23,11 +25,19 @@ namespace View {
namespace {
constexpr auto kAddDelay = TimeId(60);
constexpr auto kSaveSettingsTimeout = TimeMs(1000);
} // namespace
PanelController::PanelController(not_null<ControllerWrap*> process)
: _process(process) {
: _process(process)
, _settings(std::make_unique<Settings>(Local::ReadExportSettings()))
, _saveSettingsTimer([=] { saveSettings(); }) {
if (_settings->path.isEmpty()) {
_settings->path = psDownloadPath();
}
_settings->internalLinksDomain = Global::InternalLinksDomain();
_process->state(
) | rpl::start_with_next([=](State &&state) {
updateState(std::move(state));
@ -52,12 +62,14 @@ void PanelController::createPanel() {
}
void PanelController::showSettings() {
auto settings = base::make_unique_q<SettingsWidget>(_panel);
auto settings = base::make_unique_q<SettingsWidget>(
_panel,
*_settings);
settings->startClicks(
) | rpl::start_with_next([=](const Settings &settings) {
) | rpl::start_with_next([=]() {
showProgress();
_process->startExport(settings);
_process->startExport(*_settings);
}, settings->lifetime());
settings->cancelClicks(
@ -65,6 +77,12 @@ void PanelController::showSettings() {
_panel->hideGetDuration();
}, settings->lifetime());
settings->changes(
) | rpl::start_with_next([=](Settings &&settings) {
*_settings = std::move(settings);
_saveSettingsTimer.callOnce(kSaveSettingsTimeout);
}, settings->lifetime());
_panel->showInner(std::move(settings));
}
@ -218,7 +236,25 @@ void PanelController::updateState(State &&state) {
}
}
PanelController::~PanelController() = default;
void PanelController::saveSettings() const {
const auto check = [](const QString &value) {
const auto result = value.endsWith('/')
? value.mid(0, value.size() - 1)
: value;
return (cPlatform() == dbipWindows) ? result.toLower() : result;
};
auto settings = *_settings;
if (check(settings.path) == check(psDownloadPath())) {
settings.path = QString();
}
Local::WriteExportSettings(settings);
}
PanelController::~PanelController() {
if (_saveSettingsTimer.isActive()) {
saveSettings();
}
}
} // namespace View
} // namespace Export

View file

@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "export/export_controller.h"
#include "export/view/export_view_content.h"
#include "base/unique_qptr.h"
#include "base/timer.h"
class BoxContent;
@ -52,7 +53,11 @@ private:
void showError(const QString &text);
void showCriticalError(const QString &text);
void saveSettings() const;
not_null<ControllerWrap*> _process;
std::unique_ptr<Settings> _settings;
base::Timer _saveSettingsTimer;
base::unique_qptr<Ui::SeparatePanel> _panel;

View file

@ -57,14 +57,22 @@ int SizeLimitByIndex(int index) {
} // namespace
SettingsWidget::SettingsWidget(QWidget *parent)
: RpWidget(parent) {
_data.path = psDownloadPath();
_data.internalLinksDomain = Global::InternalLinksDomain();
SettingsWidget::SettingsWidget(QWidget *parent, Settings data)
: RpWidget(parent)
, _internal_data(std::move(data)) {
setupContent();
}
const Settings &SettingsWidget::readData() const {
return _internal_data;
}
template <typename Callback>
void SettingsWidget::changeData(Callback &&callback) {
callback(_internal_data);
_changes.fire_copy(_internal_data);
}
void SettingsWidget::setupContent() {
const auto scroll = Ui::CreateChild<Ui::ScrollArea>(
this,
@ -78,8 +86,6 @@ void SettingsWidget::setupContent() {
setupOptions(content);
setupPathAndFormat(content);
_refreshButtons.fire({});
sizeValue(
) | rpl::start_with_next([=](QSize size) {
scroll->resize(size.width(), size.height() - buttons->height());
@ -138,8 +144,9 @@ void SettingsWidget::setupMediaOptions(
addMediaOption(media, lng_export_option_files, MediaType::File);
addSizeSlider(media);
_dataTypesChanges.events_starting_with_copy(
_data.types
value() | rpl::map([](const Settings &data) {
return data.types;
}) | rpl::distinct_until_changed(
) | rpl::start_with_next([=](Settings::Types types) {
mediaWrap->toggle((types & (Type::PersonalChats
| Type::BotChats
@ -158,9 +165,11 @@ void SettingsWidget::setupMediaOptions(
void SettingsWidget::setupPathAndFormat(
not_null<Ui::VerticalLayout*> container) {
const auto formatGroup = std::make_shared<Ui::RadioenumGroup<Format>>(
_data.format);
readData().format);
formatGroup->setChangedCallback([=](Format format) {
_data.format = format;
changeData([&](Settings &data) {
data.format = format;
});
});
const auto addFormatOption = [&](LangKey key, Format format) {
const auto radio = container->add(
@ -180,10 +189,17 @@ void SettingsWidget::setupPathAndFormat(
void SettingsWidget::addLocationLabel(
not_null<Ui::VerticalLayout*> container) {
auto pathLabel = _locationChanges.events_starting_with_copy(
_data.path
auto pathLabel = value() | rpl::map([](const Settings &data) {
return data.path;
}) | rpl::distinct_until_changed(
) | rpl::map([](const QString &path) {
const auto text = (path == psDownloadPath())
const auto check = [](const QString &value) {
const auto result = value.endsWith('/')
? value.mid(0, value.size() - 1)
: value;
return (cPlatform() == dbipWindows) ? result.toLower() : result;
};
const auto text = (check(path) == check(psDownloadPath()))
? QString("Downloads/Telegram Desktop")
: path;
auto pathLink = TextWithEntities{
@ -194,7 +210,7 @@ void SettingsWidget::addLocationLabel(
EntityInTextCustomUrl,
0,
text.size(),
"internal:edit_export_path"));
QString("internal:edit_export_path")));
return lng_export_option_location__generic<TextWithEntities>(
lt_path,
pathLink);
@ -236,9 +252,11 @@ not_null<Ui::RpWidget*> SettingsWidget::setupButtons(
return top < scroll->scrollTopMax();
}));
_refreshButtons.events(
) | rpl::start_with_next([=] {
refreshButtons(buttons);
value() | rpl::map([](const Settings &data) {
return data.types != Types(0);
}) | rpl::distinct_until_changed(
) | rpl::start_with_next([=](bool canStart) {
refreshButtons(buttons, canStart);
topShadow->raise();
bottomShadow->raise();
}, buttons->lifetime());
@ -276,19 +294,19 @@ not_null<Ui::Checkbox*> SettingsWidget::addOption(
object_ptr<Ui::Checkbox>(
container,
lang(key),
((_data.types & types) == types),
((readData().types & types) == types),
st::defaultBoxCheckbox),
st::exportSettingPadding);
base::ObservableViewer(
checkbox->checkedChanged
) | rpl::start_with_next([=](bool checked) {
if (checked) {
_data.types |= types;
} else {
_data.types &= ~types;
}
_dataTypesChanges.fire_copy(_data.types);
_refreshButtons.fire({});
changeData([&](Settings &data) {
if (checked) {
data.types |= types;
} else {
data.types &= ~types;
}
});
}, lifetime());
return checkbox;
}
@ -304,18 +322,20 @@ void SettingsWidget::addChatOption(
object_ptr<Ui::Checkbox>(
container,
lang(lng_export_option_only_my),
((_data.fullChats & types) != types),
((readData().fullChats & types) != types),
st::defaultBoxCheckbox),
st::exportSubSettingPadding));
base::ObservableViewer(
onlyMy->entity()->checkedChanged
) | rpl::start_with_next([=](bool checked) {
if (checked) {
_data.fullChats &= ~types;
} else {
_data.fullChats |= types;
}
changeData([&](Settings &data) {
if (checked) {
data.fullChats &= ~types;
} else {
data.fullChats |= types;
}
});
}, checkbox->lifetime());
onlyMy->toggleOn(base::ObservableViewer(
@ -338,18 +358,19 @@ void SettingsWidget::addMediaOption(
object_ptr<Ui::Checkbox>(
container,
lang(key),
((_data.media.types & type) == type),
((readData().media.types & type) == type),
st::defaultBoxCheckbox),
st::exportSettingPadding);
base::ObservableViewer(
checkbox->checkedChanged
) | rpl::start_with_next([=](bool checked) {
if (checked) {
_data.media.types |= type;
} else {
_data.media.types &= ~type;
}
_refreshButtons.fire({});
changeData([&](Settings &data) {
if (checked) {
data.media.types |= type;
} else {
data.media.types &= ~type;
}
});
}, lifetime());
}
@ -364,7 +385,7 @@ void SettingsWidget::addSizeSlider(
slider->setAlwaysDisplayMarker(true);
slider->setDirection(Ui::ContinuousSlider::Direction::Horizontal);
for (auto i = 0; i != kSizeValueCount + 1; ++i) {
if (_data.media.sizeLimit <= SizeLimitByIndex(i)) {
if (readData().media.sizeLimit <= SizeLimitByIndex(i)) {
slider->setValue(i / float64(kSizeValueCount));
break;
}
@ -373,24 +394,26 @@ void SettingsWidget::addSizeSlider(
const auto label = Ui::CreateChild<Ui::LabelSimple>(
container.get(),
st::exportFileSizeLabel);
const auto refreshSizeLimit = [=] {
const auto limit = _data.media.sizeLimit / kMegabyte;
const auto size = ((limit > 0)
? QString::number(limit)
: QString::number(float64(_data.media.sizeLimit) / kMegabyte))
+ " MB";
const auto text = lng_export_option_size_limit(lt_size, size);
label->setText(text);
};
slider->setAdjustCallback([=](float64 value) {
return std::round(value * kSizeValueCount) / kSizeValueCount;
});
slider->setChangeProgressCallback([=](float64 value) {
const auto index = int(std::round(value * kSizeValueCount));
_data.media.sizeLimit = SizeLimitByIndex(index);
refreshSizeLimit();
changeData([&](Settings &data) {
data.media.sizeLimit = SizeLimitByIndex(index);
});
});
refreshSizeLimit();
value() | rpl::map([](const Settings &data) {
return data.media.sizeLimit;
}) | rpl::start_with_next([=](int sizeLimit) {
const auto limit = sizeLimit / kMegabyte;
const auto size = ((limit > 0)
? QString::number(limit)
: QString::number(float64(sizeLimit) / kMegabyte))
+ " MB";
const auto text = lng_export_option_size_limit(lt_size, size);
label->setText(text);
}, slider->lifetime());
rpl::combine(
label->widthValue(),
@ -404,7 +427,9 @@ void SettingsWidget::addSizeSlider(
}
void SettingsWidget::refreshButtons(not_null<Ui::RpWidget*> container) {
void SettingsWidget::refreshButtons(
not_null<Ui::RpWidget*> container,
bool canStart) {
container->hideChildren();
const auto children = container->children();
for (const auto child : children) {
@ -412,7 +437,7 @@ void SettingsWidget::refreshButtons(not_null<Ui::RpWidget*> container) {
child->deleteLater();
}
}
const auto start = _data.types
const auto start = canStart
? Ui::CreateChild<Ui::RoundButton>(
container.get(),
langFactory(lng_export_start),
@ -420,9 +445,7 @@ void SettingsWidget::refreshButtons(not_null<Ui::RpWidget*> container) {
: nullptr;
if (start) {
start->show();
start->addClickHandler([=] {
_startClicks.fire(base::duplicate(_data));
});
_startClicks = start->clicks();
container->sizeValue(
) | rpl::start_with_next([=](QSize size) {
@ -451,15 +474,31 @@ void SettingsWidget::refreshButtons(not_null<Ui::RpWidget*> container) {
}
void SettingsWidget::chooseFolder() {
const auto ready = [=](QString &&result) {
_data.path = result;
_locationChanges.fire(std::move(result));
const auto callback = [=](QString &&result) {
changeData([&](Settings &data) {
data.path = std::move(result);
});
};
FileDialog::GetFolder(this, lang(lng_export_folder), _data.path, ready);
FileDialog::GetFolder(
this,
lang(lng_export_folder),
readData().path,
callback);
}
rpl::producer<Settings> SettingsWidget::startClicks() const {
return _startClicks.events();
rpl::producer<Settings> SettingsWidget::changes() const {
return _changes.events();
}
rpl::producer<Settings> SettingsWidget::value() const {
return rpl::single(readData()) | rpl::then(changes());
}
rpl::producer<> SettingsWidget::startClicks() const {
return _startClicks.value(
) | rpl::map([](Wrap &&wrap) {
return std::move(wrap.value);
}) | rpl::flatten_latest();
}
rpl::producer<> SettingsWidget::cancelClicks() const {

View file

@ -23,9 +23,11 @@ namespace View {
class SettingsWidget : public Ui::RpWidget {
public:
SettingsWidget(QWidget *parent);
SettingsWidget(QWidget *parent, Settings data);
rpl::producer<Settings> startClicks() const;
rpl::producer<Settings> value() const;
rpl::producer<Settings> changes() const;
rpl::producer<> startClicks() const;
rpl::producer<> cancelClicks() const;
private:
@ -61,9 +63,17 @@ private:
void addLocationLabel(
not_null<Ui::VerticalLayout*> container);
void chooseFolder();
void refreshButtons(not_null<Ui::RpWidget*> container);
void refreshButtons(
not_null<Ui::RpWidget*> container,
bool canStart);
const Settings &readData() const;
template <typename Callback>
void changeData(Callback &&callback);
// Use through readData / changeData wrappers.
Settings _internal_data;
Settings _data;
struct Wrap {
Wrap(rpl::producer<> value = rpl::never<>())
: value(std::move(value)) {
@ -71,11 +81,9 @@ private:
rpl::producer<> value;
};
rpl::event_stream<Settings> _startClicks;
rpl::event_stream<Settings> _changes;
rpl::variable<Wrap> _startClicks;
rpl::variable<Wrap> _cancelClicks;
rpl::event_stream<Settings::Types> _dataTypesChanges;
rpl::event_stream<> _refreshButtons;
rpl::event_stream<QString> _locationChanges;
};

View file

@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_drafts.h"
#include "boxes/send_files_box.h"
#include "window/themes/window_theme.h"
#include "export/export_settings.h"
#include "core/crash_reports.h"
#include "core/update_checker.h"
#include "observer_peer.h"
@ -501,6 +502,7 @@ enum { // Local Storage Keys
lskStickersKeys = 0x10, // no data
lskTrustedBots = 0x11, // no data
lskFavedStickers = 0x12, // no data
lskExportSettings = 0x13, // no data
};
enum {
@ -634,6 +636,8 @@ FileKey _userSettingsKey = 0;
FileKey _recentHashtagsAndBotsKey = 0;
bool _recentHashtagsAndBotsWereRead = false;
FileKey _exportSettingsKey = 0;
FileKey _savedPeersKey = 0;
FileKey _langPackKey = 0;
@ -2065,7 +2069,7 @@ ReadMapState _readMap(const QByteArray &pass) {
quint64 recentStickersKeyOld = 0;
quint64 installedStickersKey = 0, featuredStickersKey = 0, recentStickersKey = 0, favedStickersKey = 0, archivedStickersKey = 0;
quint64 savedGifsKey = 0;
quint64 backgroundKey = 0, userSettingsKey = 0, recentHashtagsAndBotsKey = 0, savedPeersKey = 0;
quint64 backgroundKey = 0, userSettingsKey = 0, recentHashtagsAndBotsKey = 0, savedPeersKey = 0, exportSettingsKey = 0;
while (!map.stream.atEnd()) {
quint32 keyType;
map.stream >> keyType;
@ -2167,6 +2171,9 @@ ReadMapState _readMap(const QByteArray &pass) {
case lskSavedPeers: {
map.stream >> savedPeersKey;
} break;
case lskExportSettings: {
map.stream >> exportSettingsKey;
} break;
default:
LOG(("App Error: unknown key type in encrypted map: %1").arg(keyType));
return ReadMapFailed;
@ -2201,6 +2208,7 @@ ReadMapState _readMap(const QByteArray &pass) {
_backgroundKey = backgroundKey;
_userSettingsKey = userSettingsKey;
_recentHashtagsAndBotsKey = recentHashtagsAndBotsKey;
_exportSettingsKey = exportSettingsKey;
_oldMapVersion = mapData.version;
if (_oldMapVersion < AppVersion) {
_mapChanged = true;
@ -2279,6 +2287,7 @@ void _writeMap(WriteMapWhen when) {
if (_backgroundKey) mapSize += sizeof(quint32) + sizeof(quint64);
if (_userSettingsKey) mapSize += sizeof(quint32) + sizeof(quint64);
if (_recentHashtagsAndBotsKey) mapSize += sizeof(quint32) + sizeof(quint64);
if (_exportSettingsKey) mapSize += sizeof(quint32) + sizeof(quint64);
if (mapSize > 30 * 1024 * 1024) {
CrashReports::SetAnnotation("MapSize", QString("%1,%2,%3,%4,%5"
@ -2356,6 +2365,9 @@ void _writeMap(WriteMapWhen when) {
if (_recentHashtagsAndBotsKey) {
mapData.stream << quint32(lskRecentHashtagsAndBots) << quint64(_recentHashtagsAndBotsKey);
}
if (_exportSettingsKey) {
mapData.stream << quint32(lskExportSettings) << quint64(_exportSettingsKey);
}
map.writeEncrypted(mapData);
_mapChanged = false;
@ -2613,7 +2625,7 @@ void reset() {
_recentStickersKeyOld = 0;
_installedStickersKey = _featuredStickersKey = _recentStickersKey = _favedStickersKey = _archivedStickersKey = 0;
_savedGifsKey = 0;
_backgroundKey = _userSettingsKey = _recentHashtagsAndBotsKey = _savedPeersKey = 0;
_backgroundKey = _userSettingsKey = _recentHashtagsAndBotsKey = _savedPeersKey = _exportSettingsKey = 0;
_oldMapVersion = _oldSettingsVersion = 0;
StoredAuthSessionCache.reset();
_mapChanged = true;
@ -4602,6 +4614,74 @@ void readRecentHashtagsAndBots() {
}
}
void WriteExportSettings(const Export::Settings &settings) {
if (!_working()) return;
const auto check = Export::Settings();
if (settings.types == check.types
&& settings.fullChats == check.fullChats
&& settings.media.types == check.media.types
&& settings.media.sizeLimit == check.media.sizeLimit
&& settings.path == check.path
&& settings.format == check.format) {
if (_exportSettingsKey) {
clearKey(_exportSettingsKey);
_exportSettingsKey = 0;
_mapChanged = true;
}
_writeMap();
} else {
if (!_exportSettingsKey) {
_exportSettingsKey = genKey();
_mapChanged = true;
_writeMap(WriteMapWhen::Fast);
}
quint32 size = sizeof(quint32) * 5
+ Serialize::stringSize(settings.path);
EncryptedDescriptor data(size);
data.stream
<< quint32(settings.types)
<< quint32(settings.fullChats)
<< quint32(settings.media.types)
<< quint32(settings.media.sizeLimit)
<< quint32(settings.format)
<< settings.path;
FileWriteDescriptor file(_exportSettingsKey);
file.writeEncrypted(data);
}
}
Export::Settings ReadExportSettings() {
FileReadDescriptor file;
if (!readEncryptedFile(file, _exportSettingsKey)) {
clearKey(_exportSettingsKey);
_exportSettingsKey = 0;
_writeMap();
return Export::Settings();
}
quint32 types = 0, fullChats = 0;
quint32 mediaTypes = 0, mediaSizeLimit = 0;
quint32 format = 0;
QString path;
file.stream
>> types
>> fullChats
>> mediaTypes
>> mediaSizeLimit
>> format
>> path;
auto result = Export::Settings();
result.types = Export::Settings::Types::from_raw(types);
result.fullChats = Export::Settings::Types::from_raw(fullChats);
result.media.types = Export::MediaSettings::Types::from_raw(mediaTypes);
result.media.sizeLimit = mediaSizeLimit;
result.format = Export::Output::Format(format);
result.path = path;
return result.validate() ? result : Export::Settings();
}
void writeSavedPeers() {
if (!_working()) return;

View file

@ -17,6 +17,10 @@ struct Cached;
} // namespace Theme
} // namespace Window
namespace Export {
struct Settings;
} // namespace Export
namespace Local {
void start();
@ -162,6 +166,9 @@ void writeLangPack();
void writeRecentHashtagsAndBots();
void readRecentHashtagsAndBots();
void WriteExportSettings(const Export::Settings &settings);
Export::Settings ReadExportSettings();
void addSavedPeer(PeerData *peer, const QDateTime &position);
void removeSavedPeer(PeerData *peer);
void readSavedPeers();

View file

@ -54,6 +54,7 @@
'<(src_loc)/export/export_api_wrap.h',
'<(src_loc)/export/export_controller.cpp',
'<(src_loc)/export/export_controller.h',
'<(src_loc)/export/export_settings.cpp',
'<(src_loc)/export/export_settings.h',
'<(src_loc)/export/data/export_data_types.cpp',
'<(src_loc)/export/data/export_data_types.h',