diff --git a/Telegram/Resources/css/export_style.css b/Telegram/Resources/css/export_style.css deleted file mode 100644 index e2e78152a..000000000 --- a/Telegram/Resources/css/export_style.css +++ /dev/null @@ -1,3 +0,0 @@ -.page_wrap { - background-color: #fff; -} diff --git a/Telegram/Resources/export_html/css/style.css b/Telegram/Resources/export_html/css/style.css new file mode 100644 index 000000000..8048f5c56 --- /dev/null +++ b/Telegram/Resources/export_html/css/style.css @@ -0,0 +1,247 @@ +body { + margin: 0; + font: 12px/18px 'Open Sans',"Lucida Grande","Lucida Sans Unicode",Arial,Helvetica,Verdana,sans-serif; +} +.clearfix:after { + content: " "; + visibility: hidden; + display: block; + height: 0; + clear: both; +} +.pull_left { + float: left; +} +.pull_right { + float: right; +} +.page_wrap { + background-color: #ffffff; + color: #000000; +} +.page_wrap a { + color: #168acd; + text-decoration: none; +} +.page_wrap a:hover { + text-decoration: underline; +} +.page_header { + position: fixed; + background-color: #ffffff; + width: 100%; + border-bottom: 1px solid #e3e6e8; +} +.page_header .content { + width: 480px; + margin: 0 auto; + border-radius: 0 !important; +} +.page_header a.content { + background-image: url(../images/back.png); + background-repeat: no-repeat; + background-position: 24px 21px; + background-size: 24px 24px; +} +.bold { + color: #212121; +} +.details { + color: #70777b; +} +.page_header .content .text { + padding: 24px 24px 22px 24px; + font-size: 22px; + font-weight: 700; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.page_header a.content .text { + padding: 24px 24px 22px 82px; +} +.page_body { + padding-top: 64px; + width: 480px; + margin: 0 auto; +} +.page_about { + padding: 24px 24px; +} +.with_divider { + border-top: 1px solid #e3e6e8; +} +.userpic_link { + display: block; + text-decoration: none; +} +.userpic_link:hover { + text-decoration: none; +} +.userpic { + display: block; + border-radius: 50%; + overflow: hidden; +} +.userpic .initials { + display: block; + color: #fff; + text-align: center; + text-transform: uppercase; + user-select: none; +} +.userpic1 { + background-color: #ff5555; +} +.userpic2 { + background-color: #64bf47; +} +.userpic3 { + background-color: #ffab00; +} +.userpic4 { + background-color: #4f9cd9; +} +.userpic5 { + background-color: #9884e8; +} +.userpic6 { + background-color: #e671a5; +} +.userpic7 { + background-color: #47bcd1; +} +.userpic8 { + background-color: #ff8c44; +} +.personal_info { + padding: 24px; +} +.personal_info .userpic .initials { + font-size: 30px; +} +.personal_info .rows { + float: left; + padding-right: 24px; +} +.personal_info .names { + width: 164px; +} +.personal_info .info { + width: 124px; +} +.personal_info .bio { + width: 400px; +} +.personal_info .row { + padding-bottom: 16px; +} +a.block_link { + display: block; + text-decoration: none !important; + border-radius: 4px; +} +a.block_link:hover { + text-decoration: none !important; + background-color: #f5f7f8; +} +.sections { + padding: 11px 0; +} +.section { + height: 48px; + background-position: 24px 12px; + background-repeat: no-repeat; + background-size: 24px 24px; +} +.section .counter { + float: right; + padding: 14px 24px 0; + font-size: 15px; +} +.section .label { + padding: 15px 0 0 82px; + font-size: 15px; + font-weight: 700; +} +.section.calls { + background-image: url(../images/calls.png); +} +.section.chats { + background-image: url(../images/chats.png); +} +.section.contacts { + background-image: url(../images/contacts.png); +} +.section.frequent { + background-image: url(../images/frequent.png); +} +.section.photos { + background-image: url(../images/photos.png); +} +.section.sessions { + background-image: url(../images/sessions.png); +} +.section.web { + background-image: url(../images/web.png); +} +@media only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min-device-pixel-ratio: 2) { +.section.calls { + background-image: url(../images/calls@2x.png); +} +.section.chats { + background-image: url(../images/chats@2x.png); +} +.section.contacts { + background-image: url(../images/contacts@2x.png); +} +.section.frequent { + background-image: url(../images/frequent@2x.png); +} +.section.photos { + background-image: url(../images/photos@2x.png); +} +.section.sessions { + background-image: url(../images/sessions@2x.png); +} +.section.web { + background-image: url(../images/web@2x.png); +} +.page_header a.content { + background-image: url(../images/back@2x.png); +} +} +.list_page .page_about { + padding: 16px 24px 0; + font-size: 11px; +} +.list_page .entry_list { + padding: 16px 0; +} +.list_page .entry { + padding: 10px 16px; +} +.list_page .entry .userpic .initials { + font-size: 18px; +} +.list_page .entry .body { + margin-left: 66px; +} +.list_page .entry .name { + padding: 4px 0 2px; + font-weight: 700; + font-size: 14px; +} +.list_page .entry .subname { + padding-top: 4px; +} +.list_page .entry .details_entry { + padding-top: 4px; +} +.list_page .entry .info { + font-size: 11px; + padding-top: 5px; +} +.history { + padding: 16px 0; +} diff --git a/Telegram/Resources/export_html/images/back.png b/Telegram/Resources/export_html/images/back.png new file mode 100644 index 000000000..846479f94 Binary files /dev/null and b/Telegram/Resources/export_html/images/back.png differ diff --git a/Telegram/Resources/export_html/images/back@2x.png b/Telegram/Resources/export_html/images/back@2x.png new file mode 100644 index 000000000..03e3e5851 Binary files /dev/null and b/Telegram/Resources/export_html/images/back@2x.png differ diff --git a/Telegram/Resources/export_html/images/calls.png b/Telegram/Resources/export_html/images/calls.png new file mode 100644 index 000000000..adb42e2f5 Binary files /dev/null and b/Telegram/Resources/export_html/images/calls.png differ diff --git a/Telegram/Resources/export_html/images/calls@2x.png b/Telegram/Resources/export_html/images/calls@2x.png new file mode 100644 index 000000000..eb2de58b0 Binary files /dev/null and b/Telegram/Resources/export_html/images/calls@2x.png differ diff --git a/Telegram/Resources/export_html/images/chats.png b/Telegram/Resources/export_html/images/chats.png new file mode 100644 index 000000000..354ea9e3a Binary files /dev/null and b/Telegram/Resources/export_html/images/chats.png differ diff --git a/Telegram/Resources/export_html/images/chats@2x.png b/Telegram/Resources/export_html/images/chats@2x.png new file mode 100644 index 000000000..b7252a82b Binary files /dev/null and b/Telegram/Resources/export_html/images/chats@2x.png differ diff --git a/Telegram/Resources/export_html/images/contacts.png b/Telegram/Resources/export_html/images/contacts.png new file mode 100644 index 000000000..1b94b8ee3 Binary files /dev/null and b/Telegram/Resources/export_html/images/contacts.png differ diff --git a/Telegram/Resources/export_html/images/contacts@2x.png b/Telegram/Resources/export_html/images/contacts@2x.png new file mode 100644 index 000000000..64a213d2f Binary files /dev/null and b/Telegram/Resources/export_html/images/contacts@2x.png differ diff --git a/Telegram/Resources/export_html/images/frequent.png b/Telegram/Resources/export_html/images/frequent.png new file mode 100644 index 000000000..165601995 Binary files /dev/null and b/Telegram/Resources/export_html/images/frequent.png differ diff --git a/Telegram/Resources/export_html/images/frequent@2x.png b/Telegram/Resources/export_html/images/frequent@2x.png new file mode 100644 index 000000000..ca9c3c85b Binary files /dev/null and b/Telegram/Resources/export_html/images/frequent@2x.png differ diff --git a/Telegram/Resources/export_html/images/photos.png b/Telegram/Resources/export_html/images/photos.png new file mode 100644 index 000000000..3b511aee4 Binary files /dev/null and b/Telegram/Resources/export_html/images/photos.png differ diff --git a/Telegram/Resources/export_html/images/photos@2x.png b/Telegram/Resources/export_html/images/photos@2x.png new file mode 100644 index 000000000..587b4ff8c Binary files /dev/null and b/Telegram/Resources/export_html/images/photos@2x.png differ diff --git a/Telegram/Resources/export_html/images/sessions.png b/Telegram/Resources/export_html/images/sessions.png new file mode 100644 index 000000000..0354af4f5 Binary files /dev/null and b/Telegram/Resources/export_html/images/sessions.png differ diff --git a/Telegram/Resources/export_html/images/sessions@2x.png b/Telegram/Resources/export_html/images/sessions@2x.png new file mode 100644 index 000000000..58b213e84 Binary files /dev/null and b/Telegram/Resources/export_html/images/sessions@2x.png differ diff --git a/Telegram/Resources/export_html/images/web.png b/Telegram/Resources/export_html/images/web.png new file mode 100644 index 000000000..d82e7fb31 Binary files /dev/null and b/Telegram/Resources/export_html/images/web.png differ diff --git a/Telegram/Resources/export_html/images/web@2x.png b/Telegram/Resources/export_html/images/web@2x.png new file mode 100644 index 000000000..b436884e8 Binary files /dev/null and b/Telegram/Resources/export_html/images/web@2x.png differ diff --git a/Telegram/Resources/qrc/telegram.qrc b/Telegram/Resources/qrc/telegram.qrc index 48b4398eb..ab56db558 100644 --- a/Telegram/Resources/qrc/telegram.qrc +++ b/Telegram/Resources/qrc/telegram.qrc @@ -1,6 +1,22 @@ - ../css/export_style.css + ../export_html/css/style.css + ../export_html/images/back.png + ../export_html/images/back@2x.png + ../export_html/images/calls.png + ../export_html/images/calls@2x.png + ../export_html/images/chats.png + ../export_html/images/chats@2x.png + ../export_html/images/contacts.png + ../export_html/images/contacts@2x.png + ../export_html/images/frequent.png + ../export_html/images/frequent@2x.png + ../export_html/images/photos.png + ../export_html/images/photos@2x.png + ../export_html/images/sessions.png + ../export_html/images/sessions@2x.png + ../export_html/images/web.png + ../export_html/images/web@2x.png ../fonts/OpenSans-Regular.ttf diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index 0cc79f7e2..c513494fd 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -8,15 +8,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "export/data/export_data_types.h" #include "export/export_settings.h" +#include "export/output/export_output_file.h" #include "core/mime_type.h" #include #include +#include namespace App { // Hackish.. QString formatPhone(QString phone); } // namespace App QString FillAmountAndCurrency(uint64 amount, const QString ¤cy); +QString formatSizeText(qint64 size); namespace Export { namespace Data { @@ -24,6 +27,7 @@ namespace { constexpr auto kUserPeerIdShift = (1ULL << 32); constexpr auto kChatPeerIdShift = (2ULL << 32); +constexpr auto kMaxImageSize = 10000; } // namespace @@ -39,6 +43,43 @@ int32 BarePeerId(PeerId peerId) { return int32(peerId & 0xFFFFFFFFULL); } +int PeerColorIndex(int32 bareId) { + const auto index = std::abs(bareId) % 7; + const int map[] = { 0, 7, 4, 1, 6, 3, 5 }; + return map[index]; +} + +int StringBarePeerId(const Utf8String &data) { + auto result = 0xFF; + for (const auto ch : data) { + result *= 239; + result += ch; + result &= 0xFF; + } + return result; +} + +int ApplicationColorIndex(int applicationId) { + static const auto official = std::map { + { 1, 0 }, // iOS + { 7, 0 }, // iOS X + { 6, 1 }, // Android + { 21724, 1 }, // Android X + { 2834, 2 }, // macOS + { 2496, 3 }, // Webogram + { 2040, 4 }, // Desktop + { 1429, 5 }, // Windows Phone + }; + if (const auto i = official.find(applicationId); i != end(official)) { + return i->second; + } + return PeerColorIndex(applicationId); +} + +int DomainApplicationId(const Utf8String &data) { + return 0x1000 + StringBarePeerId(data); +} + bool IsChatPeerId(PeerId peerId) { return (peerId & kChatPeerIdShift) == kChatPeerIdShift; } @@ -440,6 +481,43 @@ UserpicsSlice ParseUserpicsSlice( return result; } +QString WriteImageThumb( + const QString &basePath, + const QString &largePath, + int width, + int height, + const QString &postfix) { + if (largePath.isEmpty()) { + return QString(); + } + const auto path = basePath + largePath; + QImageReader reader(path); + if (!reader.canRead()) { + return QString(); + } + const auto size = reader.size(); + if (size.isEmpty() + || size.width() >= kMaxImageSize + || size.height() >= kMaxImageSize) { + return QString(); + } + auto image = reader.read(); + if (image.isNull()) { + return QString(); + } + const auto format = reader.format(); + const auto lastSlash = largePath.lastIndexOf('/'); + const auto firstDot = largePath.indexOf('.', lastSlash + 1); + const auto thumb = (firstDot >= 0) + ? largePath.mid(0, firstDot) + postfix + largePath.mid(firstDot) + : largePath + postfix; + const auto result = Output::File::PrepareRelativePath(basePath, thumb); + if (!image.save(basePath + result, reader.format(), reader.quality())) { + return QString(); + } + return result; +} + ContactInfo ParseContactInfo(const MTPUser &data) { auto result = ContactInfo(); data.match([&](const MTPDuser &data) { @@ -459,6 +537,13 @@ ContactInfo ParseContactInfo(const MTPUser &data) { return result; } +int ContactColorIndex(const ContactInfo &data) { + if (data.userId != 0) { + return PeerColorIndex(data.userId); + } + return PeerColorIndex(StringBarePeerId(data.phoneNumber)); +} + User ParseUser(const MTPUser &data) { auto result = User(); result.info = ParseContactInfo(data); @@ -1066,6 +1151,7 @@ bool AppendTopPeers(ContactsList &to, const MTPcontacts_TopPeers &data) { Session ParseSession(const MTPAuthorization &data) { return data.match([&](const MTPDauthorization &data) { auto result = Session(); + result.applicationId = data.vapi_id.v; result.platform = ParseString(data.vplatform); result.deviceModel = ParseString(data.vdevice_model); result.systemVersion = ParseString(data.vsystem_version); @@ -1170,7 +1256,12 @@ DialogsInfo ParseDialogsInfo(const MTPmessages_Dialogs &data) { info.type = peer.user() ? DialogTypeFromUser(*peer.user()) : DialogTypeFromChat(*peer.chat()); - info.name = peer.name(); + info.name = peer.user() + ? peer.user()->info.firstName + : peer.name(); + info.lastName = peer.user() + ? peer.user()->info.lastName + : Utf8String(); info.input = peer.input(); } info.topMessageId = fields.vtop_message.v; @@ -1297,5 +1388,9 @@ Utf8String FormatMoneyAmount(uint64 amount, const Utf8String ¤cy) { QString::fromUtf8(currency)).toUtf8(); } +Utf8String FormatFileSize(int64 size) { + return formatSizeText(size).toUtf8(); +} + } // namespace Data } // namespace Export diff --git a/Telegram/SourceFiles/export/data/export_data_types.h b/Telegram/SourceFiles/export/data/export_data_types.h index e2e62afab..7449f934a 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.h +++ b/Telegram/SourceFiles/export/data/export_data_types.h @@ -26,6 +26,9 @@ using PeerId = uint64; PeerId UserPeerId(int32 userId); PeerId ChatPeerId(int32 chatId); int32 BarePeerId(PeerId peerId); +int PeerColorIndex(int32 bareId); +int ApplicationColorIndex(int applicationId); +int DomainApplicationId(const Utf8String &data); Utf8String ParseString(const MTPstring &data); @@ -77,6 +80,13 @@ struct Image { File file; }; +QString WriteImageThumb( + const QString &basePath, + const QString &largePath, + int width, + int height, + const QString &postfix = "_thumb"); + struct ContactInfo { int32 userId = 0; Utf8String firstName; @@ -88,6 +98,7 @@ struct ContactInfo { }; ContactInfo ParseContactInfo(const MTPUser &data); +int ContactColorIndex(const ContactInfo &data); struct Photo { uint64 id = 0; @@ -230,6 +241,7 @@ std::vector SortedContactsIndices(const ContactsList &data); bool AppendTopPeers(ContactsList &to, const MTPcontacts_TopPeers &data); struct Session { + int applicationId = 0; Utf8String platform; Utf8String deviceModel; Utf8String systemVersion; @@ -485,6 +497,7 @@ struct DialogInfo { }; Type type = Type::Unknown; Utf8String name; + Utf8String lastName; MTPInputPeer input = MTP_inputPeerEmpty(); int32 topMessageId = 0; @@ -526,14 +539,13 @@ MessagesSlice ParseMessagesSlice( const QString &mediaFolder); Utf8String FormatPhoneNumber(const Utf8String &phoneNumber); - Utf8String FormatDateTime( TimeId date, QChar dateSeparator = QChar('.'), QChar timeSeparator = QChar(':'), QChar separator = QChar(' ')); - Utf8String FormatMoneyAmount(uint64 amount, const Utf8String ¤cy); +Utf8String FormatFileSize(int64 size); } // namespace Data } // namespace Export diff --git a/Telegram/SourceFiles/export/output/export_output_abstract.cpp b/Telegram/SourceFiles/export/output/export_output_abstract.cpp index 3e1e9d0be..4d8c20881 100644 --- a/Telegram/SourceFiles/export/output/export_output_abstract.cpp +++ b/Telegram/SourceFiles/export/output/export_output_abstract.cpp @@ -56,19 +56,11 @@ std::unique_ptr CreateWriter(Format format) { Unexpected("Format in Export::Output::CreateWriter."); } -Stats AbstractWriter::produceTestExample(const QString &path) { +Stats AbstractWriter::produceTestExample( + const QString &path, + const Environment &environment) { auto result = Stats(); const auto folder = QDir(path).absolutePath(); - auto environment = Environment(); - environment.internalLinksDomain = "https://t.me/"; - environment.aboutTelegram = "About Telegram"; - environment.aboutContacts = "About contacts"; - environment.aboutFrequent = "About frequent"; - environment.aboutSessions = "About sessions"; - environment.aboutWebSessions = "About web sessions"; - environment.aboutChats = "About chats"; - environment.aboutLeftChats = "About left chats"; - auto settings = Settings(); settings.format = format(); settings.path = (folder.endsWith('/') ? folder : (folder + '/')) diff --git a/Telegram/SourceFiles/export/output/export_output_abstract.h b/Telegram/SourceFiles/export/output/export_output_abstract.h index 5fac809a2..540c139c8 100644 --- a/Telegram/SourceFiles/export/output/export_output_abstract.h +++ b/Telegram/SourceFiles/export/output/export_output_abstract.h @@ -90,7 +90,9 @@ public: virtual ~AbstractWriter() = default; - Stats produceTestExample(const QString &path); + Stats produceTestExample( + const QString &path, + const Environment &environment); }; diff --git a/Telegram/SourceFiles/export/output/export_output_html.cpp b/Telegram/SourceFiles/export/output/export_output_html.cpp index 469663310..40b3f222c 100644 --- a/Telegram/SourceFiles/export/output/export_output_html.cpp +++ b/Telegram/SourceFiles/export/output/export_output_html.cpp @@ -18,9 +18,15 @@ namespace Output { namespace { constexpr auto kMessagesInFile = 1000; +constexpr auto kPersonalUserpicSize = 90; +constexpr auto kEntryUserpicSize = 48; +constexpr auto kSavedMessagesColorIndex = 3; const auto kLineBreak = QByteArrayLiteral("
"); +using Context = details::HtmlContext; +using UserpicData = details::UserpicData; + QByteArray SerializeString(const QByteArray &value) { const auto size = value.size(); const auto begin = value.data(); @@ -599,12 +605,105 @@ QByteArray SerializeMessage( } // namespace +namespace details { + +struct UserpicData { + int colorIndex = 0; + int pixelSize = 0; + QString imageLink; + QString largeLink; + QByteArray firstName; + QByteArray lastName; +}; + +QByteArray HtmlContext::pushTag( + const QByteArray &tag, + std::map &&attributes) { + auto data = Tag(); + data.name = tag; + auto empty = false; + auto inner = QByteArray(); + for (const auto &[name, value] : attributes) { + if (name == "inline") { + data.block = false; + } else if (name == "empty") { + empty = true; + } else { + inner.append(' ').append(name); + inner.append("=\"").append(SerializeString(value)).append("\""); + } + } + auto result = (data.block ? ("\n" + indent()) : QByteArray()) + + "<" + data.name + inner + (empty ? "/" : "") + ">" + + (data.block ? "\n" : ""); + if (!empty) { + _tags.push_back(data); + } + return result; +} + +QByteArray HtmlContext::popTag() { + Expects(!_tags.empty()); + + const auto data = _tags.back(); + _tags.pop_back(); + return (data.block ? ("\n" + indent()) : QByteArray()) + + "" + + (data.block ? "\n" : ""); +} + +QByteArray HtmlContext::indent() const { + return QByteArray(_tags.size(), ' '); +} + +bool HtmlContext::empty() const { + return _tags.empty(); +} + +} // namespace details + class HtmlWriter::Wrap { public: Wrap(const QString &path, const QString &base, Stats *stats); [[nodiscard]] bool empty() const; + [[nodiscard]] QByteArray pushTag( + const QByteArray &tag, + std::map &&attributes = {}); + [[nodiscard]] QByteArray popTag(); + [[nodiscard]] QByteArray indent() const; + + [[nodiscard]] QByteArray pushDiv( + const QByteArray &className, + const QByteArray &style = {}); + + [[nodiscard]] QByteArray pushUserpic(const UserpicData &userpic); + [[nodiscard]] QByteArray pushListEntry( + const UserpicData &userpic, + const QByteArray &name, + const QByteArray &details, + const QByteArray &info, + const QString &link = QString()); + [[nodiscard]] QByteArray pushSessionListEntry( + int apiId, + const QByteArray &name, + const QByteArray &subname, + std::initializer_list details, + const QByteArray &info = QByteArray()); + + [[nodiscard]] QByteArray pushHeader( + const QByteArray &header, + const QString &path = QString()); + [[nodiscard]] QByteArray pushSection( + const QByteArray &label, + const QByteArray &type, + int count, + const QString &path); + [[nodiscard]] QByteArray pushAbout( + const QByteArray &text, + bool withDivider = false); + [[nodiscard]] Result writeBlock(const QByteArray &block); [[nodiscard]] Result close(); @@ -615,15 +714,36 @@ public: ~Wrap(); private: - QByteArray begin() const; - QByteArray end() const; + [[nodiscard]] QByteArray composeStart(); + [[nodiscard]] QByteArray pushGenericListEntry( + const QString &link, + const UserpicData &userpic, + const QByteArray &name, + const QByteArray &subname, + std::initializer_list details, + const QByteArray &info); File _file; bool _closed = false; QByteArray _base; + Context _context; }; +struct HtmlWriter::SavedSection { + int priority = 0; + QByteArray label; + QByteArray type; + int count = 0; + QString path; +}; + +QByteArray ComposeName(const UserpicData &data, const QByteArray &empty) { + return ((data.firstName.isEmpty() && data.lastName.isEmpty()) + ? empty + : (data.firstName + ' ' + data.lastName)); +} + HtmlWriter::Wrap::Wrap( const QString &path, const QString &base, @@ -641,6 +761,153 @@ bool HtmlWriter::Wrap::empty() const { return _file.empty(); } +QByteArray HtmlWriter::Wrap::pushTag( + const QByteArray &tag, + std::map &&attributes) { + return _context.pushTag(tag, std::move(attributes)); +} + +QByteArray HtmlWriter::Wrap::popTag() { + return _context.popTag(); +} + +QByteArray HtmlWriter::Wrap::indent() const { + return _context.indent(); +} + +QByteArray HtmlWriter::Wrap::pushDiv( + const QByteArray &className, + const QByteArray &style) { + return style.isEmpty() + ? _context.pushTag("div", { { "class", className } }) + : _context.pushTag("div", { + { "class", className }, + { "style", style } + }); +} + +QByteArray HtmlWriter::Wrap::pushUserpic(const UserpicData &userpic) { + const auto size = Data::NumberToString(userpic.pixelSize) + "px"; + auto result = QByteArray(); + if (!userpic.largeLink.isEmpty()) { + result.append(pushTag("a", { + { "class", "userpic_link" }, + { "href", relativePath(userpic.largeLink).toUtf8() } + })); + } + const auto sizeStyle = "width: " + size + "; height: " + size; + if (!userpic.imageLink.isEmpty()) { + result.append(pushTag("img", { + { "class", "userpic" }, + { "style", sizeStyle }, + { "src", relativePath(userpic.imageLink).toUtf8() }, + { "empty", "" } + })); + } else { + result.append(pushTag("div", { + { + "class", + "userpic userpic" + + Data::NumberToString(userpic.colorIndex + 1) + }, + { "style", sizeStyle } + })); + result.append(pushDiv( + "initials", + "line-height: " + size)); + auto character = [](const QByteArray &from) { + const auto utf = QString::fromUtf8(from).trimmed(); + return utf.isEmpty() ? QByteArray() : utf.mid(0, 1).toUtf8(); + }; + result.append(character(userpic.firstName)); + result.append(character(userpic.lastName)); + result.append(popTag()); + result.append(popTag()); + } + if (!userpic.largeLink.isEmpty()) { + result.append(popTag()); + } + return result; +} + +QByteArray HtmlWriter::Wrap::pushListEntry( + const UserpicData &userpic, + const QByteArray &name, + const QByteArray &details, + const QByteArray &info, + const QString &link) { + return pushGenericListEntry( + link, + userpic, + name, + {}, + { details }, + info); +} + +QByteArray HtmlWriter::Wrap::pushSessionListEntry( + int apiId, + const QByteArray &name, + const QByteArray &subname, + std::initializer_list details, + const QByteArray &info) { + const auto link = QString(); + auto userpic = UserpicData{ + Data::ApplicationColorIndex(apiId), + kEntryUserpicSize + }; + userpic.firstName = name; + return pushGenericListEntry( + link, + userpic, + name, + subname, + details, + info); +} + +QByteArray HtmlWriter::Wrap::pushGenericListEntry( + const QString &link, + const UserpicData &userpic, + const QByteArray &name, + const QByteArray &subname, + std::initializer_list details, + const QByteArray &info) { + auto result = link.isEmpty() + ? pushDiv("entry clearfix") + : pushTag("a", { + { "class", "entry block_link clearfix" }, + { "href", relativePath(link).toUtf8() }, + }); + result.append(pushDiv("pull_left userpic_wrap")); + result.append(pushUserpic(userpic)); + result.append(popTag()); + result.append(pushDiv("body")); + if (!info.isEmpty()) { + result.append(pushDiv("pull_right info details")); + result.append(SerializeString(info)); + result.append(popTag()); + } + if (!name.isEmpty()) { + result.append(pushDiv("name bold")); + result.append(SerializeString(name)); + result.append(popTag()); + } + if (!subname.isEmpty()) { + result.append(pushDiv("subname bold")); + result.append(SerializeString(subname)); + result.append(popTag()); + } + for (const auto detail : details) { + result.append(pushDiv("details_entry details")); + result.append(SerializeString(detail)); + result.append(popTag()); + } + result.append(popTag()); + result.append(popTag()); + return result; +} + Result HtmlWriter::Wrap::writeBlock(const QByteArray &block) { Expects(!_closed); @@ -648,7 +915,7 @@ Result HtmlWriter::Wrap::writeBlock(const QByteArray &block) { if (block.isEmpty()) { return _file.writeBlock(block); } else if (_file.empty()) { - return _file.writeBlock(begin() + block); + return _file.writeBlock(composeStart() + block); } return _file.writeBlock(block); }(); @@ -658,9 +925,61 @@ Result HtmlWriter::Wrap::writeBlock(const QByteArray &block) { return result; } +QByteArray HtmlWriter::Wrap::pushHeader( + const QByteArray &header, + const QString &path) { + auto result = pushDiv("page_header"); + result.append(path.isEmpty() + ? pushDiv("content") + : pushTag("a", { + { "class", "content block_link" }, + { "href", relativePath(path).toUtf8() } + })); + result.append(pushDiv("text bold")); + result.append(SerializeString(header)); + result.append(popTag()); + result.append(popTag()); + result.append(popTag()); + return result; +} + +QByteArray HtmlWriter::Wrap::pushSection( + const QByteArray &header, + const QByteArray &type, + int count, + const QString &link) { + auto result = pushTag("a", { + { "class", "section block_link " + type }, + { "href", link.toUtf8() }, + }); + result.append(pushDiv("counter details")); + result.append(Data::NumberToString(count)); + result.append(popTag()); + result.append(pushDiv("label bold")); + result.append(SerializeString(header)); + result.append(popTag()); + result.append(popTag()); + return result; +} + +QByteArray HtmlWriter::Wrap::pushAbout( + const QByteArray &text, + bool withDivider) { + auto result = pushDiv(withDivider + ? "page_about details with_divider" + : "page_about details"); + result.append(MakeLinks(SerializeString(text))); + result.append(popTag()); + return result; +} + Result HtmlWriter::Wrap::close() { if (!std::exchange(_closed, true) && !_file.empty()) { - return _file.writeBlock(end()); + auto block = QByteArray(); + while (!_context.empty()) { + block.append(_context.popTag()); + } + return _file.writeBlock(block); } return Result::Success(); } @@ -673,26 +992,30 @@ QString HtmlWriter::Wrap::relativePath(const Data::File &file) const { return relativePath(file.relativePath); } -QByteArray HtmlWriter::Wrap::begin() const { - return "\ -\n\ -\n\ -\n\ - \n\ - Exported Data\n\ - \n\ - \n\ -\n\ -\n\ -
\n"; -} - -QByteArray HtmlWriter::Wrap::end() const { - return "\ -
\n\ -\n\ -\n"; +QByteArray HtmlWriter::Wrap::composeStart() { + auto result = "" + _context.pushTag("html"); + result.append(pushTag("head")); + result.append(pushTag("meta", { + { "charset", "utf-8" }, + { "empty", "" } + })); + result.append(pushTag("title", { { "inline", "" } })); + result.append("Exported Data"); + result.append(popTag()); + result.append(_context.pushTag("meta", { + { "name", "viewport" }, + { "content", "width=device-width, initial-scale=1.0" }, + { "empty", "" } + })); + result.append(_context.pushTag("link", { + { "href", _base + "css/style.css" }, + { "rel", "stylesheet" }, + { "empty", "" } + })); + result.append(popTag()); + result.append(pushTag("body")); + result.append(pushDiv("page_wrap")); + return result; } HtmlWriter::Wrap::~Wrap() { @@ -718,33 +1041,128 @@ Result HtmlWriter::start( //if (!result) { // return result; //} - const auto result = copyFile(":/export/css/style.css", "css/style.css"); - if (!result) { - return result; + const auto copy = [&](const QString &filename) { + return copyFile(":/export/" + filename, filename); + }; + const auto files = { + "css/style.css", + "images/back.png", + "images/calls.png", + "images/chats.png", + "images/contacts.png", + "images/frequent.png", + "images/photos.png", + "images/sessions.png", + "images/web.png", + }; + for (const auto path : files) { + const auto name = QString(path); + if (const auto result = copy(name); !result) { + return result; + } else if (const auto png = name.indexOf(".png"); png > 0) { + const auto x2 = name.mid(0, png) + "@2x.png"; + if (const auto result = copy(x2); !result) { + return result; + } + } } - return _summary->writeBlock( - MakeLinks(SerializeString(_environment.aboutTelegram)) - + kLineBreak - + kLineBreak); + auto block = _summary->pushHeader("Exported Data"); + block.append(_summary->pushDiv("page_body")); + return _summary->writeBlock(block); } Result HtmlWriter::writePersonal(const Data::PersonalInfo &data) { Expects(_summary != nullptr); + _selfColorIndex = Data::PeerColorIndex(data.user.info.userId); + if (_settings.types & Settings::Type::Userpics) { + _delayedPersonalInfo = std::make_unique(data); + return Result::Success(); + } + return writeDefaultPersonal(data); +} + +Result HtmlWriter::writeDefaultPersonal(const Data::PersonalInfo &data) { + return writePreparedPersonal(data, QString()); +} + +Result HtmlWriter::writeDelayedPersonal(const QString &userpicPath) { + if (!_delayedPersonalInfo) { + return Result::Success(); + } + const auto result = writePreparedPersonal( + *base::take(_delayedPersonalInfo), + userpicPath); + if (!result) { + return result; + } + if (_userpicsCount) { + pushUserpicsSection(); + } + return Result::Success(); +} + +Result HtmlWriter::writePreparedPersonal( + const Data::PersonalInfo &data, + const QString &userpicPath) { const auto &info = data.user.info; - const auto serialized = SerializeKeyValue({ - { "First name", SerializeString(info.firstName) }, - { "Last name", SerializeString(info.lastName) }, - { - "Phone number", - SerializeString(Data::FormatPhoneNumber(info.phoneNumber)) - }, - { "Username", SerializeString(FormatUsername(data.user.username)) }, - { "Bio", SerializeString(data.bio) }, - }) - + kLineBreak - + kLineBreak; - return _summary->writeBlock(serialized); + + auto userpic = UserpicData{ _selfColorIndex, kPersonalUserpicSize }; + userpic.largeLink = userpicPath.isEmpty() + ? QString() + : userpicsFilePath(); + userpic.imageLink = writeUserpicThumb(userpicPath, userpic, "_info"); + userpic.firstName = info.firstName; + userpic.lastName = info.lastName; + + auto block = _summary->pushDiv("personal_info clearfix"); + block.append(_summary->pushDiv("pull_right userpic_wrap")); + block.append(_summary->pushUserpic(userpic)); + block.append(_summary->popTag()); + const auto pushRows = [&]( + QByteArray name, + std::vector> &&values) { + block.append(_summary->pushDiv("rows " + name)); + for (const auto &[key, value] : values) { + if (value.isEmpty()) { + continue; + } + block.append(_summary->pushDiv("row")); + block.append(_summary->pushDiv("label details")); + block.append(SerializeString(key)); + block.append(_summary->popTag()); + block.append(_summary->pushDiv("value bold")); + block.append(SerializeString(value)); + block.append(_summary->popTag()); + block.append(_summary->popTag()); + } + block.append(_summary->popTag()); + }; + pushRows("names", { + { "First name", info.firstName }, + { "Last name", info.lastName }, + }); + pushRows("info", { + { "Phone number", Data::FormatPhoneNumber(info.phoneNumber) }, + { "Username", FormatUsername(data.user.username) }, + }); + pushRows("bio", { { "Bio", data.bio } }); + block.append(_summary->popTag()); + + _summaryNeedDivider = true; + return _summary->writeBlock(block); +} + +QString HtmlWriter::writeUserpicThumb( + const QString &largePath, + const UserpicData &userpic, + const QString &postfix) { + return Data::WriteImageThumb( + _settings.path, + largePath, + userpic.pixelSize * 2, + userpic.pixelSize * 2, + postfix); } Result HtmlWriter::writeUserpicsStart(const Data::UserpicsInfo &data) { @@ -755,67 +1173,89 @@ Result HtmlWriter::writeUserpicsStart(const Data::UserpicsInfo &data) { if (!_userpicsCount) { return Result::Success(); } - const auto filename = "lists/profile_pictures.html"; - _userpics = fileWithRelativePath(filename); + _userpics = fileWithRelativePath(userpicsFilePath()); - const auto serialized = SerializeLink( - "Profile pictures " - "(" + Data::NumberToString(_userpicsCount) + ")", - _summary->relativePath(filename)) - + kLineBreak - + kLineBreak; - return _summary->writeBlock(serialized); + auto block = _userpics->pushHeader( + "Personal photos", + mainFileRelativePath()); + block.append(_userpics->pushDiv("page_body list_page")); + block.append(_userpics->pushDiv("entry_list")); + if (const auto result = _userpics->writeBlock(block); !result) { + return result; + } + if (!_delayedPersonalInfo) { + pushUserpicsSection(); + } + return Result::Success(); } Result HtmlWriter::writeUserpicsSlice(const Data::UserpicsSlice &data) { Expects(_userpics != nullptr); Expects(!data.list.empty()); - auto lines = std::vector(); - lines.reserve(data.list.size()); - for (const auto &userpic : data.list) { - if (!userpic.date) { - lines.push_back("(deleted photo)"); - } else { - using SkipReason = Data::File::SkipReason; - const auto &file = userpic.image.file; - Assert(!file.relativePath.isEmpty() - || file.skipReason != SkipReason::None); - const auto path = [&]() -> Data::Utf8String { - switch (file.skipReason) { - case SkipReason::Unavailable: - return "(Photo unavailable, please try again later)"; - case SkipReason::FileSize: - return "(Photo exceeds maximum size. " - "Change data exporting settings to download.)"; - case SkipReason::FileType: - return "(Photo not included. " - "Change data exporting settings to download.)"; - case SkipReason::None: return SerializeLink( - FormatFilePath(file), - _userpics->relativePath(file.relativePath)); - } - Unexpected("Skip reason while writing photo path."); - }(); - lines.push_back(SerializeKeyValue({ - { - "Added", - SerializeString(Data::FormatDateTime(userpic.date)) - }, - { "Photo", path }, - })); - } + const auto firstPath = data.list.front().image.file.relativePath; + if (const auto result = writeDelayedPersonal(firstPath); !result) { + return result; } - return _userpics->writeBlock(JoinList(kLineBreak, lines) + kLineBreak); + + auto block = QByteArray(); + for (const auto &userpic : data.list) { + auto data = UserpicData{ _selfColorIndex, kEntryUserpicSize }; + using SkipReason = Data::File::SkipReason; + const auto &file = userpic.image.file; + Assert(!file.relativePath.isEmpty() + || file.skipReason != SkipReason::None); + const auto status = [&]() -> Data::Utf8String { + switch (file.skipReason) { + case SkipReason::Unavailable: + return "(Photo unavailable, please try again later)"; + case SkipReason::FileSize: + return "(Photo exceeds maximum size. " + "Change data exporting settings to download.)"; + case SkipReason::FileType: + return "(Photo not included. " + "Change data exporting settings to download.)"; + case SkipReason::None: return Data::FormatFileSize(file.size); + } + Unexpected("Skip reason while writing photo path."); + }(); + const auto &path = userpic.image.file.relativePath; + data.imageLink = writeUserpicThumb(path, data); + data.firstName = path.toUtf8(); + block.append(_userpics->pushListEntry( + data, + (path.isEmpty() ? QString("Photo unavailable") : path).toUtf8(), + status, + (userpic.date > 0 + ? Data::FormatDateTime(userpic.date) + : QByteArray()), + path)); + } + return _userpics->writeBlock(block); } Result HtmlWriter::writeUserpicsEnd() { - if (_userpics) { + if (const auto result = writeDelayedPersonal(QString()); !result) { + return result; + } else if (_userpics) { return base::take(_userpics)->close(); } return Result::Success(); } +QString HtmlWriter::userpicsFilePath() const { + return "lists/profile_pictures.html"; +} + +void HtmlWriter::pushUserpicsSection() { + pushSection( + 4, + "Profile pictures", + "photos", + _userpicsCount, + userpicsFilePath()); +} + Result HtmlWriter::writeContactsList(const Data::ContactsList &data) { Expects(_summary != nullptr); @@ -834,47 +1274,39 @@ Result HtmlWriter::writeSavedContacts(const Data::ContactsList &data) { const auto filename = "lists/contacts.html"; const auto file = fileWithRelativePath(filename); - auto list = std::vector(); - list.reserve(data.list.size()); + auto block = file->pushHeader( + "Contacts", + mainFileRelativePath()); + block.append(file->pushDiv("page_body list_page")); + block.append(file->pushAbout(_environment.aboutContacts)); + block.append(file->pushDiv("entry_list")); for (const auto index : Data::SortedContactsIndices(data)) { const auto &contact = data.list[index]; - if (contact.firstName.isEmpty() - && contact.lastName.isEmpty() - && contact.phoneNumber.isEmpty()) { - list.push_back("(deleted user)" + kLineBreak); - } else { - list.push_back(SerializeKeyValue({ - { "First name", SerializeString(contact.firstName) }, - { "Last name", SerializeString(contact.lastName) }, - { - "Phone number", - SerializeString( - Data::FormatPhoneNumber(contact.phoneNumber)) - }, - { - "Added", - SerializeString(Data::FormatDateTime(contact.date)) - } - })); - } + auto userpic = UserpicData{ + Data::ContactColorIndex(contact), + kEntryUserpicSize + }; + userpic.firstName = contact.firstName; + userpic.lastName = contact.lastName; + block.append(file->pushListEntry( + userpic, + ComposeName(userpic, "Deleted Account"), + Data::FormatPhoneNumber(contact.phoneNumber), + Data::FormatDateTime(contact.date))); } - const auto full = MakeLinks(SerializeString(_environment.aboutContacts)) - + kLineBreak - + kLineBreak - + JoinList(kLineBreak, list); - if (const auto result = file->writeBlock(full); !result) { + if (const auto result = file->writeBlock(block); !result) { return result; } else if (const auto closed = file->close(); !closed) { return closed; } - const auto header = SerializeLink( - "Contacts " - "(" + Data::NumberToString(data.list.size()) + ")", - _summary->relativePath(filename)) - + kLineBreak - + kLineBreak; - return _summary->writeBlock(header); + pushSection( + 2, + "Contacts", + "contacts", + data.list.size(), + filename); + return Result::Success(); } Result HtmlWriter::writeFrequentContacts(const Data::ContactsList &data) { @@ -887,83 +1319,62 @@ Result HtmlWriter::writeFrequentContacts(const Data::ContactsList &data) { const auto filename = "lists/frequent.html"; const auto file = fileWithRelativePath(filename); - auto list = std::vector(); - list.reserve(size); + auto block = file->pushHeader( + "Frequent contacts", + mainFileRelativePath()); + block.append(file->pushDiv("page_body list_page")); + block.append(file->pushAbout(_environment.aboutFrequent)); + block.append(file->pushDiv("entry_list")); const auto writeList = [&]( const std::vector &peers, Data::Utf8String category) { for (const auto &top : peers) { - const auto user = [&]() -> Data::Utf8String { - if (!top.peer.user() || top.peer.user()->isSelf) { - return Data::Utf8String(); - } else if (top.peer.name().isEmpty()) { - return "(deleted user)"; + const auto name = [&]() -> Data::Utf8String { + if (top.peer.chat()) { + return top.peer.name(); + } else if (top.peer.user()->isSelf) { + return "Saved messages"; + } else { + return top.peer.user()->info.firstName; } - return top.peer.name(); }(); - const auto chatType = [&] { - if (const auto chat = top.peer.chat()) { - return chat->username.isEmpty() - ? (chat->isBroadcast - ? "Private channel" - : (chat->isSupergroup - ? "Private supergroup" - : "Private group")) - : (chat->isBroadcast - ? "Public channel" - : "Public supergroup"); + const auto lastName = [&]() -> Data::Utf8String { + if (top.peer.user() && !top.peer.user()->isSelf) { + return top.peer.user()->info.lastName; } - return ""; + return {}; }(); - const auto chat = [&]() -> Data::Utf8String { - if (!top.peer.chat()) { - return Data::Utf8String(); - } else if (top.peer.name().isEmpty()) { - return "(deleted chat)"; - } - return top.peer.name(); - }(); - const auto saved = [&]() -> Data::Utf8String { - if (!top.peer.user() || !top.peer.user()->isSelf) { - return Data::Utf8String(); - } - return "Saved messages"; - }(); - list.push_back(SerializeKeyValue({ - { "Category", SerializeString(category) }, - { - "User", - top.peer.user() ? SerializeString(user) : QByteArray() - }, - { "Chat", SerializeString(saved) }, - { chatType, SerializeString(chat) }, - { - "Rating", - SerializeString(Data::NumberToString(top.rating)) - } - })); + auto userpic = UserpicData{ + Data::PeerColorIndex(Data::BarePeerId(top.peer.id())), + kEntryUserpicSize + }; + userpic.firstName = name; + userpic.lastName = lastName; + block.append(file->pushListEntry( + userpic, + ((name.isEmpty() && lastName.isEmpty()) + ? QByteArray("Deleted Account") + : (name + ' ' + lastName)), + "Rating: " + Data::NumberToString(top.rating), + category)); } }; - writeList(data.correspondents, "People"); - writeList(data.inlineBots, "Inline bots"); - writeList(data.phoneCalls, "Calls"); - const auto full = MakeLinks(SerializeString(_environment.aboutFrequent)) - + kLineBreak - + kLineBreak - + JoinList(kLineBreak, list); - if (const auto result = file->writeBlock(full); !result) { + writeList(data.correspondents, "people"); + writeList(data.inlineBots, "inline bots"); + writeList(data.phoneCalls, "calls"); + if (const auto result = file->writeBlock(block); !result) { return result; } else if (const auto closed = file->close(); !closed) { return closed; } - const auto header = SerializeLink( - "Frequent contacts " - "(" + Data::NumberToString(size) + ")", - _summary->relativePath(filename)) - + kLineBreak - + kLineBreak; - return _summary->writeBlock(header); + pushSection( + 3, + "Frequent contacts", + "frequent", + size, + filename); + return Result::Success(); } Result HtmlWriter::writeSessionsList(const Data::SessionsList &data) { @@ -986,50 +1397,50 @@ Result HtmlWriter::writeSessions(const Data::SessionsList &data) { const auto filename = "lists/sessions.html"; const auto file = fileWithRelativePath(filename); - auto list = std::vector(); - list.reserve(data.list.size()); + auto block = file->pushHeader( + "Sessions", + mainFileRelativePath()); + block.append(file->pushDiv("page_body list_page")); + block.append(file->pushAbout(_environment.aboutSessions)); + block.append(file->pushDiv("entry_list")); for (const auto &session : data.list) { - list.push_back(SerializeKeyValue({ + block.append(file->pushSessionListEntry( + session.applicationId, + ((session.applicationName.isEmpty() + ? Data::Utf8String("Unknown") + : session.applicationName) + + ' ' + + session.applicationVersion), + (session.deviceModel + + ", " + + session.platform + + ' ' + + session.systemVersion), { - "Last active", - SerializeString(Data::FormatDateTime(session.lastActive)) - }, - { "Last IP address", SerializeString(session.ip) }, - { "Last country", SerializeString(session.country) }, - { "Last region", SerializeString(session.region) }, - { - "Application name", - (session.applicationName.isEmpty() - ? Data::Utf8String("(unknown)") - : SerializeString(session.applicationName)) - }, - { - "Application version", - SerializeString(session.applicationVersion) - }, - { "Device model", SerializeString(session.deviceModel) }, - { "Platform", SerializeString(session.platform) }, - { "System version", SerializeString(session.systemVersion) }, - { "Created", Data::FormatDateTime(session.created) }, - })); + (session.ip + + " \xE2\x80\x93 " + + session.region + + ((session.region.isEmpty() || session.country.isEmpty()) + ? QByteArray() + : QByteArray(", ")) + + session.country), + "Last active: " + Data::FormatDateTime(session.lastActive), + "Created: " + Data::FormatDateTime(session.created) + })); } - const auto full = MakeLinks(SerializeString(_environment.aboutSessions)) - + kLineBreak - + kLineBreak - + JoinList(kLineBreak, list); - if (const auto result = file->writeBlock(full); !result) { + if (const auto result = file->writeBlock(block); !result) { return result; } else if (const auto closed = file->close(); !closed) { return closed; } - const auto header = SerializeLink( - "Sessions " - "(" + Data::NumberToString(data.list.size()) + ")", - _summary->relativePath(filename)) - + kLineBreak - + kLineBreak; - return _summary->writeBlock(header); + pushSection( + 5, + "Sessions", + "sessions", + data.list.size(), + filename); + return Result::Success(); } Result HtmlWriter::writeWebSessions(const Data::SessionsList &data) { @@ -1041,54 +1452,41 @@ Result HtmlWriter::writeWebSessions(const Data::SessionsList &data) { const auto filename = "lists/web_sessions.html"; const auto file = fileWithRelativePath(filename); - auto list = std::vector(); - list.reserve(data.webList.size()); + auto block = file->pushHeader( + "Web sessions", + mainFileRelativePath()); + block.append(file->pushDiv("page_body list_page")); + block.append(file->pushAbout(_environment.aboutWebSessions)); + block.append(file->pushDiv("entry_list")); for (const auto &session : data.webList) { - list.push_back(SerializeKeyValue({ + block.append(file->pushSessionListEntry( + Data::DomainApplicationId(session.domain), + (session.domain.isEmpty() + ? Data::Utf8String("Unknown") + : session.domain), + session.platform + ", " + session.browser, { - "Last active", - SerializeString(Data::FormatDateTime(session.lastActive)) + session.ip + " \xE2\x80\x93 " + session.region, + "Last active: " + Data::FormatDateTime(session.lastActive), + "Created: " + Data::FormatDateTime(session.created) }, - { "Last IP address", SerializeString(session.ip) }, - { "Last region", SerializeString(session.region) }, - { - "Bot username", - (session.botUsername.isEmpty() - ? Data::Utf8String("(unknown)") - : SerializeString(session.botUsername)) - }, - { - "Domain name", - (session.domain.isEmpty() - ? Data::Utf8String("(unknown)") - : SerializeString(session.domain)) - }, - { "Browser", SerializeString(session.browser) }, - { "Platform", SerializeString(session.platform) }, - { - "Created", - SerializeString(Data::FormatDateTime(session.created)) - }, - })); + (session.botUsername.isEmpty() + ? QByteArray() + : ('@' + session.botUsername)))); } - const auto full = MakeLinks( - SerializeString(_environment.aboutWebSessions)) - + kLineBreak - + kLineBreak - + JoinList(kLineBreak, list); - if (const auto result = file->writeBlock(full); !result) { + if (const auto result = file->writeBlock(block); !result) { return result; } else if (const auto closed = file->close(); !closed) { return closed; } - const auto header = SerializeLink( - "Web sessions " - "(" + Data::NumberToString(data.webList.size()) + ")", - _summary->relativePath(filename)) - + kLineBreak - + kLineBreak; - return _summary->writeBlock(header); + pushSection( + 6, + "Web sessions", + "web", + data.webList.size(), + filename); + return Result::Success(); } Result HtmlWriter::writeOtherData(const Data::File &data) { @@ -1162,22 +1560,28 @@ Result HtmlWriter::writeChatsStart( return Result::Success(); } + _dialogsRelativePath = fileName; _chats = fileWithRelativePath(fileName); _dialogIndex = 0; _dialogsCount = data.list.size(); - const auto block = MakeLinks(SerializeString(about)) + kLineBreak; + auto block = _chats->pushHeader( + listName, + mainFileRelativePath()); + block.append(_chats->pushDiv("page_body list_page")); + block.append(_chats->pushAbout(about)); + block.append(_chats->pushDiv("entry_list")); if (const auto result = _chats->writeBlock(block); !result) { return result; } - const auto header = SerializeLink( - listName + " " - "(" + Data::NumberToString(data.list.size()) + ")", - _summary->relativePath(fileName)) - + kLineBreak - + kLineBreak; - return _summary->writeBlock(header); + pushSection( + 0, + listName, + "chats", + data.list.size(), + fileName); + return writeSections(); } Result HtmlWriter::writeChatStart(const Data::DialogInfo &data) { @@ -1196,6 +1600,17 @@ Result HtmlWriter::writeChatSlice(const Data::MessagesSlice &data) { Expects(_chat != nullptr); Expects(!data.list.empty()); + if (_chat->empty()) { + auto block = _chat->pushHeader( + _dialog.name + ' ' + _dialog.lastName, + _dialogsRelativePath); + block.append(_chat->pushDiv("page_body chat_page")); + block.append(_chat->pushDiv("history")); + if (const auto result = _chat->writeBlock(block); !result) { + return result; + } + } + const auto wasIndex = (_messagesCount / kMessagesInFile); _messagesCount += data.list.size(); const auto nowIndex = (_messagesCount / kMessagesInFile); @@ -1231,59 +1646,71 @@ Result HtmlWriter::writeChatEnd() { using Type = Data::DialogInfo::Type; const auto TypeString = [](Type type) { switch (type) { - case Type::Unknown: return "(unknown)"; + case Type::Unknown: return "unknown"; case Type::Self: - case Type::Personal: return "Personal chat"; - case Type::Bot: return "Bot chat"; - case Type::PrivateGroup: return "Private group"; - case Type::PrivateSupergroup: return "Private supergroup"; - case Type::PublicSupergroup: return "Public supergroup"; - case Type::PrivateChannel: return "Private channel"; - case Type::PublicChannel: return "Public channel"; + case Type::Personal: return "private"; + case Type::Bot: return "bot"; + case Type::PrivateGroup: + case Type::PrivateSupergroup: + case Type::PublicSupergroup: return "group"; + case Type::PrivateChannel: + case Type::PublicChannel: return "channel"; + } + Unexpected("Dialog type in TypeString."); + }; + const auto DeletedString = [](Type type) { + switch (type) { + case Type::Unknown: + case Type::Self: + case Type::Personal: + case Type::Bot: return "Deleted Account"; + case Type::PrivateGroup: + case Type::PrivateSupergroup: + case Type::PublicSupergroup: return "Deleted Group"; + case Type::PrivateChannel: + case Type::PublicChannel: return "Deleted Channel"; } Unexpected("Dialog type in TypeString."); }; const auto NameString = []( - const Data::DialogInfo &dialog, - Type type) -> QByteArray { + const Data::DialogInfo &dialog) -> QByteArray { if (dialog.type == Type::Self) { return "Saved messages"; } - const auto name = dialog.name; - if (!name.isEmpty()) { - return name; - } - switch (type) { - case Type::Unknown: return "(unknown)"; - case Type::Personal: return "(deleted user)"; - case Type::Bot: return "(deleted bot)"; - case Type::PrivateGroup: - case Type::PrivateSupergroup: - case Type::PublicSupergroup: return "(deleted group)"; - case Type::PrivateChannel: - case Type::PublicChannel: return "(deleted channel)"; - } - Unexpected("Dialog type in TypeString."); + return dialog.name; }; - return _chats->writeBlock(kLineBreak + SerializeKeyValue({ - { "Name", SerializeString(NameString(_dialog, _dialog.type)) }, - { "Type", SerializeString(TypeString(_dialog.type)) }, - { - (_dialog.onlyMyMessages - ? "Outgoing messages count" - : "Messages count"), - SerializeString(Data::NumberToString(_messagesCount)) - }, - { - "Content", - (_messagesCount > 0 - ? SerializeLink( - (_dialog.relativePath + "messages.html").toUtf8(), - _chats->relativePath( - (_dialog.relativePath + "messages.html"))) - : QByteArray()) + const auto LastNameString = []( + const Data::DialogInfo &dialog) -> QByteArray { + if (dialog.type != Type::Personal && dialog.type != Type::Bot) { + return {}; } - })); + return dialog.lastName; + }; + const auto CountString = [](int count, bool outgoing) -> QByteArray { + if (count == 1) { + return outgoing ? "1 outgoing message" : "1 message"; + } + return Data::NumberToString(count) + + (outgoing ? " outgoing messages" : " messages"); + }; + auto userpic = UserpicData{ + (_dialog.type == Type::Self + ? kSavedMessagesColorIndex + : Data::PeerColorIndex(Data::BarePeerId(_dialog.peerId))), + kEntryUserpicSize + }; + userpic.firstName = NameString(_dialog); + userpic.lastName = LastNameString(_dialog); + return _chats->writeBlock(_chats->pushListEntry( + userpic, + ((userpic.firstName.isEmpty() && userpic.lastName.isEmpty()) + ? QByteArray(DeletedString(_dialog.type)) + : (userpic.firstName + ' ' + userpic.lastName)), + CountString(_messagesCount, _dialog.onlyMyMessages), + TypeString(_dialog.type), + (_messagesCount > 0 + ? (_dialog.relativePath + "messages.html") + : QString()))); } Result HtmlWriter::writeChatsEnd() { @@ -1293,23 +1720,91 @@ Result HtmlWriter::writeChatsEnd() { return Result::Success(); } +void HtmlWriter::pushSection( + int priority, + const QByteArray &label, + const QByteArray &type, + int count, + const QString &path) { + _savedSections.push_back({ + priority, + label, + type, + count, + path + }); +} + +Result HtmlWriter::writeSections() { + Expects(_summary != nullptr); + + if (!_haveSections && _summaryNeedDivider) { + auto block = _summary->pushDiv( + _summaryNeedDivider ? "sections with_divider" : "sections"); + if (const auto result = _summary->writeBlock(block); !result) { + return result; + } + _haveSections = true; + _summaryNeedDivider = false; + } + + auto block = QByteArray(); + ranges::sort(_savedSections, std::less<>(), [](const SavedSection &data) { + return data.priority; + }); + for (const auto §ion : base::take(_savedSections)) { + block.append(_summary->pushSection( + section.label, + section.type, + section.count, + _summary->relativePath(section.path))); + } + return _summary->writeBlock(block); +} + Result HtmlWriter::switchToNextChatFile(int index) { Expects(_chat != nullptr); const auto nextPath = messagesFile(index); - const auto link = kLineBreak + "Next messages part"; - if (const auto result = _chat->writeBlock(link); !result) { + auto next = _chat->pushTag("a", { + { "class", "pagination" }, + { "href", nextPath.toUtf8() } + }); + next.append("Next messages part"); + next.append(_chat->popTag()); + if (const auto result = _chat->writeBlock(next); !result) { return result; } _chat = fileWithRelativePath(_dialog.relativePath + nextPath); - return Result::Success(); + auto block = _chat->pushHeader( + _dialog.name + ' ' + _dialog.lastName, + _dialogsRelativePath); + block.append(_chat->pushDiv("page_body chat_page")); + block.append(_chat->pushDiv("history")); + block.append(_chat->pushTag("a", { + { "class", "pagination" }, + { "href", nextPath.toUtf8() } + })); + block.append("Previous messages part"); + block.append(_chat->popTag()); + return _chat->writeBlock(block); } Result HtmlWriter::finish() { Expects(_summary != nullptr); + auto block = QByteArray(); + if (_haveSections) { + block.append(_summary->popTag()); + _summaryNeedDivider = true; + _haveSections = false; + } + block.append(_summary->pushAbout( + _environment.aboutTelegram, + _summaryNeedDivider)); + if (const auto result = _summary->writeBlock(block); !result) { + return result; + } return _summary->close(); } diff --git a/Telegram/SourceFiles/export/output/export_output_html.h b/Telegram/SourceFiles/export/output/export_output_html.h index 644957605..9a04b7cac 100644 --- a/Telegram/SourceFiles/export/output/export_output_html.h +++ b/Telegram/SourceFiles/export/output/export_output_html.h @@ -14,6 +14,29 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Export { namespace Output { +namespace details { + +class HtmlContext { +public: + [[nodiscard]] QByteArray pushTag( + const QByteArray &tag, + std::map &&attributes = {}); + [[nodiscard]] QByteArray popTag(); + [[nodiscard]] QByteArray indent() const; + [[nodiscard]] bool empty() const; + +private: + struct Tag { + QByteArray name; + bool block = true; + }; + std::vector _tags; + +}; + +struct UserpicData; + +} // namespace details class HtmlWriter : public AbstractWriter { public: @@ -59,9 +82,11 @@ public: ~HtmlWriter(); private: + using Context = details::HtmlContext; + using UserpicData = details::UserpicData; class Wrap; - Result copyFile( + [[nodiscard]] Result copyFile( const QString &source, const QString &relativePath) const; @@ -70,34 +95,66 @@ private: std::unique_ptr fileWithRelativePath(const QString &path) const; QString messagesFile(int index) const; - Result writeSavedContacts(const Data::ContactsList &data); - Result writeFrequentContacts(const Data::ContactsList &data); + [[nodiscard]] Result writeSavedContacts(const Data::ContactsList &data); + [[nodiscard]] Result writeFrequentContacts(const Data::ContactsList &data); - Result writeSessions(const Data::SessionsList &data); - Result writeWebSessions(const Data::SessionsList &data); + [[nodiscard]] Result writeSessions(const Data::SessionsList &data); + [[nodiscard]] Result writeWebSessions(const Data::SessionsList &data); - Result writeChatsStart( + [[nodiscard]] Result writeChatsStart( const Data::DialogsInfo &data, const QByteArray &listName, const QByteArray &about, const QString &fileName); - Result writeChatStart(const Data::DialogInfo &data); - Result writeChatSlice(const Data::MessagesSlice &data); - Result writeChatEnd(); - Result writeChatsEnd(); - Result switchToNextChatFile(int index); + [[nodiscard]] Result writeChatStart(const Data::DialogInfo &data); + [[nodiscard]] Result writeChatSlice(const Data::MessagesSlice &data); + [[nodiscard]] Result writeChatEnd(); + [[nodiscard]] Result writeChatsEnd(); + [[nodiscard]] Result switchToNextChatFile(int index); + + 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(); + + [[nodiscard]] QString writeUserpicThumb( + const QString &largePath, + const UserpicData &userpic, + const QString &postfix = "_thumb"); + + [[nodiscard]] QString userpicsFilePath() const; Settings _settings; Environment _environment; Stats *_stats = nullptr; + struct SavedSection; + std::vector _savedSections; + std::unique_ptr _summary; + bool _summaryNeedDivider = false; + bool _haveSections = false; + + int _selfColorIndex = 0; + std::unique_ptr _delayedPersonalInfo; int _userpicsCount = 0; std::unique_ptr _userpics; int _dialogsCount = 0; int _dialogIndex = 0; + QString _dialogsRelativePath; Data::DialogInfo _dialog; int _messagesCount = 0; diff --git a/Telegram/SourceFiles/export/view/export_view_panel_controller.cpp b/Telegram/SourceFiles/export/view/export_view_panel_controller.cpp index 81dccea17..d803ee092 100644 --- a/Telegram/SourceFiles/export/view/export_view_panel_controller.cpp +++ b/Telegram/SourceFiles/export/view/export_view_panel_controller.cpp @@ -69,6 +69,8 @@ void SuggestBox::prepare() { }, content->lifetime()); } +} // namespace + Environment PrepareEnvironment() { auto result = Environment(); const auto utfLang = [](LangKey key) { @@ -85,8 +87,6 @@ Environment PrepareEnvironment() { return result; } -} // namespace - QPointer SuggestStart() { ClearSuggestStart(); return Ui::show(Box(), LayerOption::KeepOther).data(); diff --git a/Telegram/SourceFiles/export/view/export_view_panel_controller.h b/Telegram/SourceFiles/export/view/export_view_panel_controller.h index 6d77970ff..f10b19aee 100644 --- a/Telegram/SourceFiles/export/view/export_view_panel_controller.h +++ b/Telegram/SourceFiles/export/view/export_view_panel_controller.h @@ -19,8 +19,12 @@ class SeparatePanel; } // namespace Ui namespace Export { + +struct Environment; + namespace View { +Environment PrepareEnvironment(); QPointer SuggestStart(); void ClearSuggestStart(); diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index d25c86805..9d1ff0508 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -90,6 +90,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_history.h" #include "styles/style_boxes.h" +#ifdef _DEBUG +#include "export/output/export_output_html.h" +#include "export/output/export_output_stats.h" +#include "export/view/export_view_panel_controller.h" +#include "platform/platform_specific.h" +#else +#error "test" +#endif + namespace { bool IsForceLogoutNotification(const MTPDupdateServiceNotification &data) { @@ -252,6 +261,10 @@ MainWidget::MainWidget( Messenger::Instance().mtp()->setUpdatesHandler(rpcDone(&MainWidget::updateReceived)); Messenger::Instance().mtp()->setGlobalFailHandler(rpcFail(&MainWidget::updateFail)); + Export::Output::HtmlWriter writer; + writer.produceTestExample(psDownloadPath(), Export::View::PrepareEnvironment()); + crl::on_main([] { App::quit(); }); + _ptsWaiter.setRequesting(true); updateScrollColors(); setupConnectingWidget(); diff --git a/Telegram/gyp/Telegram.gyp b/Telegram/gyp/Telegram.gyp index 65917d9b5..d22fa5de8 100644 --- a/Telegram/gyp/Telegram.gyp +++ b/Telegram/gyp/Telegram.gyp @@ -109,7 +109,9 @@ '<@(style_files)', '