/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "export/view/export_view_settings.h" #include "export/output/export_output_abstract.h" #include "export/view/export_view_panel_controller.h" #include "lang/lang_keys.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/continuous_sliders.h" #include "ui/wrap/vertical_layout.h" #include "ui/wrap/padding_wrap.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/fade_wrap.h" #include "ui/text/text_utilities.h" #include "platform/platform_specific.h" #include "core/file_utilities.h" #include "boxes/calendar_box.h" #include "base/unixtime.h" #include "main/main_session.h" #include "styles/style_widgets.h" #include "styles/style_export.h" #include "styles/style_layers.h" namespace Export { namespace View { namespace { constexpr auto kMegabyte = 1024 * 1024; PeerId ReadPeerId(const MTPInputPeer &data) { return data.match([](const MTPDinputPeerUser &data) { return peerFromUser(data.vuser_id().v); }, [](const MTPDinputPeerUserFromMessage &data) { return peerFromUser(data.vuser_id().v); }, [](const MTPDinputPeerChat &data) { return peerFromChat(data.vchat_id().v); }, [](const MTPDinputPeerChannel &data) { return peerFromChannel(data.vchannel_id().v); }, [](const MTPDinputPeerChannelFromMessage &data) { return peerFromChannel(data.vchannel_id().v); }, [](const MTPDinputPeerSelf &data) { return Auth().userPeerId(); }, [](const MTPDinputPeerEmpty &data) { return PeerId(0); }); } } // namespace int SizeLimitByIndex(int index) { Expects(index >= 0 && index < kSizeValueCount); index += 1; const auto megabytes = [&] { if (index <= 10) { return index; } else if (index <= 30) { return 10 + (index - 10) * 2; } else if (index <= 40) { return 50 + (index - 30) * 5; } else if (index <= 60) { return 100 + (index - 40) * 10; } else if (index <= 70) { return 300 + (index - 60) * 20; } else { return 500 + (index - 70) * 100; } }(); return megabytes * kMegabyte; } SettingsWidget::SettingsWidget(QWidget *parent, Settings data) : RpWidget(parent) , _singlePeerId(ReadPeerId(data.singlePeer)) , _internal_data(std::move(data)) { ResolveSettings(_internal_data); setupContent(); } const Settings &SettingsWidget::readData() const { return _internal_data; } template void SettingsWidget::changeData(Callback &&callback) { callback(_internal_data); _changes.fire_copy(_internal_data); } void SettingsWidget::setupContent() { const auto scroll = Ui::CreateChild( this, st::boxScroll); const auto wrap = scroll->setOwnedWidget( object_ptr( scroll, object_ptr(scroll))); const auto content = static_cast(wrap->entity()); const auto buttons = setupButtons(scroll, wrap); setupOptions(content); setupPathAndFormat(content); sizeValue( ) | rpl::start_with_next([=](QSize size) { scroll->resize(size.width(), size.height() - buttons->height()); wrap->resizeToWidth(size.width()); content->resizeToWidth(size.width()); }, lifetime()); } void SettingsWidget::setupOptions(not_null container) { if (!_singlePeerId) { setupFullExportOptions(container); } setupMediaOptions(container); if (!_singlePeerId) { setupOtherOptions(container); } } void SettingsWidget::setupFullExportOptions( not_null container) { addOptionWithAbout( container, tr::lng_export_option_info(tr::now), Type::PersonalInfo | Type::Userpics, tr::lng_export_option_info_about(tr::now)); addOptionWithAbout( container, tr::lng_export_option_contacts(tr::now), Type::Contacts, tr::lng_export_option_contacts_about(tr::now)); addHeader(container, tr::lng_export_header_chats(tr::now)); addOption( container, tr::lng_export_option_personal_chats(tr::now), Type::PersonalChats); addOption( container, tr::lng_export_option_bot_chats(tr::now), Type::BotChats); addChatOption( container, tr::lng_export_option_private_groups(tr::now), Type::PrivateGroups); addChatOption( container, tr::lng_export_option_private_channels(tr::now), Type::PrivateChannels); addChatOption( container, tr::lng_export_option_public_groups(tr::now), Type::PublicGroups); addChatOption( container, tr::lng_export_option_public_channels(tr::now), Type::PublicChannels); } void SettingsWidget::setupMediaOptions( not_null container) { if (_singlePeerId != 0) { addMediaOptions(container); return; } const auto mediaWrap = container->add( object_ptr>( container, object_ptr(container))); const auto media = mediaWrap->entity(); addHeader(media, tr::lng_export_header_media(tr::now)); addMediaOptions(media); value() | rpl::map([](const Settings &data) { return data.types; }) | rpl::distinct_until_changed( ) | rpl::start_with_next([=](Settings::Types types) { mediaWrap->toggle((types & (Type::PersonalChats | Type::BotChats | Type::PrivateGroups | Type::PrivateChannels | Type::PublicGroups | Type::PublicChannels)) != 0, anim::type::normal); }, mediaWrap->lifetime()); widthValue( ) | rpl::start_with_next([=](int width) { mediaWrap->resizeToWidth(width); }, mediaWrap->lifetime()); } void SettingsWidget::setupOtherOptions( not_null container) { addHeader(container, tr::lng_export_header_other(tr::now)); addOptionWithAbout( container, tr::lng_export_option_sessions(tr::now), Type::Sessions, tr::lng_export_option_sessions_about(tr::now)); addOptionWithAbout( container, tr::lng_export_option_other(tr::now), Type::OtherData, tr::lng_export_option_other_about(tr::now)); } void SettingsWidget::setupPathAndFormat( not_null container) { if (_singlePeerId != 0) { addLocationLabel(container); addLimitsLabel(container); return; } const auto formatGroup = std::make_shared>( readData().format); formatGroup->setChangedCallback([=](Format format) { changeData([&](Settings &data) { data.format = format; }); }); const auto addFormatOption = [&](QString label, Format format) { const auto radio = container->add( object_ptr>( container, formatGroup, format, label, st::defaultBoxCheckbox), st::exportSettingPadding); }; addHeader(container, tr::lng_export_header_format(tr::now)); addLocationLabel(container); addFormatOption(tr::lng_export_option_html(tr::now), Format::Html); addFormatOption(tr::lng_export_option_json(tr::now), Format::Json); } void SettingsWidget::addLocationLabel( not_null container) { #ifndef OS_MAC_STORE auto pathLink = value() | rpl::map([](const Settings &data) { return data.path; }) | rpl::distinct_until_changed( ) | rpl::map([](const QString &path) { const auto text = IsDefaultPath(path) ? QString("Downloads/Telegram Desktop") : path; return Ui::Text::Link( QDir::toNativeSeparators(text), QString("internal:edit_export_path")); }); const auto label = container->add( object_ptr( container, tr::lng_export_option_location( lt_path, std::move(pathLink), Ui::Text::WithEntities), st::exportLocationLabel), st::exportLocationPadding); label->setClickHandlerFilter([=](auto&&...) { chooseFolder(); return false; }); #endif // OS_MAC_STORE } void SettingsWidget::addLimitsLabel( not_null container) { auto fromLink = value() | rpl::map([](const Settings &data) { return data.singlePeerFrom; }) | rpl::distinct_until_changed( ) | rpl::map([](TimeId from) { return (from ? rpl::single(langDayOfMonthFull( base::unixtime::parse(from).date())) : tr::lng_export_beginning() ) | Ui::Text::ToLink(qsl("internal:edit_from")); }) | rpl::flatten_latest(); auto tillLink = value() | rpl::map([](const Settings &data) { return data.singlePeerTill; }) | rpl::distinct_until_changed( ) | rpl::map([](TimeId till) { return (till ? rpl::single(langDayOfMonthFull( base::unixtime::parse(till).date())) : tr::lng_export_end() ) | Ui::Text::ToLink(qsl("internal:edit_till")); }) | rpl::flatten_latest(); auto datesText = tr::lng_export_limits( lt_from, std::move(fromLink), lt_till, std::move(tillLink), Ui::Text::WithEntities ) | rpl::after_next([=] { container->resizeToWidth(container->width()); }); const auto label = container->add( object_ptr( container, std::move(datesText), st::exportLocationLabel), st::exportLimitsPadding); label->setClickHandlerFilter([=]( const ClickHandlerPtr &handler, Qt::MouseButton) { const auto url = handler->dragText(); if (url == qstr("internal:edit_from")) { const auto done = [=](TimeId limit) { changeData([&](Settings &settings) { settings.singlePeerFrom = limit; }); }; editDateLimit( readData().singlePeerFrom, 0, readData().singlePeerTill, tr::lng_export_from_beginning(), done); } else if (url == qstr("internal:edit_till")) { const auto done = [=](TimeId limit) { changeData([&](Settings &settings) { settings.singlePeerTill = limit; }); }; editDateLimit( readData().singlePeerTill, readData().singlePeerFrom, 0, tr::lng_export_till_end(), done); } else { Unexpected("Click handler URL in export limits edit."); } return false; }); } void SettingsWidget::editDateLimit( TimeId current, TimeId min, TimeId max, rpl::producer resetLabel, Fn done) { Expects(_showBoxCallback != nullptr); const auto highlighted = current ? base::unixtime::parse(current).date() : max ? base::unixtime::parse(max).date() : min ? base::unixtime::parse(min).date() : QDate::currentDate(); const auto month = highlighted; const auto shared = std::make_shared>(); const auto finalize = [=](not_null box) { box->setMaxDate(max ? base::unixtime::parse(max).date() : QDate::currentDate()); box->setMinDate(min ? base::unixtime::parse(min).date() : QDate(2013, 8, 1)); // Telegram was launched in August 2013 :) box->addLeftButton(std::move(resetLabel), crl::guard(this, [=] { done(0); if (const auto weak = shared->data()) { weak->closeBox(); } })); }; const auto callback = crl::guard(this, [=](const QDate &date) { done(base::unixtime::serialize(QDateTime(date))); if (const auto weak = shared->data()) { weak->closeBox(); } }); auto box = Box( month, highlighted, callback, finalize, st::exportCalendarSizes); *shared = Ui::MakeWeak(box.data()); _showBoxCallback(std::move(box)); } not_null SettingsWidget::setupButtons( not_null scroll, not_null wrap) { using namespace rpl::mappers; const auto buttonsPadding = st::defaultBox.buttonPadding; const auto buttonsHeight = buttonsPadding.top() + st::defaultBoxButton.height + buttonsPadding.bottom(); const auto buttons = Ui::CreateChild( this, buttonsHeight); const auto topShadow = Ui::CreateChild(this); const auto bottomShadow = Ui::CreateChild(this); topShadow->toggleOn(scroll->scrollTopValue( ) | rpl::map(_1 > 0)); bottomShadow->toggleOn(rpl::combine( scroll->heightValue(), scroll->scrollTopValue(), wrap->heightValue(), _2 ) | rpl::map([=](int top) { return top < scroll->scrollTopMax(); })); value() | rpl::map([](const Settings &data) { return (data.types != Types(0)) || data.onlySinglePeer(); }) | rpl::distinct_until_changed( ) | rpl::start_with_next([=](bool canStart) { refreshButtons(buttons, canStart); topShadow->raise(); bottomShadow->raise(); }, buttons->lifetime()); sizeValue( ) | rpl::start_with_next([=](QSize size) { buttons->resizeToWidth(size.width()); buttons->moveToLeft(0, size.height() - buttons->height()); topShadow->resizeToWidth(size.width()); topShadow->moveToLeft(0, 0); bottomShadow->resizeToWidth(size.width()); bottomShadow->moveToLeft(0, buttons->y() - st::lineWidth); }, buttons->lifetime()); return buttons; } void SettingsWidget::addHeader( not_null container, const QString &text) { container->add( object_ptr( container, text, st::exportHeaderLabel), st::exportHeaderPadding); } not_null SettingsWidget::addOption( not_null container, const QString &text, Types types) { const auto checkbox = container->add( object_ptr( container, text, ((readData().types & types) == types), st::defaultBoxCheckbox), st::exportSettingPadding); checkbox->checkedChanges( ) | rpl::start_with_next([=](bool checked) { changeData([&](Settings &data) { if (checked) { data.types |= types; } else { data.types &= ~types; } }); }, checkbox->lifetime()); return checkbox; } not_null SettingsWidget::addOptionWithAbout( not_null container, const QString &text, Types types, const QString &about) { const auto result = addOption(container, text, types); const auto label = container->add( object_ptr( container, about, st::exportAboutOptionLabel), st::exportAboutOptionPadding); return result; } void SettingsWidget::addChatOption( not_null container, const QString &text, Types types) { const auto checkbox = addOption(container, text, types); const auto onlyMy = container->add( object_ptr>( container, object_ptr( container, tr::lng_export_option_only_my(tr::now), ((readData().fullChats & types) != types), st::defaultBoxCheckbox), st::exportSubSettingPadding)); onlyMy->entity()->checkedChanges( ) | rpl::start_with_next([=](bool checked) { changeData([&](Settings &data) { if (checked) { data.fullChats &= ~types; } else { data.fullChats |= types; } }); }, onlyMy->lifetime()); onlyMy->toggleOn(checkbox->checkedValue()); if (types & (Type::PublicGroups | Type::PublicChannels)) { onlyMy->entity()->setChecked(true); onlyMy->entity()->setDisabled(true); } } void SettingsWidget::addMediaOptions( not_null container) { addMediaOption( container, tr::lng_export_option_photos(tr::now), MediaType::Photo); addMediaOption( container, tr::lng_export_option_video_files(tr::now), MediaType::Video); addMediaOption( container, tr::lng_export_option_voice_messages(tr::now), MediaType::VoiceMessage); addMediaOption( container, tr::lng_export_option_video_messages(tr::now), MediaType::VideoMessage); addMediaOption( container, tr::lng_export_option_stickers(tr::now), MediaType::Sticker); addMediaOption( container, tr::lng_export_option_gifs(tr::now), MediaType::GIF); addMediaOption( container, tr::lng_export_option_files(tr::now), MediaType::File); addSizeSlider(container); } void SettingsWidget::addMediaOption( not_null container, const QString &text, MediaType type) { const auto checkbox = container->add( object_ptr( container, text, ((readData().media.types & type) == type), st::defaultBoxCheckbox), st::exportSettingPadding); checkbox->checkedChanges( ) | rpl::start_with_next([=](bool checked) { changeData([&](Settings &data) { if (checked) { data.media.types |= type; } else { data.media.types &= ~type; } }); }, checkbox->lifetime()); } void SettingsWidget::addSizeSlider( not_null container) { using namespace rpl::mappers; const auto slider = container->add( object_ptr(container, st::exportFileSizeSlider), st::exportFileSizePadding); slider->resize(st::exportFileSizeSlider.seekSize); slider->setPseudoDiscrete( kSizeValueCount, SizeLimitByIndex, readData().media.sizeLimit, [=](int limit) { changeData([&](Settings &data) { data.media.sizeLimit = limit; }); }); const auto label = Ui::CreateChild( container.get(), st::exportFileSizeLabel); value() | rpl::map([](const Settings &data) { return data.media.sizeLimit; }) | rpl::start_with_next([=](int sizeLimit) { const auto limit = sizeLimit / kMegabyte; const auto size = QString::number(limit) + " MB"; const auto text = tr::lng_export_option_size_limit(tr::now, lt_size, size); label->setText(text); }, slider->lifetime()); rpl::combine( label->widthValue(), slider->geometryValue(), _2 ) | rpl::start_with_next([=](QRect geometry) { label->moveToRight( st::exportFileSizePadding.right(), geometry.y() - label->height() - st::exportFileSizeLabelBottom); }, label->lifetime()); } void SettingsWidget::refreshButtons( not_null container, bool canStart) { container->hideChildren(); const auto children = container->children(); for (const auto child : children) { if (child->isWidgetType()) { child->deleteLater(); } } const auto start = canStart ? Ui::CreateChild( container.get(), tr::lng_export_start(), st::defaultBoxButton) : nullptr; if (start) { start->show(); _startClicks = start->clicks( ) | rpl::map([] { return rpl::empty_value(); }); container->sizeValue( ) | rpl::start_with_next([=](QSize size) { const auto right = st::defaultBox.buttonPadding.right(); const auto top = st::defaultBox.buttonPadding.top(); start->moveToRight(right, top); }, start->lifetime()); } const auto cancel = Ui::CreateChild( container.get(), tr::lng_cancel(), st::defaultBoxButton); cancel->show(); _cancelClicks = cancel->clicks( ) | rpl::map([] { return rpl::empty_value(); }); rpl::combine( container->sizeValue(), start ? start->widthValue() : rpl::single(0) ) | rpl::start_with_next([=](QSize size, int width) { const auto right = st::defaultBox.buttonPadding.right() + (width ? width + st::defaultBox.buttonPadding.left() : 0); const auto top = st::defaultBox.buttonPadding.top(); cancel->moveToRight(right, top); }, cancel->lifetime()); } void SettingsWidget::chooseFolder() { const auto callback = [=](QString &&result) { changeData([&](Settings &data) { data.path = std::move(result); data.forceSubPath = IsDefaultPath(data.path); }); }; FileDialog::GetFolder( this, tr::lng_export_folder(tr::now), readData().path, callback); } rpl::producer SettingsWidget::changes() const { return _changes.events(); } rpl::producer SettingsWidget::value() const { return rpl::single(readData()) | rpl::then(changes()); } rpl::producer<> SettingsWidget::startClicks() const { return _startClicks.value( ) | rpl::map([](Wrap &&wrap) { return std::move(wrap.value); }) | rpl::flatten_latest(); } rpl::producer<> SettingsWidget::cancelClicks() const { return _cancelClicks.value( ) | rpl::map([](Wrap &&wrap) { return std::move(wrap.value); }) | rpl::flatten_latest(); } } // namespace View } // namespace Export