init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,326 @@
/*
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
*/
#pragma once
#include "mtproto/mtproto_concurrent_sender.h"
#include "data/data_peer_id.h"
namespace Export {
namespace Data {
struct File;
struct Chat;
struct Document;
struct FileLocation;
struct PersonalInfo;
struct UserpicsInfo;
struct UserpicsSlice;
struct StoriesInfo;
struct StoriesSlice;
struct ProfileMusicInfo;
struct ProfileMusicSlice;
struct ContactsList;
struct SessionsList;
struct DialogsInfo;
struct DialogInfo;
struct MessagesSlice;
struct Message;
struct Story;
struct FileOrigin;
} // namespace Data
namespace Output {
struct Result;
class Stats;
} // namespace Output
struct Settings;
class ApiWrap {
public:
ApiWrap(
base::weak_qptr<MTP::Instance> weak,
Fn<void(FnMut<void()>)> runner);
rpl::producer<MTP::Error> errors() const;
rpl::producer<Output::Result> ioErrors() const;
struct StartInfo {
int userpicsCount = 0;
int storiesCount = 0;
int profileMusicCount = 0;
int dialogsCount = 0;
};
void startExport(
const Settings &settings,
Output::Stats *stats,
FnMut<void(StartInfo)> done);
void requestDialogsList(
Fn<bool(int count)> progress,
FnMut<void(Data::DialogsInfo&&)> done);
void requestPersonalInfo(FnMut<void(Data::PersonalInfo&&)> done);
void requestOtherData(
const QString &suggestedPath,
FnMut<void(Data::File&&)> done);
struct DownloadProgress {
uint64 randomId = 0;
QString path;
int itemIndex = 0;
int64 ready = 0;
int64 total = 0;
};
void requestUserpics(
FnMut<bool(Data::UserpicsInfo&&)> start,
Fn<bool(DownloadProgress)> progress,
Fn<bool(Data::UserpicsSlice&&)> slice,
FnMut<void()> finish);
void requestStories(
FnMut<bool(Data::StoriesInfo&&)> start,
Fn<bool(DownloadProgress)> progress,
Fn<bool(Data::StoriesSlice&&)> slice,
FnMut<void()> finish);
void requestProfileMusic(
FnMut<bool(Data::ProfileMusicInfo&&)> start,
Fn<bool(DownloadProgress)> progress,
Fn<bool(Data::ProfileMusicSlice&&)> slice,
FnMut<void()> finish);
void requestContacts(FnMut<void(Data::ContactsList&&)> done);
void requestSessions(FnMut<void(Data::SessionsList&&)> done);
void requestMessages(
const Data::DialogInfo &info,
FnMut<bool(const Data::DialogInfo &)> start,
Fn<bool(DownloadProgress)> progress,
Fn<bool(Data::MessagesSlice&&)> slice,
FnMut<void()> done);
void requestTopicMessages(
PeerId peerId,
MTPInputPeer inputPeer,
int32 topicRootId,
FnMut<bool(int count)> start,
Fn<bool(DownloadProgress)> progress,
Fn<bool(Data::MessagesSlice&&)> slice,
FnMut<void()> done);
void finishExport(FnMut<void()> done);
void skipFile(uint64 randomId);
void cancelExportFast();
~ApiWrap();
private:
class LoadedFileCache;
struct StartProcess;
struct ContactsProcess;
struct UserpicsProcess;
struct StoriesProcess;
struct ProfileMusicProcess;
struct OtherDataProcess;
struct FileProcess;
struct FileProgress;
struct ChatsProcess;
struct LeftChannelsProcess;
struct DialogsProcess;
struct AbstractMessagesProcess;
struct ChatProcess;
struct TopicProcess;
void startMainSession(FnMut<void()> done);
void sendNextStartRequest();
void requestUserpicsCount();
void requestStoriesCount();
void requestProfileMusicCount();
void requestSplitRanges();
void requestDialogsCount();
void requestLeftChannelsCount();
void finishStartProcess();
void requestTopPeersSlice();
void handleUserpicsSlice(const MTPphotos_Photos &result);
void loadUserpicsFiles(Data::UserpicsSlice &&slice);
void loadNextUserpic();
bool loadUserpicProgress(FileProgress value);
void loadUserpicDone(const QString &relativePath);
void finishUserpicsSlice();
void finishUserpics();
void handleStoriesSlice(const MTPstories_Stories &result);
void loadStoriesFiles(Data::StoriesSlice &&slice);
void loadNextStory();
bool loadStoryProgress(FileProgress value);
void loadStoryDone(const QString &relativePath);
bool loadStoryThumbProgress(FileProgress value);
void loadStoryThumbDone(const QString &relativePath);
void finishStoriesSlice();
void finishStories();
void handleProfileMusicSlice(const MTPusers_SavedMusic &result);
void loadProfileMusicFiles(Data::ProfileMusicSlice &&slice);
void loadNextProfileMusic();
bool loadProfileMusicProgress(FileProgress value);
void loadProfileMusicDone(const QString &relativePath);
bool loadProfileMusicThumbProgress(FileProgress value);
void loadProfileMusicThumbDone(const QString &relativePath);
void finishProfileMusicSlice();
void finishProfileMusic();
void otherDataDone(const QString &relativePath);
bool useOnlyLastSplit() const;
void requestDialogsSlice();
void appendDialogsSlice(Data::DialogsInfo &&info);
void finishDialogsList();
void requestSinglePeerDialog();
mtpRequestId requestSinglePeerMigrated(const Data::DialogInfo &info);
void appendSinglePeerDialogs(Data::DialogsInfo &&info);
void requestLeftChannelsIfNeeded();
void requestLeftChannelsList(
Fn<bool(int count)> progress,
FnMut<void(Data::DialogsInfo&&)> done);
void requestLeftChannelsSliceGeneric(FnMut<void()> done);
void requestLeftChannelsSlice();
void appendLeftChannelsSlice(Data::DialogsInfo &&info);
void appendChatsSlice(
ChatsProcess &process,
std::vector<Data::DialogInfo> &to,
std::vector<Data::DialogInfo> &&from,
int splitIndex);
void requestMessagesCount(int localSplitIndex);
void checkFirstMessageDate(int localSplitIndex, int count);
void messagesCountLoaded(int localSplitIndex, int count);
void requestMessagesSlice();
void requestChatMessages(
int splitIndex,
int offsetId,
int addOffset,
int limit,
FnMut<void(MTPmessages_Messages&&)> done);
void requestTopicMessagesSlice();
void requestTopicReplies(
int offsetId,
int addOffset,
int limit,
FnMut<void(MTPmessages_Messages&&)> done);
void collectMessagesCustomEmoji(const Data::MessagesSlice &slice);
void resolveCustomEmoji();
void loadMessagesFiles(Data::MessagesSlice &&slice);
void loadNextMessageFile();
[[nodiscard]] std::optional<QByteArray> getCustomEmoji(QByteArray &data);
bool messageCustomEmojiReady(Data::Message &message);
bool loadMessageFileProgress(FileProgress value);
void loadMessageFileDone(const QString &relativePath);
bool loadMessageThumbProgress(FileProgress value);
void loadMessageThumbDone(const QString &relativePath);
bool loadMessageEmojiProgress(FileProgress progress);
void loadMessageEmojiDone(uint64 id, const QString &relativePath);
void finishMessagesSlice();
void finishMessages();
void loadTopicMessagesFiles(Data::MessagesSlice &&slice);
void resolveTopicCustomEmoji();
void loadNextTopicMessageFile();
bool loadTopicEmojiProgress(FileProgress progress);
void loadCustomEmojiDone(uint64 id, const QString &relativePath);
void loadTopicMessageFileOrThumbDone(
Data::File &file,
const QString &relativePath);
void finishTopicMessagesSlice();
void finishTopicMessages();
[[nodiscard]] Data::Message *currentFileMessage() const;
[[nodiscard]] Data::FileOrigin currentFileMessageOrigin() const;
bool processFileLoad(
Data::File &file,
const Data::FileOrigin &origin,
Fn<bool(FileProgress)> progress,
FnMut<void(QString)> done,
Data::Message *message = nullptr,
Data::Story *story = nullptr);
std::unique_ptr<FileProcess> prepareFileProcess(
const Data::File &file,
const Data::FileOrigin &origin) const;
bool writePreloadedFile(
Data::File &file,
const Data::FileOrigin &origin);
void loadFile(
const Data::File &file,
const Data::FileOrigin &origin,
Fn<bool(FileProgress)> progress,
FnMut<void(QString)> done);
void loadFilePart();
void filePartDone(int64 offset, const MTPupload_File &result);
void filePartUnavailable();
void filePartRefreshReference(int64 offset);
void filePartExtractReference(
int64 offset,
const MTPmessages_Messages &result);
void filePartExtractReference(
int64 offset,
const MTPstories_Stories &result);
template <typename Request>
class RequestBuilder;
template <typename Request>
[[nodiscard]] auto mainRequest(Request &&request);
template <typename Request>
[[nodiscard]] auto splitRequest(int index, Request &&request);
[[nodiscard]] auto fileRequest(
const Data::FileLocation &location,
int64 offset);
void error(const MTP::Error &error);
void error(const QString &text);
void ioError(const Output::Result &result);
MTP::ConcurrentSender _mtp;
std::optional<uint64> _takeoutId;
std::optional<UserId> _selfId;
Output::Stats *_stats = nullptr;
std::unique_ptr<Settings> _settings;
MTPInputUser _user = MTP_inputUserSelf();
std::unique_ptr<StartProcess> _startProcess;
std::unique_ptr<LoadedFileCache> _fileCache;
std::unique_ptr<ContactsProcess> _contactsProcess;
std::unique_ptr<UserpicsProcess> _userpicsProcess;
std::unique_ptr<StoriesProcess> _storiesProcess;
std::unique_ptr<ProfileMusicProcess> _profileMusicProcess;
std::unique_ptr<OtherDataProcess> _otherDataProcess;
std::unique_ptr<FileProcess> _fileProcess;
std::unique_ptr<LeftChannelsProcess> _leftChannelsProcess;
std::unique_ptr<DialogsProcess> _dialogsProcess;
std::unique_ptr<ChatProcess> _chatProcess;
std::unique_ptr<TopicProcess> _topicProcess;
base::flat_set<uint64> _unresolvedCustomEmoji;
base::flat_map<uint64, Data::Document> _resolvedCustomEmoji;
QVector<MTPMessageRange> _splits;
rpl::event_stream<MTP::Error> _errors;
rpl::event_stream<Output::Result> _ioErrors;
};
} // namespace Export

View File

@@ -0,0 +1,924 @@
/*
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_controller.h"
#include "export/export_api_wrap.h"
#include "export/export_settings.h"
#include "export/data/export_data_types.h"
#include "export/output/export_output_abstract.h"
#include "export/output/export_output_result.h"
#include "export/output/export_output_stats.h"
#include "mtproto/mtp_instance.h"
namespace Export {
namespace {
const auto kNullStateCallback = [](ProcessingState&) {};
Settings NormalizeSettings(const Settings &settings) {
if (!settings.onlySinglePeer()) {
return base::duplicate(settings);
}
auto result = base::duplicate(settings);
result.types = result.fullChats = Settings::Type::AnyChatsMask;
return result;
}
} // namespace
class ControllerObject {
public:
ControllerObject(
crl::weak_on_queue<ControllerObject> weak,
QPointer<MTP::Instance> mtproto,
const MTPInputPeer &peer);
ControllerObject(
crl::weak_on_queue<ControllerObject> weak,
QPointer<MTP::Instance> mtproto,
const MTPInputPeer &peer,
int32 topicRootId,
uint64 peerId,
const QString &topicTitle);
rpl::producer<State> state() const;
// Password step.
//void submitPassword(const QString &password);
//void requestPasswordRecover();
//rpl::producer<PasswordUpdate> passwordUpdate() const;
//void reloadPasswordState();
//void cancelUnconfirmedPassword();
// Processing step.
void startExport(
const Settings &settings,
const Environment &environment);
void skipFile(uint64 randomId);
void cancelExportFast();
private:
using Step = ProcessingState::Step;
using DownloadProgress = ApiWrap::DownloadProgress;
[[nodiscard]] bool stopped() const;
void setState(State &&state);
void ioError(const QString &path);
bool ioCatchError(Output::Result result);
void setFinishedState();
//void requestPasswordState();
//void passwordStateDone(const MTPaccount_Password &password);
void fillExportSteps();
void fillSubstepsInSteps(const ApiWrap::StartInfo &info);
void exportNext();
void initialize();
void initialized(const ApiWrap::StartInfo &info);
void collectDialogsList();
void exportPersonalInfo();
void exportUserpics();
void exportStories();
void exportProfileMusic();
void exportContacts();
void exportSessions();
void exportOtherData();
void exportDialogs();
void exportNextDialog();
void exportTopic();
template <typename Callback = const decltype(kNullStateCallback) &>
ProcessingState prepareState(
Step step,
Callback &&callback = kNullStateCallback) const;
ProcessingState stateInitializing() const;
ProcessingState stateDialogsList(int processed) const;
ProcessingState statePersonalInfo() const;
ProcessingState stateUserpics(const DownloadProgress &progress) const;
ProcessingState stateStories(const DownloadProgress &progress) const;
ProcessingState stateProfileMusic(const DownloadProgress &progress) const;
ProcessingState stateContacts() const;
ProcessingState stateSessions() const;
ProcessingState stateOtherData() const;
ProcessingState stateDialogs(const DownloadProgress &progress) const;
ProcessingState stateTopic(const DownloadProgress &progress) const;
void fillMessagesState(
ProcessingState &result,
const Data::DialogsInfo &info,
int index,
const DownloadProgress &progress) const;
int substepsInStep(Step step) const;
ApiWrap _api;
Settings _settings;
Environment _environment;
Data::DialogsInfo _dialogsInfo;
int _dialogIndex = -1;
int _messagesWritten = 0;
int _messagesCount = 0;
int _userpicsWritten = 0;
int _userpicsCount = 0;
int _storiesWritten = 0;
int _storiesCount = 0;
int _profileMusicWritten = 0;
int _profileMusicCount = 0;
// rpl::variable<State> fails to compile in MSVC :(
State _state;
rpl::event_stream<State> _stateChanges;
Output::Stats _stats;
std::vector<int> _substepsInStep;
int _substepsTotal = 0;
mutable int _substepsPassed = 0;
mutable Step _lastProcessingStep = Step::Initializing;
std::unique_ptr<Output::AbstractWriter> _writer;
std::vector<Step> _steps;
int _stepIndex = -1;
int32 _topicRootId = 0;
uint64 _topicPeerId = 0;
QString _topicTitle;
rpl::lifetime _lifetime;
};
ControllerObject::ControllerObject(
crl::weak_on_queue<ControllerObject> weak,
QPointer<MTP::Instance> mtproto,
const MTPInputPeer &peer)
: _api(mtproto, weak.runner())
, _state(PasswordCheckState{}) {
_api.errors(
) | rpl::on_next([=](const MTP::Error &error) {
setState(ApiErrorState{ error });
}, _lifetime);
_api.ioErrors(
) | rpl::on_next([=](const Output::Result &result) {
ioCatchError(result);
}, _lifetime);
//requestPasswordState();
auto state = PasswordCheckState();
state.checked = false;
state.requesting = false;
state.singlePeer = peer;
setState(std::move(state));
}
ControllerObject::ControllerObject(
crl::weak_on_queue<ControllerObject> weak,
QPointer<MTP::Instance> mtproto,
const MTPInputPeer &peer,
int32 topicRootId,
uint64 peerId,
const QString &topicTitle)
: _api(mtproto, weak.runner())
, _state(PasswordCheckState{})
, _topicRootId(topicRootId)
, _topicPeerId(peerId)
, _topicTitle(topicTitle) {
_api.errors(
) | rpl::on_next([=](const MTP::Error &error) {
setState(ApiErrorState{ error });
}, _lifetime);
_api.ioErrors(
) | rpl::on_next([=](const Output::Result &result) {
ioCatchError(result);
}, _lifetime);
//requestPasswordState();
auto state = PasswordCheckState();
state.checked = false;
state.requesting = false;
state.singlePeer = peer;
setState(std::move(state));
}
rpl::producer<State> ControllerObject::state() const {
return rpl::single(
_state
) | rpl::then(
_stateChanges.events()
) | rpl::filter([](const State &state) {
const auto password = std::get_if<PasswordCheckState>(&state);
return !password || !password->requesting;
});
}
bool ControllerObject::stopped() const {
return v::is<CancelledState>(_state)
|| v::is<ApiErrorState>(_state)
|| v::is<OutputErrorState>(_state)
|| v::is<FinishedState>(_state);
}
void ControllerObject::setState(State &&state) {
if (stopped()) {
return;
}
_state = std::move(state);
_stateChanges.fire_copy(_state);
}
void ControllerObject::ioError(const QString &path) {
setState(OutputErrorState{ path });
}
bool ControllerObject::ioCatchError(Output::Result result) {
if (!result) {
ioError(result.path);
return true;
}
return false;
}
//void ControllerObject::submitPassword(const QString &password) {
//
//}
//
//void ControllerObject::requestPasswordRecover() {
//
//}
//
//rpl::producer<PasswordUpdate> ControllerObject::passwordUpdate() const {
// return nullptr;
//}
//
//void ControllerObject::reloadPasswordState() {
// //_mtp.request(base::take(_passwordRequestId)).cancel();
// requestPasswordState();
//}
//
//void ControllerObject::requestPasswordState() {
// if (_passwordRequestId) {
// return;
// }
// //_passwordRequestId = _mtp.request(MTPaccount_GetPassword(
// //)).done([=](const MTPaccount_Password &result) {
// // _passwordRequestId = 0;
// // passwordStateDone(result);
// //}).fail([=](const MTP::Error &error) {
// // apiError(error);
// //}).send();
//}
//
//void ControllerObject::passwordStateDone(const MTPaccount_Password &result) {
// auto state = PasswordCheckState();
// state.checked = false;
// state.requesting = false;
// state.hasPassword;
// state.hint;
// state.unconfirmedPattern;
// setState(std::move(state));
//}
//
//void ControllerObject::cancelUnconfirmedPassword() {
//
//}
void ControllerObject::startExport(
const Settings &settings,
const Environment &environment) {
if (!_settings.path.isEmpty()) {
return;
}
_settings = NormalizeSettings(settings);
_environment = environment;
_settings.singleTopicRootId = _topicRootId;
_settings.singleTopicPeerId = _topicPeerId;
_settings.path = Output::NormalizePath(_settings);
_writer = Output::CreateWriter(_settings.format);
fillExportSteps();
exportNext();
}
void ControllerObject::skipFile(uint64 randomId) {
if (stopped()) {
return;
}
_api.skipFile(randomId);
}
void ControllerObject::fillExportSteps() {
using Type = Settings::Type;
_steps.push_back(Step::Initializing);
if (_settings.onlySingleTopic()) {
_steps.push_back(Step::Topic);
return;
}
if (_settings.types & Type::AnyChatsMask) {
_steps.push_back(Step::DialogsList);
}
if (_settings.types & Type::PersonalInfo) {
_steps.push_back(Step::PersonalInfo);
}
if (_settings.types & Type::Userpics) {
_steps.push_back(Step::Userpics);
}
if (_settings.types & Type::Stories) {
_steps.push_back(Step::Stories);
}
if (_settings.types & Type::ProfileMusic) {
_steps.push_back(Step::ProfileMusic);
}
if (_settings.types & Type::Contacts) {
_steps.push_back(Step::Contacts);
}
if (_settings.types & Type::Sessions) {
_steps.push_back(Step::Sessions);
}
if (_settings.types & Type::OtherData) {
_steps.push_back(Step::OtherData);
}
if (_settings.types & Type::AnyChatsMask) {
_steps.push_back(Step::Dialogs);
}
}
void ControllerObject::fillSubstepsInSteps(const ApiWrap::StartInfo &info) {
auto result = std::vector<int>();
const auto push = [&](Step step, int count) {
const auto index = static_cast<int>(step);
if (index >= result.size()) {
result.resize(index + 1, 0);
}
result[index] = count;
};
push(Step::Initializing, 1);
if (_settings.types & Settings::Type::AnyChatsMask) {
push(Step::DialogsList, 1);
}
if (_settings.types & Settings::Type::PersonalInfo) {
push(Step::PersonalInfo, 1);
}
if (_settings.types & Settings::Type::Userpics) {
push(Step::Userpics, 1);
}
if (_settings.types & Settings::Type::Stories) {
push(Step::Stories, 1);
}
if (_settings.types & Settings::Type::ProfileMusic) {
push(Step::ProfileMusic, 1);
}
if (_settings.types & Settings::Type::Contacts) {
push(Step::Contacts, 1);
}
if (_settings.types & Settings::Type::Sessions) {
push(Step::Sessions, 1);
}
if (_settings.types & Settings::Type::OtherData) {
push(Step::OtherData, 1);
}
if (_settings.types & Settings::Type::AnyChatsMask) {
push(Step::Dialogs, info.dialogsCount);
}
if (_settings.onlySingleTopic()) {
push(Step::Topic, 1);
}
_substepsInStep = std::move(result);
_substepsTotal = ranges::accumulate(_substepsInStep, 0);
}
void ControllerObject::cancelExportFast() {
_api.cancelExportFast();
setState(CancelledState());
}
void ControllerObject::exportNext() {
if (++_stepIndex >= _steps.size()) {
if (ioCatchError(_writer->finish())) {
return;
}
_api.finishExport([=] {
setFinishedState();
});
return;
}
const auto step = _steps[_stepIndex];
switch (step) {
case Step::Initializing: return initialize();
case Step::DialogsList: return collectDialogsList();
case Step::PersonalInfo: return exportPersonalInfo();
case Step::Userpics: return exportUserpics();
case Step::Stories: return exportStories();
case Step::ProfileMusic: return exportProfileMusic();
case Step::Contacts: return exportContacts();
case Step::Sessions: return exportSessions();
case Step::OtherData: return exportOtherData();
case Step::Dialogs: return exportDialogs();
case Step::Topic: return exportTopic();
}
Unexpected("Step in ControllerObject::exportNext.");
}
void ControllerObject::initialize() {
setState(stateInitializing());
_api.startExport(_settings, &_stats, [=](ApiWrap::StartInfo info) {
initialized(info);
});
}
void ControllerObject::initialized(const ApiWrap::StartInfo &info) {
if (ioCatchError(_writer->start(_settings, _environment, &_stats))) {
return;
}
fillSubstepsInSteps(info);
exportNext();
}
void ControllerObject::collectDialogsList() {
setState(stateDialogsList(0));
_api.requestDialogsList([=](int count) {
if (count > 0) {
setState(stateDialogsList(count - 1));
}
return true;
}, [=](Data::DialogsInfo &&result) {
_dialogsInfo = std::move(result);
exportNext();
});
}
void ControllerObject::exportPersonalInfo() {
setState(statePersonalInfo());
_api.requestPersonalInfo([=](Data::PersonalInfo &&result) {
if (ioCatchError(_writer->writePersonal(result))) {
return;
}
exportNext();
});
}
void ControllerObject::exportUserpics() {
_api.requestUserpics([=](Data::UserpicsInfo &&start) {
if (ioCatchError(_writer->writeUserpicsStart(start))) {
return false;
}
_userpicsWritten = 0;
_userpicsCount = start.count;
return true;
}, [=](DownloadProgress progress) {
setState(stateUserpics(progress));
return true;
}, [=](Data::UserpicsSlice &&slice) {
if (ioCatchError(_writer->writeUserpicsSlice(slice))) {
return false;
}
_userpicsWritten += slice.list.size();
setState(stateUserpics(DownloadProgress()));
return true;
}, [=] {
if (ioCatchError(_writer->writeUserpicsEnd())) {
return;
}
exportNext();
});
}
void ControllerObject::exportStories() {
_api.requestStories([=](Data::StoriesInfo &&start) {
if (ioCatchError(_writer->writeStoriesStart(start))) {
return false;
}
_storiesWritten = 0;
_storiesCount = start.count;
return true;
}, [=](DownloadProgress progress) {
setState(stateStories(progress));
return true;
}, [=](Data::StoriesSlice &&slice) {
if (ioCatchError(_writer->writeStoriesSlice(slice))) {
return false;
}
_storiesWritten += slice.list.size();
setState(stateStories(DownloadProgress()));
return true;
}, [=] {
if (ioCatchError(_writer->writeStoriesEnd())) {
return;
}
exportNext();
});
}
void ControllerObject::exportProfileMusic() {
_api.requestProfileMusic([=](Data::ProfileMusicInfo &&start) {
if (ioCatchError(_writer->writeProfileMusicStart(start))) {
return false;
}
_profileMusicWritten = 0;
_profileMusicCount = start.count;
return true;
}, [=](DownloadProgress progress) {
setState(stateProfileMusic(progress));
return true;
}, [=](Data::ProfileMusicSlice &&slice) {
if (ioCatchError(_writer->writeProfileMusicSlice(slice))) {
return false;
}
_profileMusicWritten += slice.list.size();
setState(stateProfileMusic(DownloadProgress()));
return true;
}, [=] {
if (ioCatchError(_writer->writeProfileMusicEnd())) {
return;
}
exportNext();
});
}
void ControllerObject::exportContacts() {
setState(stateContacts());
_api.requestContacts([=](Data::ContactsList &&result) {
if (ioCatchError(_writer->writeContactsList(result))) {
return;
}
exportNext();
});
}
void ControllerObject::exportSessions() {
setState(stateSessions());
_api.requestSessions([=](Data::SessionsList &&result) {
if (ioCatchError(_writer->writeSessionsList(result))) {
return;
}
exportNext();
});
}
void ControllerObject::exportOtherData() {
setState(stateOtherData());
const auto relativePath = "lists/other_data.json";
_api.requestOtherData(relativePath, [=](Data::File &&result) {
if (ioCatchError(_writer->writeOtherData(result))) {
return;
}
exportNext();
});
}
void ControllerObject::exportDialogs() {
if (ioCatchError(_writer->writeDialogsStart(_dialogsInfo))) {
return;
}
exportNextDialog();
}
void ControllerObject::exportNextDialog() {
const auto index = ++_dialogIndex;
const auto info = _dialogsInfo.item(index);
if (info) {
_api.requestMessages(*info, [=](const Data::DialogInfo &info) {
if (ioCatchError(_writer->writeDialogStart(info))) {
return false;
}
_messagesWritten = 0;
_messagesCount = ranges::accumulate(
info.messagesCountPerSplit,
0);
setState(stateDialogs(DownloadProgress()));
return true;
}, [=](DownloadProgress progress) {
setState(stateDialogs(progress));
return true;
}, [=](Data::MessagesSlice &&result) {
if (ioCatchError(_writer->writeDialogSlice(result))) {
return false;
}
_messagesWritten += result.list.size();
setState(stateDialogs(DownloadProgress()));
return true;
}, [=] {
if (ioCatchError(_writer->writeDialogEnd())) {
return;
}
exportNextDialog();
});
return;
}
if (ioCatchError(_writer->writeDialogsEnd())) {
return;
}
exportNext();
}
template <typename Callback>
ProcessingState ControllerObject::prepareState(
Step step,
Callback &&callback) const {
if (step != _lastProcessingStep) {
_substepsPassed += substepsInStep(_lastProcessingStep);
_lastProcessingStep = step;
}
auto result = ProcessingState();
callback(result);
result.step = step;
result.substepsPassed = _substepsPassed;
result.substepsNow = substepsInStep(_lastProcessingStep);
result.substepsTotal = _substepsTotal;
return result;
}
ProcessingState ControllerObject::stateInitializing() const {
return ProcessingState();
}
ProcessingState ControllerObject::stateDialogsList(int processed) const {
const auto step = Step::DialogsList;
return prepareState(step, [&](ProcessingState &result) {
result.entityIndex = processed;
result.entityCount = std::max(
processed,
substepsInStep(Step::Dialogs));
});
}
ProcessingState ControllerObject::statePersonalInfo() const {
return prepareState(Step::PersonalInfo);
}
ProcessingState ControllerObject::stateUserpics(
const DownloadProgress &progress) const {
return prepareState(Step::Userpics, [&](ProcessingState &result) {
result.entityIndex = _userpicsWritten + progress.itemIndex;
result.entityCount = std::max(_userpicsCount, result.entityIndex);
result.bytesRandomId = progress.randomId;
if (!progress.path.isEmpty()) {
const auto last = progress.path.lastIndexOf('/');
result.bytesName = progress.path.mid(last + 1);
}
result.bytesLoaded = progress.ready;
result.bytesCount = progress.total;
});
}
ProcessingState ControllerObject::stateStories(
const DownloadProgress &progress) const {
return prepareState(Step::Stories, [&](ProcessingState &result) {
result.entityIndex = _storiesWritten + progress.itemIndex;
result.entityCount = std::max(_storiesCount, result.entityIndex);
result.bytesRandomId = progress.randomId;
if (!progress.path.isEmpty()) {
const auto last = progress.path.lastIndexOf('/');
result.bytesName = progress.path.mid(last + 1);
}
result.bytesLoaded = progress.ready;
result.bytesCount = progress.total;
});
}
ProcessingState ControllerObject::stateProfileMusic(
const DownloadProgress &progress) const {
return prepareState(Step::ProfileMusic, [&](ProcessingState &result) {
result.entityIndex = _profileMusicWritten + progress.itemIndex;
result.entityCount = std::max(_profileMusicCount, result.entityIndex);
result.bytesRandomId = progress.randomId;
if (!progress.path.isEmpty()) {
const auto last = progress.path.lastIndexOf('/');
result.bytesName = progress.path.mid(last + 1);
}
result.bytesLoaded = progress.ready;
result.bytesCount = progress.total;
});
}
ProcessingState ControllerObject::stateContacts() const {
return prepareState(Step::Contacts);
}
ProcessingState ControllerObject::stateSessions() const {
return prepareState(Step::Sessions);
}
ProcessingState ControllerObject::stateOtherData() const {
return prepareState(Step::OtherData);
}
ProcessingState ControllerObject::stateDialogs(
const DownloadProgress &progress) const {
const auto step = Step::Dialogs;
return prepareState(step, [&](ProcessingState &result) {
fillMessagesState(
result,
_dialogsInfo,
_dialogIndex,
progress);
});
}
void ControllerObject::fillMessagesState(
ProcessingState &result,
const Data::DialogsInfo &info,
int index,
const DownloadProgress &progress) const {
const auto dialog = info.item(index);
Assert(dialog != nullptr);
result.entityIndex = index;
result.entityCount = info.chats.size() + info.left.size();
result.entityName = dialog->name;
result.entityType = (dialog->type == Data::DialogInfo::Type::Self)
? ProcessingState::EntityType::SavedMessages
: (dialog->type == Data::DialogInfo::Type::Replies)
? ProcessingState::EntityType::RepliesMessages
: (dialog->type == Data::DialogInfo::Type::VerifyCodes)
? ProcessingState::EntityType::VerifyCodes
: ProcessingState::EntityType::Chat;
result.itemIndex = _messagesWritten + progress.itemIndex;
result.itemCount = std::max(_messagesCount, result.itemIndex);
result.bytesRandomId = progress.randomId;
if (!progress.path.isEmpty()) {
const auto last = progress.path.lastIndexOf('/');
result.bytesName = progress.path.mid(last + 1);
}
result.bytesLoaded = progress.ready;
result.bytesCount = progress.total;
}
int ControllerObject::substepsInStep(Step step) const {
Expects(_substepsInStep.size() > static_cast<int>(step));
return _substepsInStep[static_cast<int>(step)];
}
void ControllerObject::exportTopic() {
auto topicInfo = Data::DialogInfo();
topicInfo.type = Data::DialogInfo::Type::PublicSupergroup;
topicInfo.name = _topicTitle.toUtf8();
topicInfo.peerId = PeerId(_topicPeerId);
topicInfo.relativePath = QString();
if (ioCatchError(_writer->writeDialogStart(topicInfo))) {
return;
}
_api.requestTopicMessages(
PeerId(_topicPeerId),
_settings.singlePeer,
_topicRootId,
[=](int count) {
_messagesWritten = 0;
_messagesCount = count;
setState(stateTopic(DownloadProgress()));
return true;
},
[=](DownloadProgress progress) {
setState(stateTopic(progress));
return true;
},
[=](Data::MessagesSlice &&slice) {
if (ioCatchError(_writer->writeDialogSlice(slice))) {
return false;
}
_messagesWritten += slice.list.size();
setState(stateTopic(DownloadProgress()));
return true;
},
[=] {
if (ioCatchError(_writer->writeDialogEnd())) {
return;
}
if (ioCatchError(_writer->finish())) {
return;
}
_api.finishExport([=] {
setFinishedState();
});
});
}
ProcessingState ControllerObject::stateTopic(
const DownloadProgress &progress) const {
return prepareState(Step::Topic, [&](ProcessingState &result) {
result.entityType = ProcessingState::EntityType::Topic;
result.entityName = _topicTitle;
result.entityIndex = 0;
result.entityCount = 1;
result.itemIndex = _messagesWritten + progress.itemIndex;
result.itemCount = std::max(_messagesCount, result.itemIndex);
result.bytesRandomId = progress.randomId;
if (!progress.path.isEmpty()) {
const auto last = progress.path.lastIndexOf('/');
result.bytesName = progress.path.mid(last + 1);
}
result.bytesLoaded = progress.ready;
result.bytesCount = progress.total;
});
}
void ControllerObject::setFinishedState() {
setState(FinishedState{
_writer->mainFilePath(),
_stats.filesCount(),
_stats.bytesCount() });
}
Controller::Controller(
QPointer<MTP::Instance> mtproto,
const MTPInputPeer &peer)
: _wrapped(std::move(mtproto), peer) {
}
Controller::Controller(
QPointer<MTP::Instance> mtproto,
const MTPInputPeer &peer,
int32 topicRootId,
uint64 peerId,
const QString &topicTitle)
: _wrapped(
std::move(mtproto),
peer,
static_cast<int32>(topicRootId),
static_cast<uint64>(peerId),
topicTitle) {
}
rpl::producer<State> Controller::state() const {
return _wrapped.producer_on_main([=](const Implementation &unwrapped) {
return unwrapped.state();
});
}
//void Controller::submitPassword(const QString &password) {
// _wrapped.with([=](Implementation &unwrapped) {
// unwrapped.submitPassword(password);
// });
//}
//
//void Controller::requestPasswordRecover() {
// _wrapped.with([=](Implementation &unwrapped) {
// unwrapped.requestPasswordRecover();
// });
//}
//
//rpl::producer<PasswordUpdate> Controller::passwordUpdate() const {
// return _wrapped.producer_on_main([=](const Implementation &unwrapped) {
// return unwrapped.passwordUpdate();
// });
//}
//
//void Controller::reloadPasswordState() {
// _wrapped.with([=](Implementation &unwrapped) {
// unwrapped.reloadPasswordState();
// });
//}
//
//void Controller::cancelUnconfirmedPassword() {
// _wrapped.with([=](Implementation &unwrapped) {
// unwrapped.cancelUnconfirmedPassword();
// });
//}
void Controller::startExport(
const Settings &settings,
const Environment &environment) {
LOG(("Export Info: Started export to '%1'.").arg(settings.path));
_wrapped.with([=](Implementation &unwrapped) {
unwrapped.startExport(settings, environment);
});
}
void Controller::skipFile(uint64 randomId) {
_wrapped.with([=](Implementation &unwrapped) {
unwrapped.skipFile(randomId);
});
}
void Controller::cancelExportFast() {
LOG(("Export Info: Cancelled export."));
_wrapped.with([=](Implementation &unwrapped) {
unwrapped.cancelExportFast();
});
}
rpl::lifetime &Controller::lifetime() {
return _lifetime;
}
Controller::~Controller() {
LOG(("Export Info: Controller destroyed."));
}
} // namespace Export

View File

@@ -0,0 +1,154 @@
/*
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
*/
#pragma once
#include "base/variant.h"
#include "mtproto/mtproto_response.h"
#include <QtCore/QPointer>
#include <crl/crl_object_on_queue.h>
namespace MTP {
class Instance;
} // namespace MTP
namespace Export {
class ControllerObject;
struct Settings;
struct Environment;
struct PasswordCheckState {
QString hint;
QString unconfirmedPattern;
bool requesting = true;
bool hasPassword = false;
bool checked = false;
MTPInputPeer singlePeer = MTP_inputPeerEmpty();
};
struct ProcessingState {
enum class Step {
Initializing,
DialogsList,
PersonalInfo,
Userpics,
Stories,
ProfileMusic,
Contacts,
Sessions,
OtherData,
Dialogs,
Topic,
};
enum class EntityType {
Chat,
SavedMessages,
RepliesMessages,
VerifyCodes,
Topic,
Other,
};
Step step = Step::Initializing;
int substepsPassed = 0;
int substepsNow = 0;
int substepsTotal = 0;
EntityType entityType = EntityType::Other;
QString entityName;
int entityIndex = 0;
int entityCount = 0;
int itemIndex = 0;
int itemCount = 0;
uint64 bytesRandomId = 0;
QString bytesName;
int64 bytesLoaded = 0;
int64 bytesCount = 0;
};
struct ApiErrorState {
MTP::Error data;
};
struct OutputErrorState {
QString path;
};
struct CancelledState {
};
struct FinishedState {
QString path;
int filesCount = 0;
int64 bytesCount = 0;
};
using State = std::variant<
v::null_t,
PasswordCheckState,
ProcessingState,
ApiErrorState,
OutputErrorState,
CancelledState,
FinishedState>;
//struct PasswordUpdate {
// enum class Type {
// CheckSucceed,
// WrongPassword,
// FloodLimit,
// RecoverUnavailable,
// };
// Type type = Type::WrongPassword;
//
//};
class Controller {
public:
Controller(
QPointer<MTP::Instance> mtproto,
const MTPInputPeer &peer);
Controller(
QPointer<MTP::Instance> mtproto,
const MTPInputPeer &peer,
int32 topicRootId,
uint64 peerId,
const QString &topicTitle);
rpl::producer<State> state() const;
// Password step.
//void submitPassword(const QString &password);
//void requestPasswordRecover();
//rpl::producer<PasswordUpdate> passwordUpdate() const;
//void reloadPasswordState();
//void cancelUnconfirmedPassword();
// Processing step.
void startExport(
const Settings &settings,
const Environment &environment);
void skipFile(uint64 randomId);
void cancelExportFast();
rpl::lifetime &lifetime();
~Controller();
private:
using Implementation = ControllerObject;
crl::object_on_queue<Implementation> _wrapped;
rpl::lifetime _lifetime;
};
} // namespace Export

View File

@@ -0,0 +1,116 @@
/*
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_manager.h"
#include "export/export_controller.h"
#include "export/view/export_view_panel_controller.h"
#include "data/data_peer.h"
#include "main/main_session.h"
#include "main/main_account.h"
#include "ui/layers/box_content.h"
#include "base/unixtime.h"
namespace Export {
Manager::Manager() = default;
Manager::~Manager() = default;
void Manager::start(not_null<PeerData*> peer) {
start(&peer->session(), peer->input());
}
void Manager::startTopic(
not_null<PeerData*> peer,
MsgId topicRootId,
const QString &topicTitle) {
if (_panel) {
_panel->activatePanel();
return;
}
_controller = std::make_unique<Controller>(
&peer->session().mtp(),
peer->input(),
int32(topicRootId.bare),
uint64(peer->id.value),
topicTitle);
setupPanel(&peer->session());
}
void Manager::start(
not_null<Main::Session*> session,
const MTPInputPeer &singlePeer) {
if (_panel) {
_panel->activatePanel();
return;
}
_controller = std::make_unique<Controller>(
&session->mtp(),
singlePeer);
setupPanel(session);
}
void Manager::setupPanel(not_null<Main::Session*> session) {
_panel = std::make_unique<View::PanelController>(
session,
_controller.get());
session->account().sessionChanges(
) | rpl::filter([=](Main::Session *value) {
return (value != session);
}) | rpl::on_next([=] {
stop();
}, _panel->lifetime());
_viewChanges.fire(_panel.get());
_panel->stopRequests(
) | rpl::on_next([=] {
LOG(("Export Info: Stop requested."));
stop();
}, _controller->lifetime());
}
rpl::producer<View::PanelController*> Manager::currentView(
) const {
return _viewChanges.events_starting_with(_panel.get());
}
bool Manager::inProgress() const {
return _controller != nullptr;
}
bool Manager::inProgress(not_null<Main::Session*> session) const {
return _panel && (&_panel->session() == session);
}
void Manager::stopWithConfirmation(Fn<void()> callback) {
if (!_panel) {
callback();
return;
}
auto closeAndCall = [=, callback = std::move(callback)]() mutable {
auto saved = std::move(callback);
LOG(("Export Info: Stop With Confirmation."));
stop();
if (saved) {
saved();
}
};
_panel->stopWithConfirmation(std::move(closeAndCall));
}
void Manager::stop() {
if (_panel) {
LOG(("Export Info: Destroying."));
_panel = nullptr;
_viewChanges.fire(nullptr);
}
_controller = nullptr;
}
} // namespace Export

View File

@@ -0,0 +1,57 @@
/*
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
*/
#pragma once
class PeerData;
namespace Ui {
class BoxContent;
} // namespace Ui
namespace Main {
class Session;
} // namespace Main
namespace Export {
class Controller;
namespace View {
class PanelController;
} // namespace View
class Manager final {
public:
Manager();
~Manager();
void start(not_null<PeerData*> peer);
void start(
not_null<Main::Session*> session,
const MTPInputPeer &singlePeer = MTP_inputPeerEmpty());
void startTopic(
not_null<PeerData*> peer,
MsgId topicRootId,
const QString &topicTitle);
[[nodiscard]] rpl::producer<View::PanelController*> currentView() const;
[[nodiscard]] bool inProgress() const;
[[nodiscard]] bool inProgress(not_null<Main::Session*> session) const;
void stopWithConfirmation(Fn<void()> callback);
void stop();
private:
void setupPanel(not_null<Main::Session*> session);
std::unique_ptr<Controller> _controller;
std::unique_ptr<View::PanelController> _panel;
rpl::event_stream<View::PanelController*> _viewChanges;
};
} // namespace Export

View File

@@ -0,0 +1,35 @@
/*
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 <QtCore/QFile>
#include <QtCore/QFileInfo>
#include <QtCore/QDir>
#include <QtCore/QSize>
#include <QtCore/QTextStream>
#include <QtCore/QDateTime>
#include <QtCore/QString>
#include <QtCore/QByteArray>
#include <QtCore/QReadWriteLock>
#include <QtCore/QRegularExpression>
#include <crl/crl.h>
#include <rpl/rpl.h>
#include <vector>
#include <map>
#include <set>
#include <deque>
#include <atomic>
#include <range/v3/all.hpp>
#include "base/flat_map.h"
#include "base/flat_set.h"
#include "scheme.h"
#include "logs.h"

View File

@@ -0,0 +1,50 @@
/*
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 = 4000 * int64(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::Html && format != Format::Json) {
return false;
} else if (!media.validate()) {
return false;
} else if (singlePeerTill > 0 && singlePeerTill <= singlePeerFrom) {
return false;
}
return true;
};
} // namespace Export

View File

@@ -0,0 +1,133 @@
/*
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
*/
#pragma once
#include "base/flags.h"
#include "base/flat_map.h"
namespace Export {
namespace Output {
enum class Format;
} // namespace Output
struct MediaSettings {
bool validate() const;
enum class Type {
Photo = 0x01,
Video = 0x02,
VoiceMessage = 0x04,
VideoMessage = 0x08,
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; };
Types types = DefaultTypes();
int64 sizeLimit = 8 * 1024 * 1024;
static inline Types DefaultTypes() {
return Type::Photo;
}
};
struct Settings {
bool validate() const;
enum class Type {
PersonalInfo = 0x001,
Userpics = 0x002,
Contacts = 0x004,
Sessions = 0x008,
OtherData = 0x010,
PersonalChats = 0x020,
BotChats = 0x040,
PrivateGroups = 0x080,
PublicGroups = 0x100,
PrivateChannels = 0x200,
PublicChannels = 0x400,
Stories = 0x800,
ProfileMusic = 0x1000,
GroupsMask = PrivateGroups | PublicGroups,
ChannelsMask = PrivateChannels | PublicChannels,
GroupsChannelsMask = GroupsMask | ChannelsMask,
NonChannelChatsMask = PersonalChats | BotChats | PrivateGroups,
AnyChatsMask = PersonalChats | BotChats | GroupsChannelsMask,
NonChatsMask = (PersonalInfo
| Userpics
| Contacts
| Stories
| ProfileMusic
| Sessions),
AllMask = NonChatsMask | OtherData | AnyChatsMask,
};
using Types = base::flags<Type>;
friend inline constexpr auto is_flag_type(Type) { return true; };
QString path;
bool forceSubPath = false;
Output::Format format = Output::Format();
Types types = DefaultTypes();
Types fullChats = DefaultFullChats();
MediaSettings media;
MTPInputPeer singlePeer = MTP_inputPeerEmpty();
TimeId singlePeerFrom = 0;
TimeId singlePeerTill = 0;
int32 singleTopicRootId = 0;
uint64 singleTopicPeerId = 0;
QString singleTopicTitle;
TimeId availableAt = 0;
bool onlySinglePeer() const {
return singlePeer.type() != mtpc_inputPeerEmpty;
}
bool onlySingleTopic() const {
return onlySinglePeer() && singleTopicRootId != 0;
}
static inline Types DefaultTypes() {
return Type::PersonalInfo
| Type::Userpics
| Type::Contacts
| Type::Stories
| Type::ProfileMusic
| Type::PersonalChats
| Type::PrivateGroups;
}
static inline Types DefaultFullChats() {
return Type::PersonalChats
| Type::BotChats;
}
};
struct Environment {
QString internalLinksDomain;
QByteArray aboutTelegram;
QByteArray aboutContacts;
QByteArray aboutFrequent;
QByteArray aboutSessions;
QByteArray aboutWebSessions;
QByteArray aboutChats;
QByteArray aboutLeftChats;
};
} // namespace Export

View File

@@ -0,0 +1,499 @@
/*
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/output/export_output_abstract.h"
#include "export/output/export_output_html_and_json.h"
#include "export/output/export_output_html.h"
#include "export/output/export_output_json.h"
#include "export/output/export_output_stats.h"
#include "export/output/export_output_result.h"
#include <QtCore/QDir>
#include <QtCore/QDate>
namespace Export {
namespace Output {
QString NormalizePath(const Settings &settings) {
QDir folder(settings.path);
const auto path = folder.absolutePath();
auto result = path.endsWith('/') ? path : (path + '/');
if (!folder.exists() && !settings.forceSubPath) {
return result;
}
const auto mode = QDir::AllEntries | QDir::NoDotAndDotDot;
const auto list = folder.entryInfoList(mode);
if (list.isEmpty() && !settings.forceSubPath) {
return result;
}
const auto date = QDate::currentDate();
const auto base = QString(settings.onlySinglePeer()
? "ChatExport_%1"
: "DataExport_%1"
).arg(date.toString(Qt::ISODate));
const auto add = [&](int i) {
return base + (i ? " (" + QString::number(i) + ')' : QString());
};
auto index = 0;
while (QDir(result + add(index)).exists()) {
++index;
}
result += add(index) + '/';
return result;
}
std::unique_ptr<AbstractWriter> CreateWriter(Format format) {
switch (format) {
case Format::Html: return std::make_unique<HtmlWriter>();
case Format::Json: return std::make_unique<JsonWriter>();
case Format::HtmlAndJson: return std::make_unique<HtmlAndJsonWriter>();
}
Unexpected("Format in Export::Output::CreateWriter.");
}
Stats AbstractWriter::produceTestExample(
const QString &path,
const Environment &environment) {
auto result = Stats();
const auto folder = QDir(path).absolutePath();
auto settings = Settings();
settings.format = format();
settings.path = (folder.endsWith('/') ? folder : (folder + '/'))
+ "ExportExample/";
settings.types = Settings::Type::AllMask;
settings.fullChats = Settings::Type::AllMask
& ~(Settings::Type::PublicChannels | Settings::Type::PublicGroups);
settings.media.types = MediaSettings::Type::AllMask;
settings.media.sizeLimit = 1024 * 1024;
const auto check = [](Result result) {
Assert(result.isSuccess());
};
check(start(settings, environment, &result));
const auto counter = [&] {
static auto GlobalCounter = 0;
return ++GlobalCounter;
};
const auto date = [&] {
return time(nullptr) - 86400 + counter();
};
const auto prevdate = [&] {
return date() - 86400;
};
auto personal = Data::PersonalInfo();
personal.bio = "Nice text about me.";
personal.user.info.firstName = "John";
personal.user.info.lastName = "Preston";
personal.user.info.phoneNumber = "447400000000";
personal.user.info.date = date();
personal.user.username = "preston";
personal.user.info.userId = counter();
personal.user.isBot = false;
personal.user.isSelf = true;
check(writePersonal(personal));
const auto generatePhoto = [&] {
static auto index = 0;
auto result = Data::Photo();
result.date = date();
result.id = counter();
result.image.width = 512;
result.image.height = 512;
result.image.file.relativePath = "files/photo_"
+ QString::number(++index)
+ ".jpg";
return result;
};
auto userpics = Data::UserpicsInfo();
userpics.count = 3;
auto userpicsSlice1 = Data::UserpicsSlice();
userpicsSlice1.list.push_back(generatePhoto());
userpicsSlice1.list.push_back(generatePhoto());
auto userpicsSlice2 = Data::UserpicsSlice();
userpicsSlice2.list.push_back(generatePhoto());
check(writeUserpicsStart(userpics));
check(writeUserpicsSlice(userpicsSlice1));
check(writeUserpicsSlice(userpicsSlice2));
check(writeUserpicsEnd());
auto contacts = Data::ContactsList();
auto topUser = Data::TopPeer();
auto user = personal.user;
auto peerUser = Data::Peer{ user };
topUser.peer = peerUser;
topUser.rating = 0.5;
auto topChat = Data::TopPeer();
auto chat = Data::Chat();
chat.bareId = counter();
chat.title = "Group chat";
auto peerChat = Data::Peer{ chat };
topChat.peer = peerChat;
topChat.rating = 0.25;
auto topBot = Data::TopPeer();
auto bot = Data::User();
bot.info.date = date();
bot.isBot = true;
bot.info.firstName = "Bot";
bot.info.lastName = "Father";
bot.info.userId = counter();
bot.username = "botfather";
auto peerBot = Data::Peer{ bot };
topBot.peer = peerBot;
topBot.rating = 0.125;
auto peers = std::map<PeerId, Data::Peer>();
peers.emplace(peerUser.id(), peerUser);
peers.emplace(peerBot.id(), peerBot);
peers.emplace(peerChat.id(), peerChat);
contacts.correspondents.push_back(topUser);
contacts.correspondents.push_back(topChat);
contacts.inlineBots.push_back(topBot);
contacts.inlineBots.push_back(topBot);
contacts.phoneCalls.push_back(topUser);
contacts.list.push_back(user.info);
contacts.list.push_back(bot.info);
check(writeContactsList(contacts));
auto sessions = Data::SessionsList();
auto session = Data::Session();
session.applicationName = "Telegram Desktop";
session.applicationVersion = "1.3.8";
session.country = "GB";
session.created = date();
session.deviceModel = "PC";
session.ip = "127.0.0.1";
session.lastActive = date();
session.platform = "Windows";
session.region = "London";
session.systemVersion = "10";
sessions.list.push_back(session);
sessions.list.push_back(session);
auto webSession = Data::WebSession();
webSession.botUsername = "botfather";
webSession.browser = "Google Chrome";
webSession.created = date();
webSession.domain = "telegram.org";
webSession.ip = "127.0.0.1";
webSession.lastActive = date();
webSession.platform = "Windows";
webSession.region = "London, GB";
sessions.webList.push_back(webSession);
sessions.webList.push_back(webSession);
check(writeSessionsList(sessions));
auto sampleMessage = [&] {
auto message = Data::Message();
message.id = counter();
message.date = prevdate();
message.edited = date();
static auto count = 0;
if (++count % 3 == 0) {
message.forwardedFromId = peerFromUser(user.info.userId);
message.forwardedDate = date();
} else if (count % 3 == 2) {
message.forwardedFromName = "Test hidden forward";
message.forwardedDate = date();
}
message.fromId = user.info.userId;
message.replyToMsgId = counter();
message.viaBotId = bot.info.userId;
message.text.push_back(Data::TextPart{
Data::TextPart::Type::Text,
("Text message " + QString::number(counter())).toUtf8()
});
return message;
};
auto sliceBot1 = Data::MessagesSlice();
sliceBot1.peers = peers;
sliceBot1.list.push_back(sampleMessage());
sliceBot1.list.push_back([&] {
auto message = sampleMessage();
message.media.content = generatePhoto();
message.media.ttl = counter();
return message;
}());
sliceBot1.list.push_back([&] {
auto message = sampleMessage();
auto document = Data::Document();
document.date = prevdate();
document.duration = counter();
auto photo = generatePhoto();
document.file = photo.image.file;
document.width = photo.image.width;
document.height = photo.image.height;
document.id = counter();
message.media.content = document;
return message;
}());
sliceBot1.list.push_back([&] {
auto message = sampleMessage();
auto contact = Data::SharedContact();
contact.info = user.info;
message.media.content = contact;
return message;
}());
auto sliceBot2 = Data::MessagesSlice();
sliceBot2.peers = peers;
sliceBot2.list.push_back([&] {
auto message = sampleMessage();
auto point = Data::GeoPoint();
point.latitude = 1.5;
point.longitude = 2.8;
point.valid = true;
message.media.content = point;
message.media.ttl = counter();
return message;
}());
sliceBot2.list.push_back([&] {
auto message = sampleMessage();
message.replyToMsgId = sliceBot1.list.back().id;
auto venue = Data::Venue();
venue.point.latitude = 1.5;
venue.point.longitude = 2.8;
venue.point.valid = true;
venue.address = "Test address";
venue.title = "Test venue";
message.media.content = venue;
return message;
}());
sliceBot2.list.push_back([&] {
auto message = sampleMessage();
auto game = Data::Game();
game.botId = bot.info.userId;
game.title = "Test game";
game.description = "Test game description";
game.id = counter();
game.shortName = "testgame";
message.media.content = game;
return message;
}());
sliceBot2.list.push_back([&] {
auto message = sampleMessage();
auto invoice = Data::Invoice();
invoice.amount = counter();
invoice.currency = "GBP";
invoice.title = "Huge invoice.";
invoice.description = "So money.";
invoice.receiptMsgId = sliceBot2.list.front().id;
message.media.content = invoice;
return message;
}());
auto serviceMessage = [&] {
auto message = Data::Message();
message.id = counter();
message.date = prevdate();
message.fromId = user.info.userId;
return message;
};
auto sliceChat1 = Data::MessagesSlice();
sliceChat1.peers = peers;
sliceChat1.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionChatCreate();
action.title = "Test chat";
action.userIds.push_back(user.info.userId);
action.userIds.push_back(bot.info.userId);
message.action.content = action;
return message;
}());
sliceChat1.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionChatEditTitle();
action.title = "New title";
message.action.content = action;
return message;
}());
sliceChat1.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionChatEditPhoto();
action.photo = generatePhoto();
message.action.content = action;
return message;
}());
sliceChat1.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionChatDeletePhoto();
message.action.content = action;
return message;
}());
sliceChat1.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionChatAddUser();
action.userIds.push_back(user.info.userId);
action.userIds.push_back(bot.info.userId);
message.action.content = action;
return message;
}());
sliceChat1.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionChatDeleteUser();
action.userId = bot.info.userId;
message.action.content = action;
return message;
}());
sliceChat1.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionChatJoinedByLink();
action.inviterId = bot.info.userId;
message.action.content = action;
return message;
}());
sliceChat1.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionChannelCreate();
action.title = "Channel name";
message.action.content = action;
return message;
}());
sliceChat1.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionChatMigrateTo();
action.channelId = ChannelId(chat.bareId);
message.action.content = action;
return message;
}());
sliceChat1.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionChannelMigrateFrom();
action.chatId = ChatId(chat.bareId);
action.title = "Supergroup now";
message.action.content = action;
return message;
}());
auto sliceChat2 = Data::MessagesSlice();
sliceChat2.peers = peers;
sliceChat2.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionPinMessage();
message.replyToMsgId = sliceChat1.list.back().id;
message.action.content = action;
return message;
}());
sliceChat2.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionHistoryClear();
message.action.content = action;
return message;
}());
sliceChat2.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionGameScore();
action.score = counter();
action.gameId = counter();
message.replyToMsgId = sliceChat2.list.back().id;
message.action.content = action;
return message;
}());
sliceChat2.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionPaymentSent();
action.amount = counter();
action.currency = "GBP";
message.replyToMsgId = sliceChat2.list.front().id;
message.action.content = action;
return message;
}());
sliceChat2.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionPhoneCall();
action.duration = counter();
action.state = Data::ActionPhoneCall::State::Busy;
message.action.content = action;
return message;
}());
sliceChat2.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionScreenshotTaken();
message.action.content = action;
return message;
}());
sliceChat2.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionCustomAction();
action.message = "Custom chat action.";
message.action.content = action;
return message;
}());
sliceChat2.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionBotAllowed();
action.domain = "telegram.org";
message.action.content = action;
return message;
}());
sliceChat2.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionSecureValuesSent();
using Type = Data::ActionSecureValuesSent::Type;
action.types.push_back(Type::BankStatement);
action.types.push_back(Type::Phone);
message.action.content = action;
return message;
}());
sliceChat2.list.push_back([&] {
auto message = serviceMessage();
auto action = Data::ActionContactSignUp();
message.action.content = action;
return message;
}());
auto dialogs = Data::DialogsInfo();
auto dialogBot = Data::DialogInfo();
dialogBot.messagesCountPerSplit.push_back(sliceBot1.list.size());
dialogBot.messagesCountPerSplit.push_back(sliceBot2.list.size());
dialogBot.type = Data::DialogInfo::Type::Bot;
dialogBot.name = peerBot.name();
dialogBot.onlyMyMessages = false;
dialogBot.peerId = peerBot.id();
dialogBot.relativePath = "chats/chat_"
+ QString::number(counter())
+ '/';
dialogBot.splits.push_back(0);
dialogBot.splits.push_back(1);
dialogBot.topMessageDate = sliceBot2.list.back().date;
dialogBot.topMessageId = sliceBot2.list.back().id;
auto dialogChat = Data::DialogInfo();
dialogChat.messagesCountPerSplit.push_back(sliceChat1.list.size());
dialogChat.messagesCountPerSplit.push_back(sliceChat2.list.size());
dialogChat.type = Data::DialogInfo::Type::PrivateGroup;
dialogChat.name = peerChat.name();
dialogChat.onlyMyMessages = true;
dialogChat.peerId = peerChat.id();
dialogChat.relativePath = "chats/chat_"
+ QString::number(counter())
+ '/';
dialogChat.splits.push_back(0);
dialogChat.splits.push_back(1);
dialogChat.topMessageDate = sliceChat2.list.back().date;
dialogChat.topMessageId = sliceChat2.list.back().id;
dialogs.chats.push_back(dialogBot);
dialogs.chats.push_back(dialogChat);
check(writeDialogsStart(dialogs));
check(writeDialogStart(dialogBot));
check(writeDialogSlice(sliceBot1));
check(writeDialogSlice(sliceBot2));
check(writeDialogEnd());
check(writeDialogStart(dialogChat));
check(writeDialogSlice(sliceChat1));
check(writeDialogSlice(sliceChat2));
check(writeDialogEnd());
check(writeDialogsEnd());
check(finish());
return result;
}
} // namespace Output
} // namespace Export

View File

@@ -0,0 +1,108 @@
/*
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
*/
#pragma once
#include <QtCore/QString>
namespace Export {
namespace Data {
struct PersonalInfo;
struct UserpicsInfo;
struct UserpicsSlice;
struct StoriesInfo;
struct StoriesSlice;
struct ProfileMusicInfo;
struct ProfileMusicSlice;
struct ContactsList;
struct SessionsList;
struct DialogsInfo;
struct DialogInfo;
struct MessagesSlice;
struct File;
} // namespace Data
struct Settings;
struct Environment;
namespace Output {
QString NormalizePath(const Settings &settings);
struct Result;
class Stats;
enum class Format {
Html,
Json,
HtmlAndJson,
};
class AbstractWriter {
public:
[[nodiscard]] virtual Format format() = 0;
[[nodiscard]] virtual Result start(
const Settings &settings,
const Environment &environment,
Stats *stats) = 0;
[[nodiscard]] virtual Result writePersonal(
const Data::PersonalInfo &data) = 0;
[[nodiscard]] virtual Result writeUserpicsStart(
const Data::UserpicsInfo &data) = 0;
[[nodiscard]] virtual Result writeUserpicsSlice(
const Data::UserpicsSlice &data) = 0;
[[nodiscard]] virtual Result writeUserpicsEnd() = 0;
[[nodiscard]] virtual Result writeStoriesStart(
const Data::StoriesInfo &data) = 0;
[[nodiscard]] virtual Result writeStoriesSlice(
const Data::StoriesSlice &data) = 0;
[[nodiscard]] virtual Result writeStoriesEnd() = 0;
[[nodiscard]] virtual Result writeProfileMusicStart(
const Data::ProfileMusicInfo &data) = 0;
[[nodiscard]] virtual Result writeProfileMusicSlice(
const Data::ProfileMusicSlice &data) = 0;
[[nodiscard]] virtual Result writeProfileMusicEnd() = 0;
[[nodiscard]] virtual Result writeContactsList(
const Data::ContactsList &data) = 0;
[[nodiscard]] virtual Result writeSessionsList(
const Data::SessionsList &data) = 0;
[[nodiscard]] virtual Result writeOtherData(
const Data::File &data) = 0;
[[nodiscard]] virtual Result writeDialogsStart(
const Data::DialogsInfo &data) = 0;
[[nodiscard]] virtual Result writeDialogStart(
const Data::DialogInfo &data) = 0;
[[nodiscard]] virtual Result writeDialogSlice(
const Data::MessagesSlice &data) = 0;
[[nodiscard]] virtual Result writeDialogEnd() = 0;
[[nodiscard]] virtual Result writeDialogsEnd() = 0;
[[nodiscard]] virtual Result finish() = 0;
[[nodiscard]] virtual QString mainFilePath() = 0;
virtual ~AbstractWriter() = default;
Stats produceTestExample(
const QString &path,
const Environment &environment);
};
std::unique_ptr<AbstractWriter> CreateWriter(Format format);
} // namespace Output
} // namespace Export

View File

@@ -0,0 +1,140 @@
/*
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/output/export_output_file.h"
#include "export/output/export_output_result.h"
#include "export/output/export_output_stats.h"
#include "base/qt/qt_string_view.h"
#include <QtCore/QFileInfo>
#include <QtCore/QDir>
#include <gsl/util>
namespace Export {
namespace Output {
File::File(const QString &path, Stats *stats) : _path(path), _stats(stats) {
}
int64 File::size() const {
return _offset;
}
bool File::empty() const {
return !_offset;
}
Result File::writeBlock(const QByteArray &block) {
const auto result = writeBlockAttempt(block);
if (!result) {
_file.reset();
}
return result;
}
Result File::writeBlockAttempt(const QByteArray &block) {
if (_stats && !_inStats) {
_inStats = true;
_stats->incrementFiles();
}
if (const auto result = reopen(); !result) {
return result;
}
const auto size = block.size();
if (!size) {
return Result::Success();
}
if (_file->write(block) == size && _file->flush()) {
_offset += size;
if (_stats) {
_stats->incrementBytes(size);
}
return Result::Success();
}
return error();
}
Result File::reopen() {
if (_file && _file->isOpen()) {
return Result::Success();
}
_file.emplace(_path);
if (_file->exists()) {
if (_file->size() < _offset) {
return fatalError();
} else if (!_file->resize(_offset)) {
return error();
}
} else if (_offset > 0) {
return fatalError();
}
if (_file->open(QIODevice::Append)) {
return Result::Success();
}
const auto info = QFileInfo(_path);
const auto dir = info.absoluteDir();
return (!dir.exists()
&& dir.mkpath(dir.absolutePath())
&& _file->open(QIODevice::Append))
? Result::Success()
: error();
}
Result File::error() const {
return Result(Result::Type::Error, _path);
}
Result File::fatalError() const {
return Result(Result::Type::FatalError, _path);
}
QString File::PrepareRelativePath(
const QString &folder,
const QString &suggested) {
if (!QFile::exists(folder + suggested)) {
return suggested;
}
// Not lastIndexOf('.') so that "file.tar.xz" won't be messed up.
const auto position = suggested.indexOf('.');
const auto base = suggested.mid(0, position);
const auto extension = (position >= 0)
? base::StringViewMid(suggested, position)
: QStringView();
const auto relativePart = [&](int attempt) {
auto result = base + QString(" (%1)").arg(attempt);
result.append(extension);
return result;
};
auto attempt = 0;
while (true) {
const auto relativePath = relativePart(++attempt);
if (!QFile::exists(folder + relativePath)) {
return relativePath;
}
}
}
Result File::Copy(
const QString &source,
const QString &path,
Stats *stats) {
QFile f(source);
if (!f.exists() || !f.open(QIODevice::ReadOnly)) {
return Result(Result::Type::FatalError, source);
}
const auto bytes = f.readAll();
if (bytes.size() != f.size()) {
return Result(Result::Type::FatalError, source);
}
return File(path, stats).writeBlock(bytes);
}
} // namespace Output
} // namespace File

View File

@@ -0,0 +1,57 @@
/*
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
*/
#pragma once
#include "base/optional.h"
#include <QtCore/QFile>
#include <QtCore/QString>
#include <QtCore/QByteArray>
namespace Export {
namespace Output {
struct Result;
class Stats;
class File {
public:
File(const QString &path, Stats *stats);
[[nodiscard]] int64 size() const;
[[nodiscard]] bool empty() const;
[[nodiscard]] Result writeBlock(const QByteArray &block);
[[nodiscard]] static QString PrepareRelativePath(
const QString &folder,
const QString &suggested);
[[nodiscard]] static Result Copy(
const QString &source,
const QString &path,
Stats *stats);
private:
[[nodiscard]] Result reopen();
[[nodiscard]] Result writeBlockAttempt(const QByteArray &block);
[[nodiscard]] Result error() const;
[[nodiscard]] Result fatalError() const;
QString _path;
int64 _offset = 0;
std::optional<QFile> _file;
Stats *_stats = nullptr;
bool _inStats = false;
};
} // namespace Output
} // namespace File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,186 @@
/*
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
*/
#pragma once
#include "export/output/export_output_abstract.h"
#include "export/output/export_output_file.h"
#include "export/export_settings.h"
#include "export/data/export_data_types.h"
namespace Export {
namespace Output {
namespace details {
class HtmlContext {
public:
[[nodiscard]] QByteArray pushTag(
const QByteArray &tag,
std::map<QByteArray, QByteArray> &&attributes = {});
[[nodiscard]] QByteArray popTag();
[[nodiscard]] QByteArray indent() const;
[[nodiscard]] bool empty() const;
private:
struct Tag {
QByteArray name;
bool block = true;
};
std::vector<Tag> _tags;
};
struct UserpicData;
struct StoryData;
class PeersMap;
struct MediaData;
} // namespace details
class HtmlWriter : public AbstractWriter {
public:
HtmlWriter();
Format format() override {
return Format::Html;
}
Result start(
const Settings &settings,
const Environment &environment,
Stats *stats) override;
Result writePersonal(const Data::PersonalInfo &data) override;
Result writeUserpicsStart(const Data::UserpicsInfo &data) override;
Result writeUserpicsSlice(const Data::UserpicsSlice &data) override;
Result writeUserpicsEnd() override;
Result writeStoriesStart(const Data::StoriesInfo &data) override;
Result writeStoriesSlice(const Data::StoriesSlice &data) override;
Result writeStoriesEnd() override;
Result writeProfileMusicStart(const Data::ProfileMusicInfo &data) override;
Result writeProfileMusicSlice(const Data::ProfileMusicSlice &data) override;
Result writeProfileMusicEnd() override;
Result writeContactsList(const Data::ContactsList &data) override;
Result writeSessionsList(const Data::SessionsList &data) override;
Result writeOtherData(const Data::File &data) override;
Result writeDialogsStart(const Data::DialogsInfo &data) override;
Result writeDialogStart(const Data::DialogInfo &data) override;
Result writeDialogSlice(const Data::MessagesSlice &data) override;
Result writeDialogEnd() override;
Result writeDialogsEnd() override;
Result finish() override;
QString mainFilePath() override;
~HtmlWriter();
private:
using Context = details::HtmlContext;
using UserpicData = details::UserpicData;
using MediaData = details::MediaData;
class Wrap;
struct MessageInfo;
enum class DialogsMode {
None,
Chats,
Left,
};
[[nodiscard]] Result copyFile(
const QString &source,
const QString &relativePath) const;
[[nodiscard]] QString mainFileRelativePath() const;
[[nodiscard]] QString pathWithRelativePath(const QString &path) const;
[[nodiscard]] std::unique_ptr<Wrap> fileWithRelativePath(
const QString &path) const;
[[nodiscard]] QString messagesFile(int index) const;
[[nodiscard]] Result writeSavedContacts(const Data::ContactsList &data);
[[nodiscard]] Result writeFrequentContacts(const Data::ContactsList &data);
[[nodiscard]] Result writeSessions(const Data::SessionsList &data);
[[nodiscard]] Result writeWebSessions(const Data::SessionsList &data);
[[nodiscard]] Result validateDialogsMode(bool isLeftChannel);
[[nodiscard]] Result writeDialogOpening(int index);
[[nodiscard]] Result switchToNextChatFile(int index);
[[nodiscard]] Result writeEmptySinglePeer();
void pushSection(
int priority,
const QByteArray &label,
const QByteArray &type,
int count,
const QString &path);
[[nodiscard]] Result writeSections();
[[nodiscard]] Result writeDefaultPersonal(
const Data::PersonalInfo &data);
[[nodiscard]] Result writeDelayedPersonal(const QString &userpicPath);
[[nodiscard]] Result writePreparedPersonal(
const Data::PersonalInfo &data,
const QString &userpicPath);
void pushUserpicsSection();
void pushStoriesSection();
void pushProfileMusicSection();
[[nodiscard]] QString userpicsFilePath() const;
[[nodiscard]] QString storiesFilePath() const;
[[nodiscard]] QString profileMusicFilePath() const;
[[nodiscard]] QByteArray wrapMessageLink(
int messageId,
QByteArray text);
Settings _settings;
Environment _environment;
Stats *_stats = nullptr;
struct SavedSection;
std::vector<SavedSection> _savedSections;
std::unique_ptr<Wrap> _summary;
bool _summaryNeedDivider = false;
bool _haveSections = false;
uint8 _selfColorIndex = 0;
std::unique_ptr<Data::PersonalInfo> _delayedPersonalInfo;
int _userpicsCount = 0;
std::unique_ptr<Wrap> _userpics;
int _storiesCount = 0;
std::unique_ptr<Wrap> _stories;
int _profileMusicCount = 0;
std::unique_ptr<Wrap> _profileMusic;
QString _dialogsRelativePath;
Data::DialogInfo _dialog;
DialogsMode _dialogsMode = DialogsMode::None;
int _messagesCount = 0;
std::unique_ptr<MessageInfo> _lastMessageInfo;
int _dateMessageId = 0;
std::unique_ptr<Wrap> _chats;
std::unique_ptr<Wrap> _chat;
std::vector<int> _lastMessageIdsPerFile;
bool _chatFileEmpty = false;
};
} // namespace Output
} // namespace Export

View File

@@ -0,0 +1,166 @@
/*
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/output/export_output_html_and_json.h"
#include "export/output/export_output_html.h"
#include "export/output/export_output_json.h"
#include "export/output/export_output_result.h"
namespace Export::Output {
HtmlAndJsonWriter::HtmlAndJsonWriter() {
_writers.push_back(CreateWriter(Format::Html));
_writers.push_back(CreateWriter(Format::Json));
}
Format HtmlAndJsonWriter::format() {
return Format::HtmlAndJson;
}
Result HtmlAndJsonWriter::start(
const Settings &settings,
const Environment &environment,
Stats *stats) {
return invoke([&](WriterPtr w) {
return w->start(settings, environment, stats);
});
}
Result HtmlAndJsonWriter::writePersonal(const Data::PersonalInfo &data) {
return invoke([&](WriterPtr w) {
return w->writePersonal(data);
});
}
Result HtmlAndJsonWriter::writeUserpicsStart(const Data::UserpicsInfo &data) {
return invoke([&](WriterPtr w) {
return w->writeUserpicsStart(data);
});
}
Result HtmlAndJsonWriter::writeUserpicsSlice(const Data::UserpicsSlice &d) {
return invoke([&](WriterPtr w) {
return w->writeUserpicsSlice(d);
});
}
Result HtmlAndJsonWriter::writeUserpicsEnd() {
return invoke([&](WriterPtr w) {
return w->writeUserpicsEnd();
});
}
Result HtmlAndJsonWriter::writeStoriesStart(const Data::StoriesInfo &data) {
return invoke([&](WriterPtr w) {
return w->writeStoriesStart(data);
});
}
Result HtmlAndJsonWriter::writeStoriesSlice(const Data::StoriesSlice &data) {
return invoke([&](WriterPtr w) {
return w->writeStoriesSlice(data);
});
}
Result HtmlAndJsonWriter::writeStoriesEnd() {
return invoke([&](WriterPtr w) {
return w->writeStoriesEnd();
});
}
Result HtmlAndJsonWriter::writeProfileMusicStart(const Data::ProfileMusicInfo &data) {
return invoke([&](WriterPtr w) {
return w->writeProfileMusicStart(data);
});
}
Result HtmlAndJsonWriter::writeProfileMusicSlice(const Data::ProfileMusicSlice &data) {
return invoke([&](WriterPtr w) {
return w->writeProfileMusicSlice(data);
});
}
Result HtmlAndJsonWriter::writeProfileMusicEnd() {
return invoke([&](WriterPtr w) {
return w->writeProfileMusicEnd();
});
}
Result HtmlAndJsonWriter::writeContactsList(const Data::ContactsList &data) {
return invoke([&](WriterPtr w) {
return w->writeContactsList(data);
});
}
Result HtmlAndJsonWriter::writeSessionsList(const Data::SessionsList &data) {
return invoke([&](WriterPtr w) {
return w->writeSessionsList(data);
});
}
Result HtmlAndJsonWriter::writeOtherData(const Data::File &data) {
return invoke([&](WriterPtr w) {
return w->writeOtherData(data);
});
}
Result HtmlAndJsonWriter::writeDialogsStart(const Data::DialogsInfo &data) {
return invoke([&](WriterPtr w) {
return w->writeDialogsStart(data);
});
}
Result HtmlAndJsonWriter::writeDialogStart(const Data::DialogInfo &data) {
return invoke([&](WriterPtr w) {
return w->writeDialogStart(data);
});
}
Result HtmlAndJsonWriter::writeDialogSlice(const Data::MessagesSlice &data) {
return invoke([&](WriterPtr w) {
return w->writeDialogSlice(data);
});
}
Result HtmlAndJsonWriter::writeDialogEnd() {
return invoke([&](WriterPtr w) {
return w->writeDialogEnd();
});
}
Result HtmlAndJsonWriter::writeDialogsEnd() {
return invoke([&](WriterPtr w) {
return w->writeDialogsEnd();
});
}
Result HtmlAndJsonWriter::finish() {
return invoke([&](WriterPtr w) {
return w->finish();
});
}
QString HtmlAndJsonWriter::mainFilePath() {
return _writers.front()->mainFilePath();
}
HtmlAndJsonWriter::~HtmlAndJsonWriter() = default;
Result HtmlAndJsonWriter::invoke(Fn<Result(WriterPtr)> method) const {
auto result = Result(Result::Type::Success, QString());
for (const auto &writer : _writers) {
const auto current = method(writer);
if (!current) {
result = current;
}
}
return result;
}
} // namespace Export::Output

View File

@@ -0,0 +1,69 @@
/*
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
*/
#pragma once
#include "export/output/export_output_abstract.h"
namespace Export::Output {
class HtmlWriter;
class JsonWriter;
struct Result;
class HtmlAndJsonWriter final : public AbstractWriter {
public:
HtmlAndJsonWriter();
Format format() override;
Result start(
const Settings &settings,
const Environment &environment,
Stats *stats) override;
Result writePersonal(const Data::PersonalInfo &data) override;
Result writeUserpicsStart(const Data::UserpicsInfo &data) override;
Result writeUserpicsSlice(const Data::UserpicsSlice &data) override;
Result writeUserpicsEnd() override;
Result writeStoriesStart(const Data::StoriesInfo &data) override;
Result writeStoriesSlice(const Data::StoriesSlice &data) override;
Result writeStoriesEnd() override;
Result writeProfileMusicStart(const Data::ProfileMusicInfo &data) override;
Result writeProfileMusicSlice(const Data::ProfileMusicSlice &data) override;
Result writeProfileMusicEnd() override;
Result writeContactsList(const Data::ContactsList &data) override;
Result writeSessionsList(const Data::SessionsList &data) override;
Result writeOtherData(const Data::File &data) override;
Result writeDialogsStart(const Data::DialogsInfo &data) override;
Result writeDialogStart(const Data::DialogInfo &data) override;
Result writeDialogSlice(const Data::MessagesSlice &data) override;
Result writeDialogEnd() override;
Result writeDialogsEnd() override;
Result finish() override;
QString mainFilePath() override;
~HtmlAndJsonWriter();
private:
using WriterPtr = const std::unique_ptr<AbstractWriter> &;
Result invoke(Fn<Result(WriterPtr)> method) const;
std::vector<std::unique_ptr<AbstractWriter>> _writers;
};
} // namespace Export::Output

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,114 @@
/*
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
*/
#pragma once
#include "export/output/export_output_abstract.h"
#include "export/output/export_output_file.h"
#include "export/export_settings.h"
#include "export/data/export_data_types.h"
namespace Export {
namespace Output {
namespace details {
struct JsonContext {
using Type = bool;
static constexpr auto kObject = Type(true);
static constexpr auto kArray = Type(false);
// Always fun to use std::vector<bool>.
std::vector<Type> nesting;
};
} // namespace details
class JsonWriter : public AbstractWriter {
public:
Format format() override {
return Format::Json;
}
Result start(
const Settings &settings,
const Environment &environment,
Stats *stats) override;
Result writePersonal(const Data::PersonalInfo &data) override;
Result writeUserpicsStart(const Data::UserpicsInfo &data) override;
Result writeUserpicsSlice(const Data::UserpicsSlice &data) override;
Result writeUserpicsEnd() override;
Result writeStoriesStart(const Data::StoriesInfo &data) override;
Result writeStoriesSlice(const Data::StoriesSlice &data) override;
Result writeStoriesEnd() override;
Result writeProfileMusicStart(const Data::ProfileMusicInfo &data) override;
Result writeProfileMusicSlice(const Data::ProfileMusicSlice &data) override;
Result writeProfileMusicEnd() override;
Result writeContactsList(const Data::ContactsList &data) override;
Result writeSessionsList(const Data::SessionsList &data) override;
Result writeOtherData(const Data::File &data) override;
Result writeDialogsStart(const Data::DialogsInfo &data) override;
Result writeDialogStart(const Data::DialogInfo &data) override;
Result writeDialogSlice(const Data::MessagesSlice &data) override;
Result writeDialogEnd() override;
Result writeDialogsEnd() override;
Result finish() override;
QString mainFilePath() override;
private:
using Context = details::JsonContext;
enum class DialogsMode {
None,
Chats,
Left,
};
[[nodiscard]] QByteArray pushNesting(Context::Type type);
[[nodiscard]] QByteArray prepareObjectItemStart(const QByteArray &key);
[[nodiscard]] QByteArray prepareArrayItemStart();
[[nodiscard]] QByteArray popNesting();
[[nodiscard]] QString mainFileRelativePath() const;
[[nodiscard]] QString pathWithRelativePath(const QString &path) const;
[[nodiscard]] std::unique_ptr<File> fileWithRelativePath(
const QString &path) const;
[[nodiscard]] Result writeSavedContacts(const Data::ContactsList &data);
[[nodiscard]] Result writeFrequentContacts(const Data::ContactsList &data);
[[nodiscard]] Result writeSessions(const Data::SessionsList &data);
[[nodiscard]] Result writeWebSessions(const Data::SessionsList &data);
[[nodiscard]] Result validateDialogsMode(bool isLeftChannel);
[[nodiscard]] Result writeChatsStart(
const QByteArray &listName,
const QByteArray &about);
[[nodiscard]] Result writeChatsEnd();
Settings _settings;
Environment _environment;
Stats *_stats = nullptr;
Context _context;
bool _currentNestingHadItem = false;
DialogsMode _dialogsMode = DialogsMode::None;
std::unique_ptr<File> _output;
};
} // namespace Output
} // namespace Export

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
*/
#pragma once
#include <QtCore/QString>
namespace Export {
namespace Output {
struct Result {
enum class Type : char {
Success,
Error,
FatalError
};
Result(Type type, QString path) : path(path), type(type) {
}
static Result Success() {
return Result(Type::Success, QString());
}
bool isSuccess() const {
return type == Type::Success;
}
bool isError() const {
return (type == Type::Error) || (type == Type::FatalError);
}
bool isFatalError() const {
return (type == Type::FatalError);
}
explicit operator bool() const {
return isSuccess();
}
QString path;
Type type;
};
} // namespace Output
} // namespace Export

View File

@@ -0,0 +1,35 @@
/*
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/output/export_output_stats.h"
namespace Export {
namespace Output {
Stats::Stats(const Stats &other)
: _files(other._files.load())
, _bytes(other._bytes.load()) {
}
void Stats::incrementFiles() {
++_files;
}
void Stats::incrementBytes(int count) {
_bytes += count;
}
int Stats::filesCount() const {
return _files;
}
int64 Stats::bytesCount() const {
return _bytes;
}
} // namespace Output
} // namespace Export

View File

@@ -0,0 +1,33 @@
/*
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
*/
#pragma once
#include <atomic>
namespace Export {
namespace Output {
class Stats {
public:
Stats() = default;
Stats(const Stats &other);
void incrementFiles();
void incrementBytes(int count);
int filesCount() const;
int64 bytesCount() const;
private:
std::atomic<int> _files;
std::atomic<int64> _bytes;
};
} // namespace Output
} // namespace Export

View File

@@ -0,0 +1,104 @@
/*
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
*/
using "ui/basic.style";
using "ui/widgets/widgets.style";
using "boxes/boxes.style";
exportPanelSize: size(364px, 480px);
exportSettingPadding: margins(22px, 8px, 22px, 8px);
exportSubSettingPadding: margins(56px, 4px, 22px, 12px);
exportHeaderLabel: FlatLabel(boxTitle) {
style: TextStyle(defaultTextStyle) {
font: font(15px semibold);
}
}
exportHeaderPadding: margins(22px, 20px, 22px, 9px);
exportFileSizeSlider: MediaSlider(defaultContinuousSlider) {
seekSize: size(15px, 15px);
}
exportFileSizeLabel: LabelSimple(defaultLabelSimple) {
font: boxTextFont;
}
exportFileSizePadding: margins(22px, 8px, 22px, 8px);
exportFileSizeLabelBottom: 18px;
exportLocationLabel: FlatLabel(boxLabel) {
maxHeight: 21px;
}
exportLocationPadding: margins(22px, 8px, 22px, 8px);
exportLimitsPadding: margins(22px, 0px, 22px, 0px);
exportAboutOptionLabel: FlatLabel(defaultFlatLabel) {
textFg: windowSubTextFg;
minWidth: 175px;
}
exportAboutOptionPadding: margins(22px, 0px, 22px, 16px);
exportErrorLabel: FlatLabel(boxLabel) {
minWidth: 175px;
align: align(top);
textFg: boxTextFgError;
}
exportProgressDuration: 200;
exportProgressRowHeight: 30px;
exportProgressRowPadding: margins(22px, 10px, 22px, 10px);
exportProgressRowSkip: 10px;
exportProgressLabel: FlatLabel(boxLabel) {
textFg: windowBoldFg;
maxHeight: 20px;
style: TextStyle(defaultTextStyle) {
font: font(14px semibold);
}
}
exportProgressInfoLabel: FlatLabel(boxLabel) {
textFg: windowSubTextFg;
maxHeight: 20px;
style: boxTextStyle;
}
exportProgressWidth: 3px;
exportProgressFg: mediaPlayerActiveFg;
exportProgressBg: mediaPlayerInactiveFg;
exportCancelButton: RoundButton(attentionBoxButton) {
width: 200px;
height: 44px;
textTop: 12px;
style: TextStyle(semiboldTextStyle) {
font: font(semibold 15px);
}
}
exportCancelBottom: 30px;
exportDoneButton: RoundButton(defaultActiveButton) {
width: 200px;
height: 44px;
textTop: 12px;
style: TextStyle(semiboldTextStyle) {
font: font(semibold 15px);
}
}
exportAboutLabel: FlatLabel(boxLabel) {
textFg: windowSubTextFg;
}
exportAboutPadding: margins(22px, 10px, 22px, 0px);
exportTopBarLabel: FlatLabel(defaultFlatLabel) {
maxHeight: 20px;
palette: TextPalette(defaultTextPalette) {
linkFg: windowSubTextFg;
}
}
exportCalendarSizes: CalendarSizes(defaultCalendarSizes) {
width: 320px;
daysHeight: 40px;
cellSize: size(42px, 38px);
cellInner: 32px;
padding: margins(14px, 0px, 14px, 0px);
}

View File

@@ -0,0 +1,197 @@
/*
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/view/export_view_content.h"
#include "export/export_settings.h"
#include "lang/lang_keys.h"
#include "ui/text/format_values.h"
namespace Export {
namespace View {
const QString Content::kDoneId = "done";
Content ContentFromState(
not_null<Settings*> settings,
const ProcessingState &state) {
using Step = ProcessingState::Step;
auto result = Content();
const auto push = [&](
const QString &id,
const QString &label,
const QString &info,
float64 progress,
uint64 randomId = 0) {
result.rows.push_back({ id, label, info, progress, randomId });
};
const auto pushMain = [&](const QString &label) {
const auto info = (state.entityCount > 0)
? (QString::number(state.entityIndex + 1)
+ " / "
+ QString::number(state.entityCount))
: QString();
if (!state.substepsTotal) {
push("main", label, info, 0.);
return;
}
const auto substepsTotal = state.substepsTotal;
const auto done = state.substepsPassed;
const auto add = state.substepsNow;
const auto doneProgress = done / float64(substepsTotal);
const auto addPart = [&](int index, int count) {
return (count > 0)
? ((float64(add) * index)
/ (float64(substepsTotal) * count))
: 0.;
};
const auto addProgress = (state.entityCount == 1
&& !state.entityIndex)
? addPart(state.itemIndex, state.itemCount)
: addPart(state.entityIndex, state.entityCount);
push("main", label, info, doneProgress + addProgress);
};
const auto pushBytes = [&](
const QString &id,
const QString &label,
uint64 randomId) {
if (!state.bytesCount) {
return;
}
const auto progress = state.bytesLoaded / float64(state.bytesCount);
const auto info = Ui::FormatDownloadText(
state.bytesLoaded,
state.bytesCount);
push(id, label, info, progress, randomId);
};
switch (state.step) {
case Step::Initializing:
pushMain(tr::lng_export_state_initializing(tr::now));
break;
case Step::DialogsList:
pushMain(tr::lng_export_state_chats_list(tr::now));
break;
case Step::PersonalInfo:
pushMain(tr::lng_export_option_info(tr::now));
break;
case Step::Userpics:
pushMain(tr::lng_export_state_userpics(tr::now));
pushBytes(
"userpic" + QString::number(state.entityIndex),
state.bytesName,
state.bytesRandomId);
break;
case Step::Contacts:
pushMain(tr::lng_export_option_contacts(tr::now));
break;
case Step::Stories:
pushMain(tr::lng_export_option_stories(tr::now));
pushBytes(
"story" + QString::number(state.entityIndex),
state.bytesName,
state.bytesRandomId);
break;
case Step::ProfileMusic:
pushMain(tr::lng_export_option_profile_music(tr::now));
pushBytes(
"music" + QString::number(state.entityIndex),
state.bytesName,
state.bytesRandomId);
break;
case Step::Sessions:
pushMain(tr::lng_export_option_sessions(tr::now));
break;
case Step::OtherData:
pushMain(tr::lng_export_option_other(tr::now));
break;
case Step::Dialogs:
if (state.entityCount > 1) {
pushMain(tr::lng_export_state_chats(tr::now));
}
push(
"chat" + QString::number(state.entityIndex),
(state.entityName.isEmpty()
? tr::lng_deleted(tr::now)
: (state.entityType == ProcessingState::EntityType::Chat)
? state.entityName
: (state.entityType == ProcessingState::EntityType::SavedMessages)
? tr::lng_saved_messages(tr::now)
: tr::lng_replies_messages(tr::now)),
(state.itemCount > 0
? (QString::number(state.itemIndex)
+ " / "
+ QString::number(state.itemCount))
: QString()),
(state.itemCount > 0
? (state.itemIndex / float64(state.itemCount))
: 0.));
pushBytes(
("file"
+ QString::number(state.entityIndex)
+ '_'
+ QString::number(state.itemIndex)),
state.bytesName,
state.bytesRandomId);
break;
case Step::Topic:
pushMain(tr::lng_export_state_chats(tr::now));
push(
"topic",
state.entityName.isEmpty()
? tr::lng_deleted(tr::now)
: state.entityName,
(state.itemCount > 0
? (QString::number(state.itemIndex)
+ " / "
+ QString::number(state.itemCount))
: QString()),
(state.itemCount > 0
? (state.itemIndex / float64(state.itemCount))
: 0.));
pushBytes(
"file_topic_" + QString::number(state.itemIndex),
state.bytesName,
state.bytesRandomId);
break;
default: Unexpected("Step in ContentFromState.");
}
const auto requiredRows = settings->onlySinglePeer() ? 2 : 3;
while (result.rows.size() < requiredRows) {
result.rows.emplace_back();
}
return result;
}
Content ContentFromState(const FinishedState &state) {
auto result = Content();
result.rows.push_back({
Content::kDoneId,
tr::lng_export_finished(tr::now),
QString(),
1. });
result.rows.push_back({
Content::kDoneId,
tr::lng_export_total_amount(
tr::now,
lt_amount,
QString::number(state.filesCount)),
QString(),
1. });
result.rows.push_back({
Content::kDoneId,
tr::lng_export_total_size(
tr::now,
lt_size,
Ui::FormatSizeText(state.bytesCount)),
QString(),
1. });
return result;
}
} // namespace View
} // namespace Export

View File

@@ -0,0 +1,57 @@
/*
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
*/
#pragma once
#include "export/export_controller.h"
namespace Export {
struct Settings;
} // namespace Export
namespace Export {
namespace View {
struct Content {
struct Row {
QString id;
QString label;
QString info;
float64 progress = 0.;
uint64 randomId = 0;
};
std::vector<Row> rows;
static const QString kDoneId;
};
[[nodiscard]] Content ContentFromState(
not_null<Settings*> settings,
const ProcessingState &state);
[[nodiscard]] Content ContentFromState(const FinishedState &state);
[[nodiscard]] inline auto ContentFromState(
not_null<Settings*> settings,
rpl::producer<State> state) {
return std::move(
state
) | rpl::filter([](const State &state) {
return v::is<ProcessingState>(state) || v::is<FinishedState>(state);
}) | rpl::map([=](const State &state) {
if (const auto process = std::get_if<ProcessingState>(&state)) {
return ContentFromState(settings, *process);
} else if (const auto done = std::get_if<FinishedState>(&state)) {
return ContentFromState(*done);
}
Unexpected("State type in ContentFromState.");
});
}
} // namespace View
} // namespace Export

View File

@@ -0,0 +1,432 @@
/*
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/view/export_view_panel_controller.h"
#include "export/view/export_view_settings.h"
#include "export/view/export_view_progress.h"
#include "export/export_manager.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/separate_panel.h"
#include "ui/wrap/padding_wrap.h"
#include "mtproto/mtproto_config.h"
#include "ui/boxes/confirm_box.h"
#include "lang/lang_keys.h"
#include "storage/storage_account.h"
#include "core/application.h"
#include "core/file_utilities.h"
#include "main/main_session.h"
#include "data/data_session.h"
#include "base/platform/base_platform_info.h"
#include "base/unixtime.h"
#include "base/qt/qt_common_adapters.h"
#include "boxes/abstract_box.h" // Ui::show().
#include "styles/style_export.h"
#include "styles/style_layers.h"
namespace Export {
namespace View {
namespace {
constexpr auto kSaveSettingsTimeout = crl::time(1000);
class SuggestBox : public Ui::BoxContent {
public:
SuggestBox(QWidget*, not_null<Main::Session*> session);
protected:
void prepare() override;
private:
const not_null<Main::Session*> _session;
};
SuggestBox::SuggestBox(QWidget*, not_null<Main::Session*> session)
: _session(session) {
}
void SuggestBox::prepare() {
setTitle(tr::lng_export_suggest_title());
addButton(tr::lng_box_ok(), [=] {
const auto session = _session;
closeBox();
Core::App().exportManager().start(
session,
session->local().readExportSettings().singlePeer);
});
addButton(tr::lng_export_suggest_cancel(), [=] { closeBox(); });
setCloseByOutsideClick(false);
const auto content = Ui::CreateChild<Ui::FlatLabel>(
this,
tr::lng_export_suggest_text(tr::now),
st::boxLabel);
widthValue(
) | rpl::on_next([=](int width) {
const auto contentWidth = width
- st::boxPadding.left()
- st::boxPadding.right();
content->resizeToWidth(contentWidth);
content->moveToLeft(st::boxPadding.left(), 0);
}, content->lifetime());
content->heightValue(
) | rpl::on_next([=](int height) {
setDimensions(st::boxWidth, height + st::boxPadding.bottom());
}, content->lifetime());
}
} // namespace
Environment PrepareEnvironment(not_null<Main::Session*> session) {
auto result = Environment();
result.internalLinksDomain = session->serverConfig().internalLinksDomain;
result.aboutTelegram = tr::lng_export_about_telegram(tr::now).toUtf8();
result.aboutContacts = tr::lng_export_about_contacts(tr::now).toUtf8();
result.aboutFrequent = tr::lng_export_about_frequent(tr::now).toUtf8();
result.aboutSessions = tr::lng_export_about_sessions(tr::now).toUtf8();
result.aboutWebSessions = tr::lng_export_about_web_sessions(tr::now).toUtf8();
result.aboutChats = tr::lng_export_about_chats(tr::now).toUtf8();
result.aboutLeftChats = tr::lng_export_about_left_chats(tr::now).toUtf8();
return result;
}
base::weak_qptr<Ui::BoxContent> SuggestStart(not_null<Main::Session*> session) {
ClearSuggestStart(session);
return Ui::show(
Box<SuggestBox>(session),
Ui::LayerOption::KeepOther).get();
}
void ClearSuggestStart(not_null<Main::Session*> session) {
session->data().clearExportSuggestion();
auto settings = session->local().readExportSettings();
if (settings.availableAt) {
settings.availableAt = 0;
session->local().writeExportSettings(settings);
}
}
bool IsDefaultPath(not_null<Main::Session*> session, const QString &path) {
const auto check = [](const QString &value) {
const auto result = value.endsWith('/')
? value.mid(0, value.size() - 1)
: value;
return Platform::IsWindows() ? result.toLower() : result;
};
return (check(path) == check(File::DefaultDownloadPath(session)));
}
void ResolveSettings(not_null<Main::Session*> session, Settings &settings) {
if (settings.path.isEmpty()) {
settings.path = File::DefaultDownloadPath(session);
settings.forceSubPath = true;
} else {
settings.forceSubPath = IsDefaultPath(session, settings.path);
}
if (!settings.onlySinglePeer()) {
settings.singlePeerFrom = settings.singlePeerTill = 0;
}
}
PanelController::PanelController(
not_null<Main::Session*> session,
not_null<Controller*> process)
: _session(session)
, _process(process)
, _settings(
std::make_unique<Settings>(_session->local().readExportSettings()))
, _saveSettingsTimer([=] { saveSettings(); }) {
ResolveSettings(session, *_settings);
_process->state(
) | rpl::on_next([=](State &&state) {
updateState(std::move(state));
}, _lifetime);
}
PanelController::~PanelController() {
if (_saveSettingsTimer.isActive()) {
saveSettings();
}
if (_panel) {
_panel->hideLayer(anim::type::instant);
}
}
void PanelController::activatePanel() {
if (_panel) {
_panel->showAndActivate();
}
}
void PanelController::createPanel() {
const auto singlePeer = _settings->onlySinglePeer();
const auto singleTopic = _settings->onlySingleTopic();
_panel = base::make_unique_q<Ui::SeparatePanel>(Ui::SeparatePanelArgs{
.onAllSpaces = true,
});
_panel->setTitle((singleTopic
? tr::lng_export_header_topic
: singlePeer
? tr::lng_export_header_chats
: tr::lng_export_title)());
_panel->setInnerSize(st::exportPanelSize);
_panel->closeRequests(
) | rpl::on_next([=] {
LOG(("Export Info: Panel Hide By Close."));
_panel->hideGetDuration();
}, _panel->lifetime());
_panelCloseEvents.fire(_panel->closeEvents());
showSettings();
}
void PanelController::showSettings() {
auto settings = base::make_unique_q<SettingsWidget>(
_panel,
_session,
*_settings);
settings->setShowBoxCallback([=](object_ptr<Ui::BoxContent> box) {
_panel->showBox(
std::move(box),
Ui::LayerOption::KeepOther,
anim::type::normal);
});
settings->startClicks(
) | rpl::on_next([=]() {
showProgress();
_process->startExport(*_settings, PrepareEnvironment(_session));
}, settings->lifetime());
settings->cancelClicks(
) | rpl::on_next([=] {
LOG(("Export Info: Panel Hide By Cancel."));
_panel->hideGetDuration();
}, settings->lifetime());
settings->changes(
) | rpl::on_next([=](Settings &&settings) {
*_settings = std::move(settings);
}, settings->lifetime());
_panel->showInner(std::move(settings));
}
void PanelController::showError(const ApiErrorState &error) {
LOG(("Export Info: API Error '%1'.").arg(error.data.type()));
if (error.data.type() == u"TAKEOUT_INVALID"_q) {
showError(tr::lng_export_invalid(tr::now));
} else if (error.data.type().startsWith(u"TAKEOUT_INIT_DELAY_"_q)) {
const auto seconds = std::max(base::StringViewMid(
error.data.type(),
u"TAKEOUT_INIT_DELAY_"_q.size()).toInt(), 1);
const auto now = QDateTime::currentDateTime();
const auto when = now.addSecs(seconds);
const auto hours = seconds / 3600;
const auto hoursText = [&] {
if (hours <= 0) {
return tr::lng_export_delay_less_than_hour(tr::now);
}
return tr::lng_hours(tr::now, lt_count, hours);
}();
showError(tr::lng_export_delay(
tr::now,
lt_hours,
hoursText,
lt_date,
langDateTimeFull(when)));
_settings->availableAt = base::unixtime::now() + seconds;
_saveSettingsTimer.callOnce(kSaveSettingsTimeout);
_session->data().suggestStartExport(_settings->availableAt);
} else {
showCriticalError("API Error happened :(\n"
+ QString::number(error.data.code()) + ": " + error.data.type()
+ "\n" + error.data.description());
}
}
void PanelController::showError(const OutputErrorState &error) {
showCriticalError("Disk Error happened :(\n"
"Could not write path:\n" + error.path);
}
void PanelController::showCriticalError(const QString &text) {
auto container = base::make_unique_q<Ui::PaddingWrap<Ui::FlatLabel>>(
_panel.get(),
object_ptr<Ui::FlatLabel>(
_panel.get(),
text,
st::exportErrorLabel),
style::margins(0, st::exportPanelSize.height() / 4, 0, 0));
container->widthValue(
) | rpl::on_next([label = container->entity()](int width) {
label->resize(width, label->height());
}, container->lifetime());
_panel->showInner(std::move(container));
_panel->setHideOnDeactivate(false);
}
void PanelController::showError(const QString &text) {
auto box = Ui::MakeInformBox(text);
const auto weak = base::make_weak(box.data());
const auto hidden = _panel->isHidden();
_panel->showBox(
std::move(box),
Ui::LayerOption::CloseOther,
hidden ? anim::type::instant : anim::type::normal);
weak->setCloseByEscape(false);
weak->setCloseByOutsideClick(false);
weak->boxClosing(
) | rpl::on_next([=] {
LOG(("Export Info: Panel Hide By Error: %1.").arg(text));
_panel->hideGetDuration();
}, weak->lifetime());
if (hidden) {
_panel->showAndActivate();
}
_panel->setHideOnDeactivate(false);
}
void PanelController::showProgress() {
_settings->availableAt = 0;
ClearSuggestStart(_session);
_panel->setTitle(tr::lng_export_progress_title());
auto progress = base::make_unique_q<ProgressWidget>(
_panel.get(),
rpl::single(
ContentFromState(_settings.get(), ProcessingState())
) | rpl::then(progressState()));
progress->skipFileClicks(
) | rpl::on_next([=](uint64 randomId) {
_process->skipFile(randomId);
}, progress->lifetime());
progress->cancelClicks(
) | rpl::on_next([=] {
stopWithConfirmation();
}, progress->lifetime());
progress->doneClicks(
) | rpl::on_next([=] {
if (const auto finished = std::get_if<FinishedState>(&_state)) {
File::ShowInFolder(finished->path);
LOG(("Export Info: Panel Hide By Done: %1."
).arg(finished->path));
_panel->hideGetDuration();
}
}, progress->lifetime());
_panel->showInner(std::move(progress));
_panel->setHideOnDeactivate(true);
}
void PanelController::stopWithConfirmation(Fn<void()> callback) {
if (!v::is<ProcessingState>(_state)) {
LOG(("Export Info: Stop Panel Without Confirmation."));
stopExport();
if (callback) {
callback();
}
return;
}
auto stop = [=, callback = std::move(callback)]() mutable {
if (auto saved = std::move(callback)) {
LOG(("Export Info: Stop Panel With Confirmation."));
stopExport();
saved();
} else {
_process->cancelExportFast();
}
};
const auto hidden = _panel->isHidden();
const auto old = _confirmStopBox;
auto box = Ui::MakeConfirmBox({
.text = tr::lng_export_sure_stop(),
.confirmed = std::move(stop),
.confirmText = tr::lng_export_stop(),
.confirmStyle = &st::attentionBoxButton,
});
_confirmStopBox = box.data();
_panel->showBox(
std::move(box),
Ui::LayerOption::CloseOther,
hidden ? anim::type::instant : anim::type::normal);
if (hidden) {
_panel->showAndActivate();
}
if (old) {
old->closeBox();
}
}
void PanelController::stopExport() {
_stopRequested = true;
_panel->showAndActivate();
LOG(("Export Info: Panel Hide By Stop"));
_panel->hideGetDuration();
}
rpl::producer<> PanelController::stopRequests() const {
return _panelCloseEvents.events(
) | rpl::flatten_latest(
) | rpl::filter([=] {
return !v::is<ProcessingState>(_state) || _stopRequested;
});
}
void PanelController::fillParams(const PasswordCheckState &state) {
_settings->singlePeer = state.singlePeer;
}
void PanelController::updateState(State &&state) {
if (const auto start = std::get_if<PasswordCheckState>(&state)) {
fillParams(*start);
}
if (!_panel) {
createPanel();
}
_state = std::move(state);
if (const auto apiError = std::get_if<ApiErrorState>(&_state)) {
showError(*apiError);
} else if (const auto error = std::get_if<OutputErrorState>(&_state)) {
showError(*error);
} else if (v::is<FinishedState>(_state)) {
_panel->setTitle(tr::lng_export_title());
_panel->setHideOnDeactivate(false);
} else if (v::is<CancelledState>(_state)) {
LOG(("Export Info: Stop Panel After Cancel."));
stopExport();
}
}
void PanelController::saveSettings() const {
const auto check = [](const QString &value) {
const auto result = value.endsWith('/')
? value.mid(0, value.size() - 1)
: value;
return Platform::IsWindows() ? result.toLower() : result;
};
auto settings = *_settings;
if (check(settings.path) == check(File::DefaultDownloadPath(_session))) {
settings.path = QString();
}
_session->local().writeExportSettings(settings);
}
} // namespace View
} // namespace Export

View File

@@ -0,0 +1,90 @@
/*
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
*/
#pragma once
#include "export/export_controller.h"
#include "export/view/export_view_content.h"
#include "base/unique_qptr.h"
#include "base/timer.h"
namespace Ui {
class SeparatePanel;
class BoxContent;
} // namespace Ui
namespace Main {
class Session;
} // namespace Main
namespace Export {
namespace View {
base::weak_qptr<Ui::BoxContent> SuggestStart(not_null<Main::Session*> session);
void ClearSuggestStart(not_null<Main::Session*> session);
bool IsDefaultPath(not_null<Main::Session*> session, const QString &path);
void ResolveSettings(not_null<Main::Session*> session, Settings &settings);
class Panel;
class PanelController {
public:
PanelController(
not_null<Main::Session*> session,
not_null<Controller*> process);
~PanelController();
[[nodiscard]] Main::Session &session() const {
return *_session;
}
void activatePanel();
void stopWithConfirmation(Fn<void()> callback = nullptr);
[[nodiscard]] rpl::producer<> stopRequests() const;
[[nodiscard]] rpl::lifetime &lifetime() {
return _lifetime;
}
auto progressState() const {
return ContentFromState(
_settings.get(),
rpl::single(_state) | rpl::then(_process->state()));
}
private:
void fillParams(const PasswordCheckState &state);
void stopExport();
void createPanel();
void updateState(State &&state);
void showSettings();
void showProgress();
void showError(const ApiErrorState &error);
void showError(const OutputErrorState &error);
void showError(const QString &text);
void showCriticalError(const QString &text);
void saveSettings() const;
const not_null<Main::Session*> _session;
const not_null<Controller*> _process;
std::unique_ptr<Settings> _settings;
base::Timer _saveSettingsTimer;
base::unique_qptr<Ui::SeparatePanel> _panel;
State _state;
base::weak_qptr<Ui::BoxContent> _confirmStopBox;
rpl::event_stream<rpl::producer<>> _panelCloseEvents;
bool _stopRequested = false;
rpl::lifetime _lifetime;
};
} // namespace View
} // namespace Export

View File

@@ -0,0 +1,382 @@
/*
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/view/export_view_progress.h"
#include "ui/effects/animations.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/buttons.h"
#include "ui/wrap/fade_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "lang/lang_keys.h"
#include "styles/style_boxes.h"
#include "styles/style_export.h"
namespace Export {
namespace View {
namespace {
constexpr auto kShowSkipFileTimeout = 5 * crl::time(1000);
} // namespace
class ProgressWidget::Row : public Ui::RpWidget {
public:
Row(QWidget *parent, Content::Row &&data);
void updateData(Content::Row &&data);
protected:
int resizeGetHeight(int newWidth) override;
void paintEvent(QPaintEvent *e) override;
private:
struct Instance {
base::unique_qptr<Ui::FadeWrap<Ui::FlatLabel>> label;
base::unique_qptr<Ui::FadeWrap<Ui::FlatLabel>> info;
float64 value = 0.;
Ui::Animations::Simple progress;
bool hiding = true;
Ui::Animations::Simple opacity;
};
void fillCurrentInstance();
void hideCurrentInstance();
void setInstanceProgress(Instance &instance, float64 progress);
void toggleInstance(Instance &data, bool shown);
void instanceOpacityCallback(base::weak_qptr<Ui::FlatLabel> label);
void removeOldInstance(base::weak_qptr<Ui::FlatLabel> label);
void paintInstance(QPainter &p, const Instance &data);
void updateControlsGeometry(int newWidth);
void updateInstanceGeometry(const Instance &instance, int newWidth);
Content::Row _data;
Instance _current;
std::vector<Instance> _old;
};
ProgressWidget::Row::Row(QWidget *parent, Content::Row &&data)
: RpWidget(parent)
, _data(std::move(data)) {
fillCurrentInstance();
}
void ProgressWidget::Row::updateData(Content::Row &&data) {
const auto wasId = _data.id;
const auto nowId = data.id;
_data = std::move(data);
if (nowId.isEmpty()) {
hideCurrentInstance();
} else if (wasId.isEmpty()) {
fillCurrentInstance();
} else {
_current.label->entity()->setText(_data.label);
_current.info->entity()->setText(_data.info);
setInstanceProgress(_current, _data.progress);
if (nowId != wasId) {
_current.progress.stop();
}
}
updateControlsGeometry(width());
update();
}
void ProgressWidget::Row::fillCurrentInstance() {
_current.label = base::make_unique_q<Ui::FadeWrap<Ui::FlatLabel>>(
this,
object_ptr<Ui::FlatLabel>(
this,
_data.label,
st::exportProgressLabel));
_current.info = base::make_unique_q<Ui::FadeWrap<Ui::FlatLabel>>(
this,
object_ptr<Ui::FlatLabel>(
this,
_data.info,
st::exportProgressInfoLabel));
_current.label->hide(anim::type::instant);
_current.info->hide(anim::type::instant);
setInstanceProgress(_current, _data.progress);
toggleInstance(_current, true);
if (_data.id == "main") {
_current.opacity.stop();
_current.label->finishAnimating();
_current.info->finishAnimating();
}
}
void ProgressWidget::Row::hideCurrentInstance() {
if (!_current.label) {
return;
}
setInstanceProgress(_current, 1.);
toggleInstance(_current, false);
_old.push_back(std::move(_current));
}
void ProgressWidget::Row::setInstanceProgress(
Instance &instance,
float64 progress) {
if (_current.value < progress) {
_current.progress.start(
[=] { update(); },
_current.value,
progress,
st::exportProgressDuration,
anim::sineInOut);
} else if (_current.value > progress) {
_current.progress.stop();
}
_current.value = progress;
}
void ProgressWidget::Row::toggleInstance(Instance &instance, bool shown) {
Expects(instance.label != nullptr);
if (instance.hiding != shown) {
return;
}
const auto label = base::make_weak(instance.label->entity());
instance.opacity.start(
[=] { instanceOpacityCallback(label); },
shown ? 0. : 1.,
shown ? 1. : 0.,
st::exportProgressDuration);
instance.hiding = !shown;
_current.label->toggle(shown, anim::type::normal);
_current.info->toggle(shown, anim::type::normal);
}
void ProgressWidget::Row::instanceOpacityCallback(
base::weak_qptr<Ui::FlatLabel> label) {
update();
const auto i = ranges::find(_old, label, [](const Instance &instance) {
return base::make_weak(instance.label->entity());
});
if (i != end(_old) && i->hiding && !i->opacity.animating()) {
crl::on_main(this, [=] {
removeOldInstance(label);
});
}
}
void ProgressWidget::Row::removeOldInstance(
base::weak_qptr<Ui::FlatLabel> label) {
const auto i = ranges::find(_old, label, [](const Instance &instance) {
return base::make_weak(instance.label->entity());
});
if (i != end(_old)) {
_old.erase(i);
}
}
int ProgressWidget::Row::resizeGetHeight(int newWidth) {
updateControlsGeometry(newWidth);
return st::exportProgressRowHeight;
}
void ProgressWidget::Row::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
const auto thickness = st::exportProgressWidth;
const auto top = height() - thickness;
p.fillRect(0, top, width(), thickness, st::shadowFg);
for (const auto &instance : _old) {
paintInstance(p, instance);
}
paintInstance(p, _current);
}
void ProgressWidget::Row::paintInstance(QPainter &p, const Instance &data) {
const auto opacity = data.opacity.value(data.hiding ? 0. : 1.);
if (!opacity) {
return;
}
p.setOpacity(opacity);
const auto thickness = st::exportProgressWidth;
const auto top = height() - thickness;
const auto till = qRound(data.progress.value(data.value) * width());
if (till > 0) {
p.fillRect(0, top, till, thickness, st::exportProgressFg);
}
if (till < width()) {
const auto left = width() - till;
p.fillRect(till, top, left, thickness, st::exportProgressBg);
}
}
void ProgressWidget::Row::updateControlsGeometry(int newWidth) {
updateInstanceGeometry(_current, newWidth);
for (const auto &instance : _old) {
updateInstanceGeometry(instance, newWidth);
}
}
void ProgressWidget::Row::updateInstanceGeometry(
const Instance &instance,
int newWidth) {
if (!instance.label) {
return;
}
instance.info->resizeToNaturalWidth(newWidth);
instance.label->resizeToWidth(newWidth - instance.info->width());
instance.info->moveToRight(0, 0, newWidth);
instance.label->moveToLeft(0, 0, newWidth);
}
ProgressWidget::ProgressWidget(
QWidget *parent,
rpl::producer<Content> content)
: RpWidget(parent)
, _body(this)
, _fileShowSkipTimer([=] { _skipFile->show(anim::type::normal); }) {
widthValue(
) | rpl::on_next([=](int width) {
_body->resizeToWidth(width);
_body->moveToLeft(0, 0);
}, _body->lifetime());
auto skipFileWrap = _body->add(object_ptr<Ui::FixedHeightWidget>(
_body.data(),
st::defaultLinkButton.font->height + st::exportProgressRowSkip));
_skipFile = base::make_unique_q<Ui::FadeWrap<Ui::LinkButton>>(
skipFileWrap,
object_ptr<Ui::LinkButton>(
this,
tr::lng_export_skip_file(tr::now),
st::defaultLinkButton));
_skipFile->hide(anim::type::instant);
_skipFile->moveToLeft(st::exportProgressRowPadding.left(), 0);
_about = _body->add(
object_ptr<Ui::FlatLabel>(
this,
tr::lng_export_progress(tr::now),
st::exportAboutLabel),
st::exportAboutPadding);
std::move(
content
) | rpl::on_next([=](Content &&content) {
updateState(std::move(content));
}, lifetime());
_cancel = base::make_unique_q<Ui::RoundButton>(
this,
tr::lng_export_stop(),
st::exportCancelButton);
setupBottomButton(_cancel.get());
}
rpl::producer<uint64> ProgressWidget::skipFileClicks() const {
return _skipFile->entity()->clicks(
) | rpl::map([=] { return _fileRandomId; });
}
rpl::producer<> ProgressWidget::cancelClicks() const {
return _cancel
? (_cancel->clicks() | rpl::to_empty)
: (rpl::never<>() | rpl::type_erased);
}
rpl::producer<> ProgressWidget::doneClicks() const {
return _doneClicks.events();
}
void ProgressWidget::setupBottomButton(not_null<Ui::RoundButton*> button) {
button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
button->show();
sizeValue(
) | rpl::on_next([=](QSize size) {
button->move(
(size.width() - button->width()) / 2,
(size.height() - st::exportCancelBottom - button->height()));
}, button->lifetime());
}
void ProgressWidget::updateState(Content &&content) {
if (!content.rows.empty() && content.rows[0].id == Content::kDoneId) {
showDone();
}
const auto wasCount = _rows.size();
auto index = 0;
for (auto &row : content.rows) {
if (index < _rows.size()) {
_rows[index]->updateData(std::move(row));
} else {
if (index > 0) {
_body->insert(
index * 2 - 1,
object_ptr<Ui::FixedHeightWidget>(
this,
st::exportProgressRowSkip));
}
_rows.push_back(_body->insert(
index * 2,
object_ptr<Row>(this, std::move(row)),
st::exportProgressRowPadding));
_rows.back()->show();
}
++index;
}
const auto fileRandomId = !content.rows.empty()
? content.rows.back().randomId
: uint64(0);
if (_fileRandomId != fileRandomId) {
_fileShowSkipTimer.cancel();
_skipFile->hide(anim::type::normal);
_fileRandomId = fileRandomId;
if (_fileRandomId) {
_fileShowSkipTimer.callOnce(kShowSkipFileTimeout);
}
}
for (const auto count = _rows.size(); index != count; ++index) {
_rows[index]->updateData(Content::Row());
}
if (_rows.size() != wasCount) {
_body->resizeToWidth(width());
}
}
void ProgressWidget::showDone() {
_cancel = nullptr;
_skipFile->hide(anim::type::instant);
_fileShowSkipTimer.cancel();
_about->setText(tr::lng_export_about_done(tr::now));
_done = base::make_unique_q<Ui::RoundButton>(
this,
tr::lng_export_done(),
st::exportDoneButton);
const auto desired = std::min(
st::exportDoneButton.style.font->width(tr::lng_export_done(tr::now))
+ st::exportDoneButton.height
- st::exportDoneButton.style.font->height,
st::exportPanelSize.width() - 2 * st::exportCancelBottom);
if (_done->width() < desired) {
_done->setFullWidth(desired);
}
_done->clicks(
) | rpl::to_empty
| rpl::start_to_stream(_doneClicks, _done->lifetime());
setupBottomButton(_done.get());
}
ProgressWidget::~ProgressWidget() = default;
} // namespace View
} // namespace Export

View File

@@ -0,0 +1,62 @@
/*
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
*/
#pragma once
#include "ui/rp_widget.h"
#include "export/view/export_view_content.h"
#include "base/object_ptr.h"
#include "base/timer.h"
namespace Ui {
class VerticalLayout;
class RoundButton;
class FlatLabel;
class LinkButton;
template <typename Widget>
class FadeWrap;
} // namespace Ui
namespace Export {
namespace View {
class ProgressWidget : public Ui::RpWidget {
public:
ProgressWidget(
QWidget *parent,
rpl::producer<Content> content);
rpl::producer<uint64> skipFileClicks() const;
rpl::producer<> cancelClicks() const;
rpl::producer<> doneClicks() const;
~ProgressWidget();
private:
void setupBottomButton(not_null<Ui::RoundButton*> button);
void updateState(Content &&content);
void showDone();
Content _content;
class Row;
object_ptr<Ui::VerticalLayout> _body;
std::vector<not_null<Row*>> _rows;
base::unique_qptr<Ui::FadeWrap<Ui::LinkButton>> _skipFile;
QPointer<Ui::FlatLabel> _about;
base::unique_qptr<Ui::RoundButton> _cancel;
base::unique_qptr<Ui::RoundButton> _done;
rpl::event_stream<> _doneClicks;
uint64 _fileRandomId = 0;
base::Timer _fileShowSkipTimer;
};
} // namespace View
} // namespace Export

View File

@@ -0,0 +1,959 @@
/*
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/view/export_view_settings.h"
#include "export/output/export_output_abstract.h"
#include "export/view/export_view_panel_controller.h"
#include "lang/lang_keys.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/scroll_area.h"
#include "ui/widgets/continuous_sliders.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/fade_wrap.h"
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/boxes/calendar_box.h"
#include "ui/boxes/choose_time.h"
#include "platform/platform_specific.h"
#include "core/application.h"
#include "core/file_utilities.h"
#include "base/unixtime.h"
#include "main/main_session.h"
#include "styles/style_widgets.h"
#include "styles/style_export.h"
#include "styles/style_layers.h"
namespace Export {
namespace View {
namespace {
constexpr auto kMegabyte = int64(1024) * 1024;
[[nodiscard]] PeerId ReadPeerId(
not_null<Main::Session*> session,
const MTPInputPeer &data) {
return data.match([](const MTPDinputPeerUser &data) {
return peerFromUser(data.vuser_id().v);
}, [](const MTPDinputPeerUserFromMessage &data) {
return peerFromUser(data.vuser_id().v);
}, [](const MTPDinputPeerChat &data) {
return peerFromChat(data.vchat_id().v);
}, [](const MTPDinputPeerChannel &data) {
return peerFromChannel(data.vchannel_id().v);
}, [](const MTPDinputPeerChannelFromMessage &data) {
return peerFromChannel(data.vchannel_id().v);
}, [&](const MTPDinputPeerSelf &data) {
return session->userPeerId();
}, [](const MTPDinputPeerEmpty &data) {
return PeerId(0);
});
}
void ChooseFormatBox(
not_null<Ui::GenericBox*> box,
Output::Format format,
Fn<void(Output::Format)> done) {
using Format = Output::Format;
const auto group = std::make_shared<Ui::RadioenumGroup<Format>>(format);
const auto addFormatOption = [&](QString label, Format format) {
box->addRow(
object_ptr<Ui::Radioenum<Format>>(
box,
group,
format,
label,
st::defaultBoxCheckbox),
st::exportSettingPadding);
};
box->setTitle(tr::lng_export_option_choose_format());
addFormatOption(tr::lng_export_option_html(tr::now), Format::Html);
addFormatOption(tr::lng_export_option_json(tr::now), Format::Json);
addFormatOption(
tr::lng_export_option_html_and_json(tr::now),
Format::HtmlAndJson);
box->addButton(tr::lng_settings_save(), [=] { done(group->current()); });
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
} // namespace
int64 SizeLimitByIndex(int index) {
Expects(index >= 0 && index < kSizeValueCount);
index += 1;
const auto megabytes = [&] {
if (index <= 10) {
return index;
} else if (index <= 30) {
return 10 + (index - 10) * 2;
} else if (index <= 40) {
return 50 + (index - 30) * 5;
} else if (index <= 60) {
return 100 + (index - 40) * 10;
} else if (index <= 70) {
return 300 + (index - 60) * 20;
} else if (index <= 80) {
return 500 + (index - 70) * 50;
} else if (index <= 90) {
return 1000 + (index - 80) * 100;
} else {
return 2000 + (index - 90) * 200;
}
}();
return megabytes * kMegabyte;
}
SettingsWidget::SettingsWidget(
QWidget *parent,
not_null<Main::Session*> session,
Settings data)
: RpWidget(parent)
, _session(session)
, _singlePeerId(ReadPeerId(session, data.singlePeer))
, _internal_data(std::move(data)) {
ResolveSettings(session, _internal_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,
st::boxScroll);
const auto wrap = scroll->setOwnedWidget(
object_ptr<Ui::OverrideMargins>(
scroll,
object_ptr<Ui::VerticalLayout>(scroll)));
const auto content = static_cast<Ui::VerticalLayout*>(wrap->entity());
const auto buttons = setupButtons(scroll, wrap);
setupOptions(content);
setupPathAndFormat(content);
sizeValue(
) | rpl::on_next([=](QSize size) {
scroll->resize(size.width(), size.height() - buttons->height());
wrap->resizeToWidth(size.width());
content->resizeToWidth(size.width());
}, lifetime());
}
void SettingsWidget::setupOptions(not_null<Ui::VerticalLayout*> container) {
if (!_singlePeerId) {
setupFullExportOptions(container);
}
setupMediaOptions(container);
if (!_singlePeerId) {
setupOtherOptions(container);
}
}
void SettingsWidget::setupFullExportOptions(
not_null<Ui::VerticalLayout*> container) {
addOptionWithAbout(
container,
tr::lng_export_option_info(tr::now),
Type::PersonalInfo | Type::Userpics,
tr::lng_export_option_info_about(tr::now));
addOptionWithAbout(
container,
tr::lng_export_option_contacts(tr::now),
Type::Contacts,
tr::lng_export_option_contacts_about(tr::now));
addOptionWithAbout(
container,
tr::lng_export_option_stories(tr::now),
Type::Stories,
tr::lng_export_option_stories_about(tr::now));
addOptionWithAbout(
container,
tr::lng_export_option_profile_music(tr::now),
Type::ProfileMusic,
tr::lng_export_option_profile_music_about(tr::now));
addHeader(container, tr::lng_export_header_chats(tr::now));
addOption(
container,
tr::lng_export_option_personal_chats(tr::now),
Type::PersonalChats);
addOption(
container,
tr::lng_export_option_bot_chats(tr::now),
Type::BotChats);
addChatOption(
container,
tr::lng_export_option_private_groups(tr::now),
Type::PrivateGroups);
addChatOption(
container,
tr::lng_export_option_private_channels(tr::now),
Type::PrivateChannels);
addChatOption(
container,
tr::lng_export_option_public_groups(tr::now),
Type::PublicGroups);
addChatOption(
container,
tr::lng_export_option_public_channels(tr::now),
Type::PublicChannels);
}
void SettingsWidget::setupMediaOptions(
not_null<Ui::VerticalLayout*> container) {
if (_singlePeerId != 0) {
addMediaOptions(container);
return;
}
const auto mediaWrap = container->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(container)));
const auto media = mediaWrap->entity();
addHeader(media, tr::lng_export_header_media(tr::now));
addMediaOptions(media);
value() | rpl::map([](const Settings &data) {
return data.types;
}) | rpl::distinct_until_changed(
) | rpl::on_next([=](Settings::Types types) {
mediaWrap->toggle((types & (Type::PersonalChats
| Type::BotChats
| Type::PrivateGroups
| Type::PrivateChannels
| Type::PublicGroups
| Type::PublicChannels
| Type::ProfileMusic)) != 0, anim::type::normal);
}, mediaWrap->lifetime());
widthValue(
) | rpl::on_next([=](int width) {
mediaWrap->resizeToWidth(width);
}, mediaWrap->lifetime());
}
void SettingsWidget::setupOtherOptions(
not_null<Ui::VerticalLayout*> container) {
addHeader(container, tr::lng_export_header_other(tr::now));
addOptionWithAbout(
container,
tr::lng_export_option_sessions(tr::now),
Type::Sessions,
tr::lng_export_option_sessions_about(tr::now));
addOptionWithAbout(
container,
tr::lng_export_option_other(tr::now),
Type::OtherData,
tr::lng_export_option_other_about(tr::now));
}
void SettingsWidget::setupPathAndFormat(
not_null<Ui::VerticalLayout*> container) {
if (_singlePeerId != 0) {
addFormatAndLocationLabel(container);
addLimitsLabel(container);
return;
}
const auto formatGroup = std::make_shared<Ui::RadioenumGroup<Format>>(
readData().format);
formatGroup->setChangedCallback([=](Format format) {
changeData([&](Settings &data) {
data.format = format;
});
});
const auto addFormatOption = [&](QString label, Format format) {
container->add(
object_ptr<Ui::Radioenum<Format>>(
container,
formatGroup,
format,
label,
st::defaultBoxCheckbox),
st::exportSettingPadding);
};
addHeader(container, tr::lng_export_header_format(tr::now));
addLocationLabel(container);
addFormatOption(tr::lng_export_option_html(tr::now), Format::Html);
addFormatOption(tr::lng_export_option_json(tr::now), Format::Json);
addFormatOption(tr::lng_export_option_html_and_json(tr::now), Format::HtmlAndJson);
}
void SettingsWidget::addLocationLabel(
not_null<Ui::VerticalLayout*> container) {
#ifndef OS_MAC_STORE
auto pathLink = value() | rpl::map([](const Settings &data) {
return data.path;
}) | rpl::distinct_until_changed(
) | rpl::map([=](const QString &path) {
const auto text = IsDefaultPath(_session, path)
? Core::App().canReadDefaultDownloadPath()
? u"Downloads/"_q + File::DefaultDownloadPathFolder(_session)
: tr::lng_download_path_temp(tr::now)
: path;
return tr::link(
QDir::toNativeSeparators(text),
QString("internal:edit_export_path"));
});
const auto label = container->add(
object_ptr<Ui::FlatLabel>(
container,
tr::lng_export_option_location(
lt_path,
std::move(pathLink),
tr::marked),
st::exportLocationLabel),
st::exportLocationPadding);
label->overrideLinkClickHandler([=] {
chooseFolder();
});
#endif // OS_MAC_STORE
}
void SettingsWidget::chooseFormat() {
const auto shared = std::make_shared<base::weak_qptr<Ui::GenericBox>>();
const auto callback = [=](Format format) {
changeData([&](Settings &data) {
data.format = format;
});
if (const auto strong = shared->get()) {
strong->closeBox();
}
};
auto box = Box(
ChooseFormatBox,
readData().format,
callback);
*shared = base::make_weak(box.data());
_showBoxCallback(std::move(box));
}
void SettingsWidget::addFormatAndLocationLabel(
not_null<Ui::VerticalLayout*> container) {
#ifndef OS_MAC_STORE
auto pathLink = value() | rpl::map([](const Settings &data) {
return data.path;
}) | rpl::distinct_until_changed(
) | rpl::map([=](const QString &path) {
const auto text = IsDefaultPath(_session, path)
? Core::App().canReadDefaultDownloadPath()
? u"Downloads/"_q + File::DefaultDownloadPathFolder(_session)
: tr::lng_download_path_temp(tr::now)
: path;
return tr::link(
QDir::toNativeSeparators(text),
u"internal:edit_export_path"_q);
});
auto formatLink = value() | rpl::map([](const Settings &data) {
return data.format;
}) | rpl::distinct_until_changed(
) | rpl::map([](Format format) {
const auto text = (format == Format::Html)
? "HTML"
: (format == Format::Json)
? "JSON"
: tr::lng_export_option_html_and_json(tr::now);
return tr::link(text, u"internal:edit_format"_q);
});
const auto label = container->add(
object_ptr<Ui::FlatLabel>(
container,
tr::lng_export_option_format_location(
lt_format,
std::move(formatLink),
lt_path,
std::move(pathLink),
tr::marked),
st::exportLocationLabel),
st::exportLocationPadding);
label->overrideLinkClickHandler([=](const QString &url) {
if (url == u"internal:edit_export_path"_q) {
chooseFolder();
} else if (url == u"internal:edit_format"_q) {
chooseFormat();
} else {
Unexpected("Click handler URL in export limits edit.");
}
});
#endif // OS_MAC_STORE
}
void SettingsWidget::addLimitsLabel(
not_null<Ui::VerticalLayout*> container) {
auto fromDateLink = value() | rpl::map([](const Settings &data) {
return data.singlePeerFrom;
}) | rpl::distinct_until_changed(
) | rpl::map([](TimeId from) {
return (from
? rpl::single(langDayOfMonthFull(
base::unixtime::parse(from).date()))
: tr::lng_export_beginning()
) | rpl::map(tr::url(u"internal:edit_from"_q));
}) | rpl::flatten_latest();
const auto mapToTime = [](TimeId id, const QString &link) {
return rpl::single(id
? QLocale().toString(
base::unixtime::parse(id).time(),
QLocale::ShortFormat)
: QString()
) | rpl::map(tr::url(link));
};
const auto concat = [](TextWithEntities date, TextWithEntities link) {
return link.text.isEmpty()
? date
: date.append(u", "_q).append(std::move(link));
};
auto fromTimeLink = value() | rpl::map([](const Settings &data) {
return data.singlePeerFrom;
}) | rpl::distinct_until_changed(
) | rpl::map([=](TimeId from) {
return mapToTime(from, u"internal:edit_from_time"_q);
}) | rpl::flatten_latest();
auto fromLink = rpl::combine(
std::move(fromDateLink),
std::move(fromTimeLink)
) | rpl::map(concat);
auto tillDateLink = value() | rpl::map([](const Settings &data) {
return data.singlePeerTill;
}) | rpl::distinct_until_changed(
) | rpl::map([](TimeId till) {
return (till
? rpl::single(langDayOfMonthFull(
base::unixtime::parse(till).date()))
: tr::lng_export_end()
) | rpl::map(tr::url(u"internal:edit_till"_q));
}) | rpl::flatten_latest();
auto tillTimeLink = value() | rpl::map([](const Settings &data) {
return data.singlePeerTill;
}) | rpl::distinct_until_changed(
) | rpl::map([=](TimeId till) {
return mapToTime(till, u"internal:edit_till_time"_q);
}) | rpl::flatten_latest();
auto tillLink = rpl::combine(
std::move(tillDateLink),
std::move(tillTimeLink)
) | rpl::map(concat);
auto datesText = tr::lng_export_limits(
lt_from,
std::move(fromLink),
lt_till,
std::move(tillLink),
tr::marked
) | rpl::after_next([=] {
container->resizeToWidth(container->width());
});
const auto label = container->add(
object_ptr<Ui::FlatLabel>(
container,
std::move(datesText),
st::boxLabel),
st::exportLimitsPadding);
const auto removeTime = [](TimeId dateTime) {
return base::unixtime::serialize(
QDateTime(
base::unixtime::parse(dateTime).date(),
QTime()));
};
const auto editTimeLimit = [=](Fn<TimeId()> now, Fn<void(TimeId)> done) {
_showBoxCallback(Box([=](not_null<Ui::GenericBox*> box) {
auto result = Ui::ChooseTimeWidget(
box->verticalLayout(),
[&] {
const auto time = base::unixtime::parse(now()).time();
return time.hour() * 3600
+ time.minute() * 60
+ time.second();
}(),
true);
const auto widget = box->addRow(std::move(result.widget));
const auto toSave = widget->lifetime().make_state<TimeId>(0);
std::move(
result.secondsValue
) | rpl::on_next([=](TimeId t) {
*toSave = t;
}, box->lifetime());
box->addButton(tr::lng_settings_save(), [=] {
done(*toSave);
box->closeBox();
});
box->addButton(tr::lng_cancel(), [=] {
box->closeBox();
});
box->setTitle(tr::lng_settings_ttl_after_custom());
}));
};
constexpr auto kOffset = 600;
label->overrideLinkClickHandler([=](const QString &url) {
if (url == u"internal:edit_from"_q) {
const auto done = [=](TimeId limit) {
changeData([&](Settings &settings) {
settings.singlePeerFrom = limit;
});
};
editDateLimit(
readData().singlePeerFrom,
0,
readData().singlePeerTill,
tr::lng_export_from_beginning(),
done);
} else if (url == u"internal:edit_from_time"_q) {
const auto now = [=] {
auto result = TimeId(0);
changeData([&](Settings &settings) {
result = settings.singlePeerFrom;
});
return result;
};
const auto done = [=](TimeId time) {
changeData([&](Settings &settings) {
const auto result = time
+ removeTime(settings.singlePeerFrom);
if (result >= settings.singlePeerTill
&& settings.singlePeerTill) {
settings.singlePeerFrom = settings.singlePeerTill
- kOffset;
} else {
settings.singlePeerFrom = result;
}
});
};
editTimeLimit(now, done);
} else if (url == u"internal:edit_till"_q) {
const auto done = [=](TimeId limit) {
changeData([&](Settings &settings) {
if (limit <= settings.singlePeerFrom
&& settings.singlePeerFrom) {
settings.singlePeerTill = settings.singlePeerFrom
+ kOffset;
} else {
settings.singlePeerTill = limit;
}
});
};
editDateLimit(
readData().singlePeerTill,
readData().singlePeerFrom,
0,
tr::lng_export_till_end(),
done);
} else if (url == u"internal:edit_till_time"_q) {
const auto now = [=] {
auto result = TimeId(0);
changeData([&](Settings &settings) {
result = settings.singlePeerTill;
});
return result;
};
const auto done = [=](TimeId time) {
changeData([&](Settings &settings) {
const auto result = time
+ removeTime(settings.singlePeerTill);
if (result <= settings.singlePeerFrom
&& settings.singlePeerFrom) {
settings.singlePeerTill = settings.singlePeerFrom
+ kOffset;
} else {
settings.singlePeerTill = result;
}
});
};
editTimeLimit(now, done);
} else {
Unexpected("Click handler URL in export limits edit.");
}
});
}
void SettingsWidget::editDateLimit(
TimeId current,
TimeId min,
TimeId max,
rpl::producer<QString> resetLabel,
Fn<void(TimeId)> done) {
Expects(_showBoxCallback != nullptr);
const auto highlighted = current
? base::unixtime::parse(current).date()
: max
? base::unixtime::parse(max).date()
: min
? base::unixtime::parse(min).date()
: QDate::currentDate();
const auto month = highlighted;
const auto shared = std::make_shared<base::weak_qptr<Ui::CalendarBox>>();
const auto finalize = [=](not_null<Ui::CalendarBox*> box) {
box->addLeftButton(std::move(resetLabel), crl::guard(this, [=] {
done(0);
if (const auto weak = shared->get()) {
weak->closeBox();
}
}));
};
const auto callback = crl::guard(this, [=](const QDate &date) {
done(base::unixtime::serialize(date.startOfDay()));
if (const auto weak = shared->get()) {
weak->closeBox();
}
});
auto box = Box<Ui::CalendarBox>(Ui::CalendarBoxArgs{
.month = month,
.highlighted = highlighted,
.callback = callback,
.finalize = finalize,
.st = st::exportCalendarSizes,
.minDate = (min
? base::unixtime::parse(min).date()
: QDate(2013, 8, 1)), // Telegram was launched in August 2013 :)
.maxDate = (max
? base::unixtime::parse(max).date()
: QDate::currentDate()),
});
*shared = base::make_weak(box.data());
_showBoxCallback(std::move(box));
}
not_null<Ui::RpWidget*> SettingsWidget::setupButtons(
not_null<Ui::ScrollArea*> scroll,
not_null<Ui::RpWidget*> wrap) {
using namespace rpl::mappers;
const auto buttonsPadding = st::defaultBox.buttonPadding;
const auto buttonsHeight = buttonsPadding.top()
+ st::defaultBoxButton.height
+ buttonsPadding.bottom();
const auto buttons = Ui::CreateChild<Ui::FixedHeightWidget>(
this,
buttonsHeight);
const auto topShadow = Ui::CreateChild<Ui::FadeShadow>(this);
const auto bottomShadow = Ui::CreateChild<Ui::FadeShadow>(this);
topShadow->toggleOn(scroll->scrollTopValue(
) | rpl::map(_1 > 0));
bottomShadow->toggleOn(rpl::combine(
scroll->heightValue(),
scroll->scrollTopValue(),
wrap->heightValue(),
_2
) | rpl::map([=](int top) {
return top < scroll->scrollTopMax();
}));
value() | rpl::map([](const Settings &data) {
return (data.types != Types(0)) || data.onlySinglePeer();
}) | rpl::distinct_until_changed(
) | rpl::on_next([=](bool canStart) {
refreshButtons(buttons, canStart);
topShadow->raise();
bottomShadow->raise();
}, buttons->lifetime());
sizeValue(
) | rpl::on_next([=](QSize size) {
buttons->resizeToWidth(size.width());
buttons->moveToLeft(0, size.height() - buttons->height());
topShadow->resizeToWidth(size.width());
topShadow->moveToLeft(0, 0);
bottomShadow->resizeToWidth(size.width());
bottomShadow->moveToLeft(0, buttons->y() - st::lineWidth);
}, buttons->lifetime());
return buttons;
}
void SettingsWidget::addHeader(
not_null<Ui::VerticalLayout*> container,
const QString &text) {
container->add(
object_ptr<Ui::FlatLabel>(
container,
text,
st::exportHeaderLabel),
st::exportHeaderPadding);
}
not_null<Ui::Checkbox*> SettingsWidget::addOption(
not_null<Ui::VerticalLayout*> container,
const QString &text,
Types types) {
const auto checkbox = container->add(
object_ptr<Ui::Checkbox>(
container,
text,
((readData().types & types) == types),
st::defaultBoxCheckbox),
st::exportSettingPadding);
checkbox->checkedChanges(
) | rpl::on_next([=](bool checked) {
changeData([&](Settings &data) {
if (checked) {
data.types |= types;
} else {
data.types &= ~types;
}
});
}, checkbox->lifetime());
return checkbox;
}
not_null<Ui::Checkbox*> SettingsWidget::addOptionWithAbout(
not_null<Ui::VerticalLayout*> container,
const QString &text,
Types types,
const QString &about) {
const auto result = addOption(container, text, types);
container->add(
object_ptr<Ui::FlatLabel>(
container,
about,
st::exportAboutOptionLabel),
st::exportAboutOptionPadding);
return result;
}
void SettingsWidget::addChatOption(
not_null<Ui::VerticalLayout*> container,
const QString &text,
Types types) {
const auto checkbox = addOption(container, text, types);
const auto onlyMy = container->add(
object_ptr<Ui::SlideWrap<Ui::Checkbox>>(
container,
object_ptr<Ui::Checkbox>(
container,
tr::lng_export_option_only_my(tr::now),
((readData().fullChats & types) != types),
st::defaultBoxCheckbox),
st::exportSubSettingPadding));
onlyMy->entity()->checkedChanges(
) | rpl::on_next([=](bool checked) {
changeData([&](Settings &data) {
if (checked) {
data.fullChats &= ~types;
} else {
data.fullChats |= types;
}
});
}, onlyMy->lifetime());
onlyMy->toggleOn(checkbox->checkedValue());
if (types & (Type::PublicGroups | Type::PublicChannels)) {
onlyMy->entity()->setChecked(true);
onlyMy->entity()->setDisabled(true);
}
}
void SettingsWidget::addMediaOptions(
not_null<Ui::VerticalLayout*> container) {
addMediaOption(
container,
tr::lng_export_option_photos(tr::now),
MediaType::Photo);
addMediaOption(
container,
tr::lng_export_option_video_files(tr::now),
MediaType::Video);
addMediaOption(
container,
tr::lng_export_option_voice_messages(tr::now),
MediaType::VoiceMessage);
addMediaOption(
container,
tr::lng_export_option_video_messages(tr::now),
MediaType::VideoMessage);
addMediaOption(
container,
tr::lng_export_option_stickers(tr::now),
MediaType::Sticker);
addMediaOption(
container,
tr::lng_export_option_gifs(tr::now),
MediaType::GIF);
addMediaOption(
container,
tr::lng_export_option_files(tr::now),
MediaType::File);
addSizeSlider(container);
}
void SettingsWidget::addMediaOption(
not_null<Ui::VerticalLayout*> container,
const QString &text,
MediaType type) {
const auto checkbox = container->add(
object_ptr<Ui::Checkbox>(
container,
text,
((readData().media.types & type) == type),
st::defaultBoxCheckbox),
st::exportSettingPadding);
checkbox->checkedChanges(
) | rpl::on_next([=](bool checked) {
changeData([&](Settings &data) {
if (checked) {
data.media.types |= type;
} else {
data.media.types &= ~type;
}
});
}, checkbox->lifetime());
}
void SettingsWidget::addSizeSlider(
not_null<Ui::VerticalLayout*> container) {
using namespace rpl::mappers;
const auto slider = container->add(
object_ptr<Ui::MediaSlider>(container, st::exportFileSizeSlider),
st::exportFileSizePadding);
slider->resize(st::exportFileSizeSlider.seekSize);
slider->setPseudoDiscrete(
kSizeValueCount,
SizeLimitByIndex,
readData().media.sizeLimit,
[=](int64 limit) {
changeData([&](Settings &data) {
data.media.sizeLimit = limit;
});
});
const auto label = Ui::CreateChild<Ui::LabelSimple>(
container.get(),
st::exportFileSizeLabel);
value() | rpl::map([](const Settings &data) {
return data.media.sizeLimit;
}) | rpl::on_next([=](int64 sizeLimit) {
const auto limit = sizeLimit / kMegabyte;
const auto size = QString::number(limit) + " MB";
const auto text = tr::lng_export_option_size_limit(
tr::now,
lt_size,
size);
label->setText(text);
}, slider->lifetime());
rpl::combine(
label->widthValue(),
slider->geometryValue(),
_2
) | rpl::on_next([=](QRect geometry) {
label->moveToRight(
st::exportFileSizePadding.right(),
geometry.y() - label->height() - st::exportFileSizeLabelBottom);
}, label->lifetime());
}
void SettingsWidget::refreshButtons(
not_null<Ui::RpWidget*> container,
bool canStart) {
container->hideChildren();
const auto children = container->children();
for (const auto child : children) {
if (child->isWidgetType()) {
child->deleteLater();
}
}
const auto start = canStart
? Ui::CreateChild<Ui::RoundButton>(
container.get(),
tr::lng_export_start(),
st::defaultBoxButton)
: nullptr;
if (start) {
start->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
start->show();
_startClicks = start->clicks() | rpl::to_empty;
container->sizeValue(
) | rpl::on_next([=](QSize size) {
const auto right = st::defaultBox.buttonPadding.right();
const auto top = st::defaultBox.buttonPadding.top();
start->moveToRight(right, top);
}, start->lifetime());
}
const auto cancel = Ui::CreateChild<Ui::RoundButton>(
container.get(),
tr::lng_cancel(),
st::defaultBoxButton);
cancel->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
cancel->show();
_cancelClicks = cancel->clicks() | rpl::to_empty;
rpl::combine(
container->sizeValue(),
start ? start->widthValue() : rpl::single(0)
) | rpl::on_next([=](QSize size, int width) {
const auto right = st::defaultBox.buttonPadding.right()
+ (width ? width + st::defaultBox.buttonPadding.left() : 0);
const auto top = st::defaultBox.buttonPadding.top();
cancel->moveToRight(right, top);
}, cancel->lifetime());
}
void SettingsWidget::chooseFolder() {
const auto callback = [=](QString &&result) {
changeData([&](Settings &data) {
data.path = std::move(result);
data.forceSubPath = IsDefaultPath(_session, data.path);
});
};
FileDialog::GetFolder(
this,
tr::lng_export_folder(tr::now),
readData().path,
callback);
}
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 {
return _cancelClicks.value(
) | rpl::map([](Wrap &&wrap) {
return std::move(wrap.value);
}) | rpl::flatten_latest();
}
} // namespace View
} // namespace Export

View File

@@ -0,0 +1,129 @@
/*
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
*/
#pragma once
#include "export/export_settings.h"
#include "ui/rp_widget.h"
#include "base/object_ptr.h"
namespace Ui {
class VerticalLayout;
class Checkbox;
class ScrollArea;
class BoxContent;
} // namespace Ui
namespace Main {
class Session;
} // namespace Main
namespace Export {
namespace View {
constexpr auto kSizeValueCount = 100;
int64 SizeLimitByIndex(int index);
class SettingsWidget : public Ui::RpWidget {
public:
SettingsWidget(
QWidget *parent,
not_null<Main::Session*> session,
Settings data);
rpl::producer<Settings> value() const;
rpl::producer<Settings> changes() const;
rpl::producer<> startClicks() const;
rpl::producer<> cancelClicks() const;
void setShowBoxCallback(Fn<void(object_ptr<Ui::BoxContent>)> callback) {
_showBoxCallback = std::move(callback);
}
private:
using Type = Settings::Type;
using Types = Settings::Types;
using MediaType = MediaSettings::Type;
using MediaTypes = MediaSettings::Types;
using Format = Output::Format;
void setupContent();
not_null<Ui::RpWidget*> setupButtons(
not_null<Ui::ScrollArea*> scroll,
not_null<Ui::RpWidget*> wrap);
void setupOptions(not_null<Ui::VerticalLayout*> container);
void setupFullExportOptions(not_null<Ui::VerticalLayout*> container);
void setupMediaOptions(not_null<Ui::VerticalLayout*> container);
void setupOtherOptions(not_null<Ui::VerticalLayout*> container);
void setupPathAndFormat(not_null<Ui::VerticalLayout*> container);
void addHeader(
not_null<Ui::VerticalLayout*> container,
const QString &text);
not_null<Ui::Checkbox*> addOption(
not_null<Ui::VerticalLayout*> container,
const QString &text,
Types types);
not_null<Ui::Checkbox*> addOptionWithAbout(
not_null<Ui::VerticalLayout*> container,
const QString &text,
Types types,
const QString &about);
void addChatOption(
not_null<Ui::VerticalLayout*> container,
const QString &text,
Types types);
void addMediaOptions(not_null<Ui::VerticalLayout*> container);
void addMediaOption(
not_null<Ui::VerticalLayout*> container,
const QString &text,
MediaType type);
void addSizeSlider(not_null<Ui::VerticalLayout*> container);
void addLocationLabel(
not_null<Ui::VerticalLayout*> container);
void addFormatAndLocationLabel(
not_null<Ui::VerticalLayout*> container);
void addLimitsLabel(
not_null<Ui::VerticalLayout*> container);
void chooseFolder();
void chooseFormat();
void refreshButtons(
not_null<Ui::RpWidget*> container,
bool canStart);
void editDateLimit(
TimeId current,
TimeId min,
TimeId max,
rpl::producer<QString> resetLabel,
Fn<void(TimeId)> done);
const Settings &readData() const;
template <typename Callback>
void changeData(Callback &&callback);
const not_null<Main::Session*> _session;
PeerId _singlePeerId = 0;
Fn<void(object_ptr<Ui::BoxContent>)> _showBoxCallback;
// Use through readData / changeData wrappers.
Settings _internal_data;
struct Wrap {
Wrap(rpl::producer<> value = nullptr)
: value(std::move(value)) {
}
rpl::producer<> value;
};
rpl::event_stream<Settings> _changes;
rpl::variable<Wrap> _startClicks;
rpl::variable<Wrap> _cancelClicks;
};
} // namespace View
} // namespace Export

View File

@@ -0,0 +1,125 @@
/*
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/view/export_view_top_bar.h"
#include "export/view/export_view_content.h"
#include "ui/rect.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/continuous_sliders.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/shadow.h"
#include "lang/lang_keys.h"
#include "styles/style_export.h"
#include "styles/style_media_player.h"
namespace Export {
namespace View {
TopBar::TopBar(QWidget *parent, Content &&content)
: RpWidget(parent)
, _infoLeft(this, st::exportTopBarLabel)
, _infoMiddle(this, st::exportTopBarLabel)
, _infoRight(this, st::exportTopBarLabel)
, _shadow(this)
, _progress(this, st::mediaPlayerPlayback)
, _button(this) {
resize(width(), st::mediaPlayerHeight + st::lineWidth);
_progress->setAttribute(Qt::WA_TransparentForMouseEvents);
updateData(std::move(content));
}
rpl::producer<Qt::MouseButton> TopBar::clicks() const {
return _button->clicks();
}
void TopBar::resizeToWidthInfo(int w) {
if (w <= 0) {
return;
}
const auto &infoFont = st::mediaPlayerName.style.font;
const auto infoTop = st::mediaPlayerNameTop - infoFont->ascent;
const auto padding = st::mediaPlayerPlayLeft + st::mediaPlayerPadding;
_infoLeft->moveToLeft(padding, infoTop);
auto availableWidth = w;
availableWidth -= rect::right(_infoLeft);
availableWidth -= padding;
_infoMiddle->resizeToWidth(_infoMiddle->naturalWidth());
_infoRight->resizeToWidth(_infoRight->naturalWidth());
if (_infoMiddle->naturalWidth() > availableWidth) {
_infoRight->moveToLeft(
w - padding - _infoRight->width(),
infoTop);
_infoMiddle->resizeToWidth(_infoRight->x()
- rect::right(_infoLeft)
- infoFont->spacew * 2);
_infoMiddle->moveToLeft(
rect::right(_infoLeft) + infoFont->spacew,
infoTop);
} else {
_infoMiddle->moveToLeft(
rect::right(_infoLeft) + infoFont->spacew,
infoTop);
_infoRight->moveToLeft(
rect::right(_infoMiddle) + infoFont->spacew,
infoTop);
}
}
void TopBar::updateData(Content &&content) {
if (content.rows.empty()) {
return;
}
const auto &row = content.rows[0];
_infoLeft->setMarkedText(
tr::lng_export_progress_title(tr::now, tr::bold)
.append(' ')
.append(QChar(0x2013)));
_infoMiddle->setText(row.label);
_infoRight->setMarkedText(Ui::Text::Colorized(row.info));
resizeToWidthInfo(width());
_progress->setValue(row.progress);
}
void TopBar::resizeEvent(QResizeEvent *e) {
resizeToWidthInfo(e->size().width());
_button->setGeometry(0, 0, width(), height() - st::lineWidth);
_progress->setGeometry(
0,
height() - st::mediaPlayerPlayback.fullWidth,
width(),
st::mediaPlayerPlayback.fullWidth);
}
void TopBar::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
auto fill = e->rect().intersected(
QRect(0, 0, width(), st::mediaPlayerHeight));
if (!fill.isEmpty()) {
p.fillRect(fill, st::mediaPlayerBg);
}
}
void TopBar::setShadowGeometryToLeft(int x, int y, int w, int h) {
_shadow->setGeometryToLeft(x, y, w, h);
}
void TopBar::showShadow() {
_shadow->show();
_progress->show();
}
void TopBar::hideShadow() {
_shadow->hide();
_progress->hide();
}
TopBar::~TopBar() = default;
} // namespace View
} // namespace Export

View File

@@ -0,0 +1,56 @@
/*
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
*/
#pragma once
#include "ui/rp_widget.h"
#include "base/object_ptr.h"
namespace Ui {
class FlatLabel;
class FilledSlider;
class AbstractButton;
class PlainShadow;
} // namespace Ui
namespace Export {
namespace View {
struct Content;
class TopBar : public Ui::RpWidget {
public:
TopBar(QWidget *parent, Content &&content);
rpl::producer<Qt::MouseButton> clicks() const;
void updateData(Content &&content);
void setShadowGeometryToLeft(int x, int y, int w, int h);
void showShadow();
void hideShadow();
~TopBar();
protected:
void resizeEvent(QResizeEvent *e) override;
void paintEvent(QPaintEvent *e) override;
private:
void resizeToWidthInfo(int w);
object_ptr<Ui::FlatLabel> _infoLeft;
object_ptr<Ui::FlatLabel> _infoMiddle;
object_ptr<Ui::FlatLabel> _infoRight;
object_ptr<Ui::PlainShadow> _shadow = { nullptr };
object_ptr<Ui::FilledSlider> _progress;
object_ptr<Ui::AbstractButton> _button;
};
} // namespace View
} // namespace Export