diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 497e13b170..b6dddc1c2c 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -971,6 +971,8 @@ PRIVATE media/view/media_view_playback_progress.cpp media/view/media_view_playback_progress.h media/view/media_view_open_common.h + menu/menu_item_download_files.cpp + menu/menu_item_download_files.h menu/menu_mute.cpp menu/menu_mute.h menu/menu_send.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index ed5b9a8186..5ca48ca666 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2211,6 +2211,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_send_now_selected" = "Send selected now"; "lng_context_reschedule_selected" = "Reschedule Selected"; "lng_context_delete_selected" = "Delete Selected"; +"lng_context_save_images_selected" = "Save Selected"; +"lng_context_save_documents_selected" = "Download Selected"; "lng_context_clear_selection" = "Clear Selection"; "lng_context_seen_loading" = "Loading..."; "lng_context_seen_text#one" = "{count} Seen"; @@ -2391,6 +2393,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_mediaview_report_profile_photo" = "Report"; "lng_mediaview_saved_to" = "Image was saved to your {downloads} folder"; +"lng_mediaview_saved_images_to" = "Images was saved to your {downloads} folder"; "lng_mediaview_downloads" = "Downloads"; "lng_mediaview_video_loading" = "Loading - {percent}"; "lng_mediaview_playback_speed" = "Playback speed: {speed}"; diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 536311edee..7126a9eada 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -63,6 +63,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "layout/layout_selection.h" #include "main/main_session.h" #include "main/main_session_settings.h" +#include "menu/menu_item_download_files.h" #include "core/application.h" #include "apiwrap.h" #include "api/api_attached_stickers.h" @@ -2236,10 +2237,12 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { &st::menuIconCopy); } addItemActions(item, item); - if (lnkPhoto) { - addPhotoActions(lnkPhoto, item); - } else { - addDocumentActions(lnkDocument, item); + if (!selectedState.count) { + if (lnkPhoto) { + addPhotoActions(lnkPhoto, item); + } else { + addDocumentActions(lnkDocument, item); + } } if (item && item->hasDirectLink() && isUponSelected != 2 && isUponSelected != -2) { _menu->addAction(item->history()->peer->isMegagroup() ? tr::lng_context_copy_message_link(tr::now) : tr::lng_context_copy_post_link(tr::now), [=] { @@ -2257,6 +2260,9 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { _widget->confirmDeleteSelected(); }, &st::menuIconDelete); } + if (selectedState.count > 0) { + Menu::AddDownloadFilesAction(_menu, controller, _selected, this); + } _menu->addAction(tr::lng_context_clear_selection(tr::now), [=] { _widget->clearSelected(); }, &st::menuIconSelect); @@ -2402,6 +2408,9 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { _widget->confirmDeleteSelected(); }, &st::menuIconDelete); } + if (selectedState.count > 0) { + Menu::AddDownloadFilesAction(_menu, controller, _selected, this); + } _menu->addAction(tr::lng_context_clear_selection(tr::now), [=] { _widget->clearSelected(); }, &st::menuIconSelect); diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index 0a2c1af3c1..cb68391996 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -34,6 +34,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/controls/who_reacted_context_action.h" #include "ui/boxes/report_box.h" #include "ui/ui_utility.h" +#include "menu/menu_item_download_files.h" #include "menu/menu_send.h" #include "ui/boxes/confirm_box.h" #include "boxes/delete_messages_box.h" @@ -803,6 +804,20 @@ void AddDeleteAction( } } +void AddDownloadFilesAction( + not_null menu, + const ContextMenuRequest &request, + not_null list) { + if (!request.overSelection || request.selectedItems.empty()) { + return; + } + Menu::AddDownloadFilesAction( + menu, + request.navigation->parentController(), + request.selectedItems, + list); +} + void AddReportAction( not_null menu, const ContextMenuRequest &request, @@ -901,6 +916,7 @@ void AddMessageActions( AddForwardAction(menu, request, list); AddSendNowAction(menu, request, list); AddDeleteAction(menu, request, list); + AddDownloadFilesAction(menu, request, list); AddReportAction(menu, request, list); AddSelectionAction(menu, request, list); AddRescheduleAction(menu, request, list); @@ -969,7 +985,7 @@ base::unique_qptr FillContextMenu( } AddTopMessageActions(result, request, list); - if (lnkPhoto) { + if (lnkPhoto && request.selectedItems.empty()) { AddPhotoActions(result, lnkPhoto, item, list); } else if (lnkDocument) { AddDocumentActions(result, lnkDocument, item, list); diff --git a/Telegram/SourceFiles/menu/menu_item_download_files.cpp b/Telegram/SourceFiles/menu/menu_item_download_files.cpp new file mode 100644 index 0000000000..a0a5571e2c --- /dev/null +++ b/Telegram/SourceFiles/menu/menu_item_download_files.cpp @@ -0,0 +1,185 @@ +/* +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 "menu/menu_item_download_files.h" + +#include "core/application.h" +#include "core/core_settings.h" +#include "core/file_utilities.h" +#include "data/data_document.h" +#include "data/data_document_media.h" +#include "data/data_file_click_handler.h" +#include "data/data_photo.h" +#include "data/data_photo_media.h" +#include "data/data_session.h" +#include "history/history_inner_widget.h" +#include "history/history_item.h" +#include "history/view/history_view_list_widget.h" // HistoryView::SelectedItem. +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "storage/storage_account.h" +#include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" +#include "ui/widgets/popup_menu.h" +#include "window/window_session_controller.h" +#include "styles/style_menu_icons.h" +#include "styles/style_widgets.h" + +namespace Menu { +namespace { + +using DocumentViewPtr = std::shared_ptr; +using Documents = std::vector>; +using Photos = std::vector>; + +[[nodiscard]] bool Added( + HistoryItem *item, + Documents &documents, + Photos &photos) { + if (item && !item->forbidsForward()) { + if (const auto media = item->media()) { + if (const auto photo = media->photo()) { + if (const auto view = photo->activeMediaView()) { + if (view->loaded()) { + photos.push_back(view); + return true; + } + } + } else if (const auto document = media->document()) { + if (const auto view = document->activeMediaView()) { + if (!view->loaded()) { + documents.emplace_back(view, item->fullId()); + return true; + } + } + } + } + } + return false; +} + +void AddAction( + not_null menu, + not_null controller, + Documents &&documents, + Photos &&photos, + Fn callback) { + const auto text = documents.empty() + ? tr::lng_context_save_images_selected(tr::now) + : tr::lng_context_save_documents_selected(tr::now); + const auto icon = documents.empty() + ? &st::menuIconSaveImage + : &st::menuIconDownload; + const auto showToast = documents.empty(); + + const auto saveImages = [=] { + const auto session = &controller->session(); + const auto downloadPath = Core::App().settings().downloadPath(); + const auto path = downloadPath.isEmpty() + ? File::DefaultDownloadPath(session) + : (downloadPath == u"tmp"_q) + ? session->local().tempDirectory() + : downloadPath; + + const auto fullPath = [&](int i) { + return filedialogDefaultName( + u"photo_"_q + QString::number(i), + u".jpg"_q, + path); + }; + auto lastPath = QString(); + for (auto i = 0; i < photos.size(); i++) { + lastPath = fullPath(i + 1); + photos[i]->saveToFile(lastPath); + } + + if (showToast) { + const auto filter = [lastPath](const auto ...) { + File::ShowInFolder(lastPath); + return false; + }; + const auto config = Ui::Toast::Config{ + .text = (photos.size() > 1 + ? tr::lng_mediaview_saved_images_to + : tr::lng_mediaview_saved_to)( + tr::now, + lt_downloads, + Ui::Text::Link( + tr::lng_mediaview_downloads(tr::now), + "internal:show_saved_message"), + Ui::Text::WithEntities), + .st = &st::defaultToast, + .filter = filter, + }; + Ui::Toast::Show(Window::Show(controller).toastParent(), config); + } + }; + const auto saveDocuments = [=] { + for (const auto &pair : documents) { + DocumentSaveClickHandler::Save(pair.second, pair.first->owner()); + } + }; + + menu->addAction(text, [=] { + saveImages(); + saveDocuments(); + callback(); + }, icon); +} + +} // namespace + +void AddDownloadFilesAction( + not_null menu, + not_null window, + const std::vector &selectedItems, + not_null list) { + if (selectedItems.empty() || Core::App().settings().askDownloadPath()) { + return; + } + auto docs = Documents(); + auto photos = Photos(); + for (const auto &selectedItem : selectedItems) { + const auto &id = selectedItem.msgId; + const auto item = window->session().data().message(id); + + if (!Added(item, docs, photos)) { + return; + } + } + const auto done = [weak = Ui::MakeWeak(list)] { + if (const auto strong = weak.data()) { + strong->cancelSelection(); + } + }; + AddAction(menu, window, std::move(docs), std::move(photos), done); +} + +void AddDownloadFilesAction( + not_null menu, + not_null window, + const std::map> &items, + not_null list) { + if (items.empty() || Core::App().settings().askDownloadPath()) { + return; + } + auto docs = Documents(); + auto photos = Photos(); + for (const auto &pair : items) { + if (!Added(pair.first, docs, photos)) { + return; + } + } + const auto done = [weak = Ui::MakeWeak(list)] { + if (const auto strong = weak.data()) { + strong->clearSelected(); + } + }; + AddAction(menu, window, std::move(docs), std::move(photos), done); +} + +} // namespace Menu diff --git a/Telegram/SourceFiles/menu/menu_item_download_files.h b/Telegram/SourceFiles/menu/menu_item_download_files.h new file mode 100644 index 0000000000..91e270b716 --- /dev/null +++ b/Telegram/SourceFiles/menu/menu_item_download_files.h @@ -0,0 +1,41 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +class HistoryInner; +class HistoryItem; + +namespace Ui { +class PopupMenu; +} // namespace Ui + +namespace HistoryView { +class ListWidget; +struct SelectedItem; +} // namespace HistoryView + +namespace Window { +class SessionController; +} // namespace Window + +namespace Menu { + +void AddDownloadFilesAction( + not_null menu, + not_null window, + const std::vector &selectedItems, + not_null list); + +void AddDownloadFilesAction( + not_null menu, + not_null window, + // From the legacy history inner widget. + const std::map> &items, + not_null list); + +} // namespace Menu