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
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:
2620
Telegram/SourceFiles/export/data/export_data_types.cpp
Normal file
2620
Telegram/SourceFiles/export/data/export_data_types.cpp
Normal file
File diff suppressed because it is too large
Load Diff
1057
Telegram/SourceFiles/export/data/export_data_types.h
Normal file
1057
Telegram/SourceFiles/export/data/export_data_types.h
Normal file
File diff suppressed because it is too large
Load Diff
3026
Telegram/SourceFiles/export/export_api_wrap.cpp
Normal file
3026
Telegram/SourceFiles/export/export_api_wrap.cpp
Normal file
File diff suppressed because it is too large
Load Diff
326
Telegram/SourceFiles/export/export_api_wrap.h
Normal file
326
Telegram/SourceFiles/export/export_api_wrap.h
Normal 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
|
||||
924
Telegram/SourceFiles/export/export_controller.cpp
Normal file
924
Telegram/SourceFiles/export/export_controller.cpp
Normal 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
|
||||
154
Telegram/SourceFiles/export/export_controller.h
Normal file
154
Telegram/SourceFiles/export/export_controller.h
Normal 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
|
||||
116
Telegram/SourceFiles/export/export_manager.cpp
Normal file
116
Telegram/SourceFiles/export/export_manager.cpp
Normal 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
|
||||
57
Telegram/SourceFiles/export/export_manager.h
Normal file
57
Telegram/SourceFiles/export/export_manager.h
Normal 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
|
||||
35
Telegram/SourceFiles/export/export_pch.h
Normal file
35
Telegram/SourceFiles/export/export_pch.h
Normal 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"
|
||||
50
Telegram/SourceFiles/export/export_settings.cpp
Normal file
50
Telegram/SourceFiles/export/export_settings.cpp
Normal 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
|
||||
133
Telegram/SourceFiles/export/export_settings.h
Normal file
133
Telegram/SourceFiles/export/export_settings.h
Normal 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
|
||||
499
Telegram/SourceFiles/export/output/export_output_abstract.cpp
Normal file
499
Telegram/SourceFiles/export/output/export_output_abstract.cpp
Normal 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
|
||||
108
Telegram/SourceFiles/export/output/export_output_abstract.h
Normal file
108
Telegram/SourceFiles/export/output/export_output_abstract.h
Normal 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
|
||||
140
Telegram/SourceFiles/export/output/export_output_file.cpp
Normal file
140
Telegram/SourceFiles/export/output/export_output_file.cpp
Normal 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
|
||||
57
Telegram/SourceFiles/export/output/export_output_file.h
Normal file
57
Telegram/SourceFiles/export/output/export_output_file.h
Normal 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
|
||||
3848
Telegram/SourceFiles/export/output/export_output_html.cpp
Normal file
3848
Telegram/SourceFiles/export/output/export_output_html.cpp
Normal file
File diff suppressed because it is too large
Load Diff
186
Telegram/SourceFiles/export/output/export_output_html.h
Normal file
186
Telegram/SourceFiles/export/output/export_output_html.h
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
1746
Telegram/SourceFiles/export/output/export_output_json.cpp
Normal file
1746
Telegram/SourceFiles/export/output/export_output_json.cpp
Normal file
File diff suppressed because it is too large
Load Diff
114
Telegram/SourceFiles/export/output/export_output_json.h
Normal file
114
Telegram/SourceFiles/export/output/export_output_json.h
Normal 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
|
||||
48
Telegram/SourceFiles/export/output/export_output_result.h
Normal file
48
Telegram/SourceFiles/export/output/export_output_result.h
Normal 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
|
||||
35
Telegram/SourceFiles/export/output/export_output_stats.cpp
Normal file
35
Telegram/SourceFiles/export/output/export_output_stats.cpp
Normal 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
|
||||
33
Telegram/SourceFiles/export/output/export_output_stats.h
Normal file
33
Telegram/SourceFiles/export/output/export_output_stats.h
Normal 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
|
||||
104
Telegram/SourceFiles/export/view/export.style
Normal file
104
Telegram/SourceFiles/export/view/export.style
Normal 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);
|
||||
}
|
||||
197
Telegram/SourceFiles/export/view/export_view_content.cpp
Normal file
197
Telegram/SourceFiles/export/view/export_view_content.cpp
Normal 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
|
||||
57
Telegram/SourceFiles/export/view/export_view_content.h
Normal file
57
Telegram/SourceFiles/export/view/export_view_content.h
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
382
Telegram/SourceFiles/export/view/export_view_progress.cpp
Normal file
382
Telegram/SourceFiles/export/view/export_view_progress.cpp
Normal 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
|
||||
62
Telegram/SourceFiles/export/view/export_view_progress.h
Normal file
62
Telegram/SourceFiles/export/view/export_view_progress.h
Normal 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
|
||||
959
Telegram/SourceFiles/export/view/export_view_settings.cpp
Normal file
959
Telegram/SourceFiles/export/view/export_view_settings.cpp
Normal 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
|
||||
129
Telegram/SourceFiles/export/view/export_view_settings.h
Normal file
129
Telegram/SourceFiles/export/view/export_view_settings.h
Normal 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
|
||||
125
Telegram/SourceFiles/export/view/export_view_top_bar.cpp
Normal file
125
Telegram/SourceFiles/export/view/export_view_top_bar.cpp
Normal 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
|
||||
56
Telegram/SourceFiles/export/view/export_view_top_bar.h
Normal file
56
Telegram/SourceFiles/export/view/export_view_top_bar.h
Normal 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
|
||||
Reference in New Issue
Block a user