From 66822f7333e82bbf32135f0ca0e0f87d5cfae2dd Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 29 Jun 2018 22:10:21 +0100 Subject: [PATCH] Add some HTML design to export, except messages. NB Testing the layout, the app is not in a working condition. --- Telegram/Resources/css/export_style.css | 3 - Telegram/Resources/export_html/css/style.css | 247 ++++ .../Resources/export_html/images/back.png | Bin 0 -> 312 bytes .../Resources/export_html/images/back@2x.png | Bin 0 -> 518 bytes .../Resources/export_html/images/calls.png | Bin 0 -> 656 bytes .../Resources/export_html/images/calls@2x.png | Bin 0 -> 1300 bytes .../Resources/export_html/images/chats.png | Bin 0 -> 283 bytes .../Resources/export_html/images/chats@2x.png | Bin 0 -> 454 bytes .../Resources/export_html/images/contacts.png | Bin 0 -> 508 bytes .../export_html/images/contacts@2x.png | Bin 0 -> 1023 bytes .../Resources/export_html/images/frequent.png | Bin 0 -> 771 bytes .../export_html/images/frequent@2x.png | Bin 0 -> 1696 bytes .../Resources/export_html/images/photos.png | Bin 0 -> 415 bytes .../export_html/images/photos@2x.png | Bin 0 -> 750 bytes .../Resources/export_html/images/sessions.png | Bin 0 -> 134 bytes .../export_html/images/sessions@2x.png | Bin 0 -> 216 bytes Telegram/Resources/export_html/images/web.png | Bin 0 -> 266 bytes .../Resources/export_html/images/web@2x.png | Bin 0 -> 447 bytes Telegram/Resources/qrc/telegram.qrc | 18 +- .../export/data/export_data_types.cpp | 97 +- .../export/data/export_data_types.h | 16 +- .../export/output/export_output_abstract.cpp | 14 +- .../export/output/export_output_abstract.h | 4 +- .../export/output/export_output_html.cpp | 1125 ++++++++++++----- .../export/output/export_output_html.h | 79 +- .../view/export_view_panel_controller.cpp | 4 +- .../view/export_view_panel_controller.h | 4 + Telegram/SourceFiles/mainwidget.cpp | 13 + Telegram/gyp/Telegram.gyp | 4 +- 29 files changed, 1280 insertions(+), 348 deletions(-) delete mode 100644 Telegram/Resources/css/export_style.css create mode 100644 Telegram/Resources/export_html/css/style.css create mode 100644 Telegram/Resources/export_html/images/back.png create mode 100644 Telegram/Resources/export_html/images/back@2x.png create mode 100644 Telegram/Resources/export_html/images/calls.png create mode 100644 Telegram/Resources/export_html/images/calls@2x.png create mode 100644 Telegram/Resources/export_html/images/chats.png create mode 100644 Telegram/Resources/export_html/images/chats@2x.png create mode 100644 Telegram/Resources/export_html/images/contacts.png create mode 100644 Telegram/Resources/export_html/images/contacts@2x.png create mode 100644 Telegram/Resources/export_html/images/frequent.png create mode 100644 Telegram/Resources/export_html/images/frequent@2x.png create mode 100644 Telegram/Resources/export_html/images/photos.png create mode 100644 Telegram/Resources/export_html/images/photos@2x.png create mode 100644 Telegram/Resources/export_html/images/sessions.png create mode 100644 Telegram/Resources/export_html/images/sessions@2x.png create mode 100644 Telegram/Resources/export_html/images/web.png create mode 100644 Telegram/Resources/export_html/images/web@2x.png 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 0000000000000000000000000000000000000000..846479f94860d2d0e8a02b239e639d66503d9d1c GIT binary patch literal 312 zcmV-80muG{P)Px#@<~KNR7eeDU>F5MDg=ruY8pV86h|`>s~BYPzyJU1f$IOVFtZx;v^88IMlTaF zijWOwWM+yX)^L#71bu*Rcz;{-3St}uQ-jw6N)5;B18NwK(*kN4j@1Gr!(qWl5Px$z)3_wRA>e5m)%OkFcg4u)`7e9b?Vx+IPeYD2l2`a!HEtPM0_9F58c%HAo>7a zyT+VBpo})@Pts(NKuKxSlkYoe+Jq2STn1bQTn1bQTn02{;PUEviif7~`>>v_;0T^k zdP50`&%((2+@wn#eQ6Qk@En*)M1C;ZZQs`^zox40s|v*99K!Ff1N?dbZ@i7l8dL@= zgYN@eJU`rhsqBl#QYK&m9wn3rn1WX#U=qHyfNA(v0ye-m7O(}rk$_F`wFPX0uO(n3 zd}V<`czn8p^7=Fr{;PU229xkIHoPwts1n3v8>zs{?9*33BqXku}Qyk*E>6{EPzC15|q+OFp7;x)DjR# z3{b+4$BXTH)#*so77$55mL*+FG!n2$qOpKXVi?AAHfMgYozBuq0KrNo_J9zYbRp4N zKqSFFxszcSEp(2A5&@ZnAD_?J*GbFkq)b30K{Cp|PI@GyYas-yQ;OG4^b~e~<2rGw zwpBU;ywk&pjf+7hHskk?rp5&z9!CT0KwSo023!VQ297QR|4jrMoBfUKMF0Q*07*qo IM6N<$g25=`O#lD@ literal 0 HcmV?d00001 diff --git a/Telegram/Resources/export_html/images/calls.png b/Telegram/Resources/export_html/images/calls.png new file mode 100644 index 0000000000000000000000000000000000000000..adb42e2f5874c4d4a4a221d9d3b0f50253cc39ee GIT binary patch literal 656 zcmV;B0&o3^P)Px%N=ZaPR7ef&RLyG>K@@*)Hqos~6@&`*Xs%-QQc%I5P%PSmUl(tJccE&vA680D z+tjAE{tYQTE4VvAn%=%p%zvL?03Jl|V)N@g1pHs=m3@9Xz|zumVp1F*ZA zOaN=^{r5*+62L{16e2+$YU+N`D&sa2np;CQR#(q{?q`+k=s5_N;4fBHV+vAZ)f3GrK5mVkr! z$|oVXrYnID@Hy#uo2`&bPpq~P4<5+luVR^qPWgF+RTua5jExOfYH?9~>kD!N10E49 z{3pO~ZJ0PWH8s2*g)oe@1f-}C$Jr3?s6%C?_k`Bi+M!x{8kI1N z-yt9XbZCSRSdqtdwSVxzop4i>*r^9zE);3~%6f1A7leEBj8#rjz=6+-3rxB!TXy-oM1H-5ZvtK_-hq|F5X?wm>|hCsi428t8`f- zxa9232RX|$9g&6R;P&9?2~G465qf!SZn!S-{Pe^&@3vD9*eE9#ApI_$*BUPHsZ8;~ q?Bv_HlP}7*{15sX_3qZJ3H$>U5xIuKC{Px(&PhZ;RA>e5SzTycMHHShcSGCMY6ER0_*1qmRPBRkE2eAHR4VubdD5<+5Bi`) z9}G>RX4~DArPz7gn7!-6URci0oSE~T`8j9qUB;?Ubq1<4P@REF&H$O5cN{Ok)J0HwHk&w)p`dgGeTmwSH{NVPgzHFmcnb z!)%sq@oT%*cE$kUs33s#R1+TRO_TgFMl(8)JlR`yIY{0dfMC~jAh%e4hmx_qepCDc z<>YI-74%sn!59EB`c?PAiS1M}w%29FOX}G6Q@R`^Zwx>Xo7RB{hujt5L{oYjBWfy; z0~Cw_a6v!oI0T6XB_o&##Y-v}YZw_BsnK;o@x}lIS(7>loZQI!eSI6H4*cVK8wx`0 z^yRDJ0$o4?u$Z5=SmJk?=aq$JSuZ)Zs1wXN4V|U*807jox+mPzbL=mr3yK<7Kmx$l z_+AHss@r`Rc%r=E^j0OnOKc?rz+U-U4*;vN7xL1~13WSD+R&-gSGrx0e8~WI)Nexo zR8S8TI9%lNjMM7_J?XPbR|#4&fY#R5+W_^Y7nlM+nCU*_Z3|j18Gr<15q;_fg_Iiq zKY*9GmJOi4r~60PeWM3K4m*My=(2tFKIP(lfxxLv=>U+O_Fc>7-_>=M;!D;69TaN9 zIb@+1luqXU4OZ>$OeQ|3bd{jKR^Se>(0icrP%k`Kh5NF0c38=kqHa!K4#4F2=%4!! z9;(5$p-Hpd6KQPv=i>O-xTXt~UBL=47Ovl#!PdE?BXopFx5wf~bX}1AiUN>zAyh+A zT%T`fAW1Mf-5yW6w^gmJ?5yuYtRrcUop{+1wyeNzf4l(AQz+CtkWLodV)cGrJ{CJw zm!sSDPe1Y4bTk^Zz3qNv7l0pQ-3D2y4OwYG*P%k+!u8?}KSl27vBWFnh|iG>HUh*= z&UEg&p=VIM%(28^Z#v`AyJFk% zt|)WF7np#Mt28ip|DNUCdIulRzAa|rI|~)-eDT8AcaZ~!YVnTzjMgqE9?mMj zD;Mm}=NwmbnF5Edb`{LR!Dh~9s1x^kyTi#3g0r(EDDBxcHfQ`zY?GgCAtDHWA8f;z z@$Gn>dRQN)>;Xt;A0!ex^Eq(_2l7eXxq-LwsYbp}+;v@-_D4J87-s1)1+bzzuryq8 zt}m6E2|sXugg8G53-{Vl)SBoLaqEWqP*BvyhjPufkk?mREV+xZ{73o)&}*|O=a7D7 z?U+JEoFR)N>o~KyocKkzH?2U~0#MF7I^zdWp%RdvSGv{EiUpv+cEq~&;-J0_`Bfay z%}Vc=Ry+U&ba;4p-R#vrn(@u18O#4HwwU|fN!fTQw%C=yo>W0rQZ{AZLdube%Ckbk zZg5CEf-|riMR6C>sa&B=xL<7oovZZUSSIXMeRT$^Gf8)`lB2S4BVSDSXczT{MvqQ z->^t;LgT~r>^=TEyl0N`^z`)9Pq>=A^}+xD`&T!1&6p@EbeiqfM&3x>Fm8u7V?#r~ zzyIfd6xx*Jl;hS`|Nf^%gqhOn!&!{N%E`&`)!xjToQiHrNb=@Xu$S^2F9->A^<{2* fu!3Q&GXulx7(=yGfycE#4>EYV`njxgN@xNAPx$fJsC_RA>e5n9WK9K@7)}x|g0j_Tp3Y;!*I4{6Z~lVZ|1|p!zyq1o7U3@8DT) zD=fP)Qx$A!#*r*LBV=G{o0(+)|6j6)Er2Z}U<8anOakb;`Tk(M14L84`UKGRoo#xL zg%yA}=WBR-asDXP7Y%%ge;}fcl-x!f5o%mVmHS+U>#5run|%MRHs`9 z)CiRYtOH7b>U8UX8lkd)bwCMFoo*daBUBdn_Z<-5iPwQS&hxG6z3Q#oYdTg@9aJ*d zz|mm5T^8j%zhG|$Rc`bUzDebRN2sdnNaBJ2*PH4>pjh_#gu* wi~rR?>f*iQe{ed=y$5X52p9n)U<8=J2mXLx*PqFGBme*a07*qoM6N<$f=$BBQ2+n{ literal 0 HcmV?d00001 diff --git a/Telegram/Resources/export_html/images/contacts.png b/Telegram/Resources/export_html/images/contacts.png new file mode 100644 index 0000000000000000000000000000000000000000..1b94b8ee3d8217b5ede47b78404ba79a15231001 GIT binary patch literal 508 zcmVPx$wn;=mR7ef&RIy7!VGzH|Ptie;Xeh9)F=}foA_#GeAm|@xj07=3sE;5JsoeVm zf?8YxaTB7Ytq5A$YG{g3!O*AIo%-NI-}6- z8{bwAf!5_d0hR+nmC9-T?uGXh5b|c-dL*D}lMDer{UNpNB{F4#oP4RgM_vqs{PS}7 zUQuF_bHbQ0PXW6vOuQ%6$`M~k?3_v1?|TaL1OED~yZ|y#9_wzS-n`^_bSf8LaNXQ+ z22TOm79Mhpdmd-vH=C|AWX{>oZr10Pu;cgTa%F9EQ}ca9R(5@y#g)kj0soWfM%Qp z@4%;$`BVL1#>F2HNTiRp0NOW9&I^EFe4&6yC8h8Q!r|_xp<0bv7A+AW4RfAGGXXvSg{_=x&hJ;gtz?_%7?81CQ16bjIXKLa y9OtZi)D1WA4{VD6h}c-uj{H+$icS3Sbl?YHl&1>|Oq5Rm0000Px&xk*GpRA>e5S4&7#Q5Zh|o$DjP2N5D=?V^w%sc2PFVxhZQ*rLEqt0;PvW!D5~&(pR^1ppHPV5eE2Xe<3CWCjzm55-Wdtjk3< za|x1T=!3P3GPq&Zi~6yFt#Mih1iD_P>(!^qs841|`3&MX7Msv55dWZRE%R zi1y)B@Ch^N$R_(bV`ZDJ?3zIGs7w}&8%43C?EH1woUu-SowaKMIz|4ak81*SNyz{M z_Kzd-T9%zQXRPe2E4wB@H>_MNF5n7&u$yHa$)qEjtarxBHeK2EDMaHz+_onQb7%Ke+ZN|nE27dH23(8w_FPce zveCKTLR5@q8yXLE3zIL!==!Jg3#2pZ+G4R-yuPmX-jXeF@52F%Q@&Z+HrV;=I0rR( zF$WPoWg%VQD!;WJr&Sny-CB0QlP%QBdAV4$4%vbo#It=DCfzAOlrzCf5@dkQf`BQ( z*e9HSJ{rJYLxK5fkn<_GWOF9a(#flYuucgk8WD6=ENW!$Tle0 z(T)G>vM|oWHQC0d^#>TmZbpXtdQxuov@vMZegna9&o*Yw-$w99GbMFnPuwSg!lvO! z_g9@fS_E1`;ToI4L)7z^*m@k2Sv;I^7Jm^eAq9~q_ibWcVC&EfENVZmDjzeSPgIT-9tzFqvN zzrSNfVP91h3iXvP8FMFu&Ca0HF{dMv_y(rf7mY-|IBoJ;*#X%B!AG6ovExrn!|WWm z%*SNkY~&TfBN#K%*2SwW+v>$PQlHF`sEM_4-I&y=elrrl2{NTS2)-RZv;kGJTy-mr zXut!a4N3eSxp7<8Wy+axn8PHcauTn>r_U|QWV=evNI(^|o^%>pC>8FdP#9mz`g#8Q tH{cUkKby%%;&;IBfZqYX0~@gezW|+wR#Yl9LplHe002ovPDHLkV1m&U=*$2B literal 0 HcmV?d00001 diff --git a/Telegram/Resources/export_html/images/frequent.png b/Telegram/Resources/export_html/images/frequent.png new file mode 100644 index 0000000000000000000000000000000000000000..1656019951c0f227c872f335f5fdc83257552750 GIT binary patch literal 771 zcmV+e1N{7nP)Px%y-7qtR7ef&R82@!Q4~Jsy*E0bs2iuqf}%yMb`pV_v}~8FASj3^BrBR!X3QCR zIsVLGPK&Y@L2X>MXjxkYO9(1gwG09arbUhfA}6JYx|y{h#*AE24{#N4h_q?Nsu0mtL9<#5xm>bX z&$m6Icw#)Mh=)*VCqT{hK+I*cBWiSsf1_-#lTvN<7>HKTU4cn0+|5mACB7|y!uaM` zItM#iO@to52Dm_qk;F^+TxL;X@kIJ8vD=T77#Q|I?0Ag}B6IoKOac~7`vl}eRy>&= z#1QV|r~|av9yKmvWEC%1_Xqwabn^%T=Mi!=;%oWb)I&dPy92*-_}0WJ$910~u@xfA z&D1iG&5nKZV?f?fELqP`=1Fw=U1K_vpPhQ{$H+PG70u5~zKCc>9~%6Ok@Z)2D^s6X zEL*pQ9IxsNDnAiK-cUdSb2H`|#`sdsAbKbEx5d0x{eG3tX8w#| zxY(n_YQf7>JY?u<#d7iGVXABaCU8W88%d=OA$uP#X{%zXL6E=2xcX$v*|O9XnY|*l zcQlnc9Kh|l8VcBs>JEI{BL(NfuEj(7o$6RCzb#J002ovPDHLkV1f#7 BWFG(k literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ca9c3c85bc9e94ea1ac7f3871b545d16010f94d3 GIT binary patch literal 1696 zcmV;R24DG!P)Px*T1iAfRA>e5SzBxrRTw^JW|r=jm=>|yC}03%A|h9fa?uBa@PJ4pA&3u#fES2L z!nPE<+b#@jyDg<7JRL zr910x;=@idng9RKfBygbFXz%NA(f|+fl3C-mVr=<`XGPH<^YD3avCRw{xa-Syw8-& z4>w+xXQQeH^^|sw;*tHzgl<($mcx-$4x`+rnbx1jr;0H)%1CK1l*><9Dxj*W(woxH z5W>cD;FOXJHGbbj$n{bOQkDwz4(d-@;5lH%$hiKrwyf0PxxKOkKp0u?1j`7`kvVmW z;Y;2^8zbs;-O#>w10xY>=FaUg*D*K!LS!YcfHAany8EUk-&RZdl(YiP%^hRXWZ*oS zzS<2XoTW&>H$I`N*WAX7k(IOpgJfWh9X#ddz*!3zBP2l5`m+{aif_pRz!nKx>_rh2 zCG2%Z*=wSVL1#!;lon5yEP(z&eKEGn$rco3J}m`&ZwJWey_~k>*r6sucgZmSABlq2 z08UfduyyMgpRU(w1`Se1gUDnZVP=p@piNST_LzeT+&PSgl6Q7>wy);=hUS*N0KR5; zjL`FxvSTotlqgAJi%nufPs%1sGO8p=6-nMg$Td{T%+7JAasmi7N7tc`AHfxjgA78U z4rgf{g79|nlYRH^imNBM+op&*Q!})a?hx`y4Pdx}WZ@3Q1EWbw=+}wP*c;-EoEEAg z)gNQxH~@YOS(}cwxZN{&N{?9Jxw9Q{^*glDVbO5`v<(0CNa8e9cLDUrWw1FW<^<5$ z8UC}zuPl-%{m`LXFbmJSoyu6yIWnhCJ|3ChSL;_6a%@hWoE8!lLg8qPnP#gfyFqI( z`T>#XC4#<{C`v5JNoKI55|A%v zuCmVnM`?SaBNlbD$8~QS09OcwTURra?E&fIy!F7~L3wP=%I>D70ZUfQ7YntzH?8l- zx?3vFhkYIB)+IXH_KI>RJz@ZSLPKQp0%jPWBBeE=0Zcx@(@rGN(}R+(*F`umEk`v17DxwRb-j-4m89V+cJf5jwA@Gj>drJL!hz z=qxPSqX>MIIEKU=!tVDJ*2HyDJ`!DU1uR2+(axPcW0@q;S1lR;^0R^}?20IJqnTWl zbb?17_spF=eW_RQ+`i`k_@ospRv1iD-s5!kp%=T`+RkKocPi1|dI8x!Ya5m5``o5Y zRv=SU0Nge;wTDF?=*4eEp3?^o9FY0zl;`T2^PM_{@u%%cEApfIjiaz&%=6N1C6SB8-A?4oW~v#PCvqxqp?d>FN!*wRP_!m zrT5?zUMb2&S_$~-z5a^rc^5KM`ASWHmSpLro$+m7iJ&a{MnF&F9`vSZWY)?QYs~r@ z>5T2qEWVJh=N7^rAh!NT2?ubVzcOX=gc{Dd_1iIT(Px$SxH1eR7eeDU>F4iLVyujzPPH+^Y7n(vltlu%OHyr%VuO`e9Xkel-}Fcv>m30 z3B~}?lo$@u|NsAg*?<53%>=PwfCa{Y&=8kT=xjyt8LArGyn^y-pdJ5ZkyW7h09k@s z>`@B{hUDN4qE)L_F%xt?Pyty!D5$88K7RJnY^qoQ%H2hkHGVL|N~#)k7#J95{b%@} zSXfyd4->~lzyFr}`_HfxWN<@61K;0&zt;k@EEmYY|Nj}M6<0QB;W8W~ zj?)65VH`jI|6U7psSwa4p1*(pZ2I{9_p<;08Pwng0xkOY|MzO3TCjz1aU=##3qF4T zGvz-6gAq(E&>}@3-y6n<(Lf8eKK%GS6~;%W(aIu(bbn_{3XliF6akz*pvY7t&1gPg zd;+QqK&2v*8j_fxIssV&iVv8XnbLqs5NHW`09N6Ur+dWe0swd{bk?u~B7y(_002ov JPDHLkV1nn+wKf0% literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..587b4ff8cdc20b691523b18d08171fe197b8f01e GIT binary patch literal 750 zcmVPx%s7XXYRA>e5SDfc@;tzQA;ABH)CbPQ? zl8A9(<3V%DAs6+a1oR1v;0yQ=Mie#}1$V;yl+_CZoiVlSOmAlQ&<)F0S9f)NUsd(& zPB(yQCITh`CIW+ufavDj@|Mm(6g&rkHxS`;x4Hcv6WauC3r5ElR+p>axb;~A5r<=Iq+teUl1yv!h z6X;9o5JIv>S-Gg{ipW|7eBU2|AbNv{=Q?jqaqux)-1RTddpTx;)a&+K}K9$FlcJlW#ZP&BCfI$>% z71y8YWo~v(ZUR)ZV|y6V69p%;iz$5~hP`i4#EAsMLi-^%f!%iWk|KOX0oxam9s*NE z_5*t^#GzVUS$arg@-q}IcLs(6wF1HrP=YZKk=+AAz<2dbQa!6IAF0^peDWoP;uB|7 zww|8JTbxgx0Nsqx&s2*uEKM&lNGwca3CJ4}c{3tFyp`p|*dLl&`t11bayxTq&dtIm g0ww|`0zF6IAIYED1vK=)hyVZp07*qoM6N<$g4j1tQUCw| literal 0 HcmV?d00001 diff --git a/Telegram/Resources/export_html/images/sessions.png b/Telegram/Resources/export_html/images/sessions.png new file mode 100644 index 0000000000000000000000000000000000000000..0354af4f534c6123e0016a9dc6e1b5f309396f7d GIT binary patch literal 134 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjjKx9jP7LeL$-D$|96VhdLp*qs z6C_v{D;m6TZV74HXn#JJh>gTe~DWM4f5+x=> literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..58b213e845bdb996cdbe33cc45db247acbad016e GIT binary patch literal 216 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC#^NA%Cx&(BWL^R}lRRALG#&z!&k0V2skh>vOtIeP9Oh+r+3Bf zER(C=9`?HOZO6`M?#E;%uTo}TQC0j#x^X>6$zEG|uityVC9Zv5?taG9-Y8?S;zOy^ z_ZztTVp#4e9jHH0EBy3!#!u!Q`x!n7!j1l?Q^0&QS3*oCynYqXbqt=aelF{r5}E+p CrB53G literal 0 HcmV?d00001 diff --git a/Telegram/Resources/export_html/images/web.png b/Telegram/Resources/export_html/images/web.png new file mode 100644 index 0000000000000000000000000000000000000000..d82e7fb31ec9ee43583866d9522782430d9211d4 GIT binary patch literal 266 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjjKx9jP7LeL$-D$|j(EB_hIsHM zCrGfeI5h~W#K(j_|M~s>{zx94|EU}+)V*d+ocPgDSU9*UKl*Z++62BVDL$k3|NsBr z^#9-A*_=t68#rd1RxjDsRLdf$JYPp+J=?91@nR`VH`t#@yGE#=)ooxCT;=T$$I{u* z#BPx$c}YY;RA>e5Sg}q6F$}e#1I(QeAJJccKY|5@GSZ>#0AImxz%TF-v=cKOTAA~y zFBe5R<*Fk}f_#flcFuWze#uEDvd0)O28@B646wiX{rvfw_PZ6a+=|eZAItyFn+EI$ zO)ef@7EfQm;|fsla`^cqp(%nzTBdJ4znr0nGk}!CZe2Wh1*-(BN{){!fJtuPEZ#N} z2gV6rLe0M}7fxPSlhfWk&ISAXSX~DYtj$^hxjSYZ$lU>V3&nu=w}7t2EfxA>d4|T3 zKepZ^6k{lLy!s9dJ_CK4?DMB)9{H>9fE9pN=CXs&KzQFn`=S4@3PJu@K9f+4q15r} zJ23bR^l7rspPG5(uf79T09u*LS_g75kA2-O#7tX%-jeDMi;c%w-r)OALc;HGmTuc2 z?-68IKr{8FW^h_M_!gsL4GYj|uTFiL_)X=61*KTU)ME1{(~52M{B!JC8T%*iBvcC3 pC*rUIP9)5h8w19GG4RhA_yz1)SIXIe#jOAU002ovPDHLkV1kLA$_fAg literal 0 HcmV?d00001 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)', '