mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-06-05 22:54:01 +02:00
Implement bot downloads list UI.
This commit is contained in:
parent
ef521624a0
commit
338122793c
14 changed files with 665 additions and 84 deletions
|
@ -3431,6 +3431,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
"lng_bot_download_file" = "Download File";
|
"lng_bot_download_file" = "Download File";
|
||||||
"lng_bot_download_file_sure" = "{bot} suggests you download the following file:";
|
"lng_bot_download_file_sure" = "{bot} suggests you download the following file:";
|
||||||
"lng_bot_download_file_button" = "Download";
|
"lng_bot_download_file_button" = "Download";
|
||||||
|
"lng_bot_download_starting" = "Starting...";
|
||||||
|
"lng_bot_download_failed" = "Failed. {retry}";
|
||||||
|
"lng_bot_download_retry" = "Retry";
|
||||||
|
|
||||||
"lng_bot_status_users#one" = "{count} monthly user";
|
"lng_bot_status_users#one" = "{count} monthly user";
|
||||||
"lng_bot_status_users#other" = "{count} monthly users";
|
"lng_bot_status_users#other" = "{count} monthly users";
|
||||||
|
|
|
@ -1361,7 +1361,7 @@ void WebViewInstance::show(ShowArgs &&args) {
|
||||||
.menuButtons = buttons,
|
.menuButtons = buttons,
|
||||||
.fullscreen = args.fullscreen,
|
.fullscreen = args.fullscreen,
|
||||||
.allowClipboardRead = allowClipboardRead,
|
.allowClipboardRead = allowClipboardRead,
|
||||||
.downloadsProgress = downloads->downloadsProgress(_bot),
|
.downloadsProgress = downloads->progress(_bot),
|
||||||
});
|
});
|
||||||
started(args.queryId);
|
started(args.queryId);
|
||||||
|
|
||||||
|
@ -1450,6 +1450,17 @@ Webview::ThemeParams WebViewInstance::botThemeParams() {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto WebViewInstance::botDownloads(bool forceCheck)
|
||||||
|
-> const std::vector<Ui::BotWebView::DownloadsEntry> & {
|
||||||
|
return _session->attachWebView().downloads().list(_bot, forceCheck);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebViewInstance::botDownloadsAction(
|
||||||
|
uint32 id,
|
||||||
|
Ui::BotWebView::DownloadsAction type) {
|
||||||
|
_session->attachWebView().downloads().action(_bot, id, type);
|
||||||
|
}
|
||||||
|
|
||||||
bool WebViewInstance::botHandleLocalUri(QString uri, bool keepOpen) {
|
bool WebViewInstance::botHandleLocalUri(QString uri, bool keepOpen) {
|
||||||
const auto local = Core::TryConvertUrlToLocal(uri);
|
const auto local = Core::TryConvertUrlToLocal(uri);
|
||||||
if (Core::InternalPassportLink(local)) {
|
if (Core::InternalPassportLink(local)) {
|
||||||
|
|
|
@ -28,6 +28,7 @@ class DropdownMenu;
|
||||||
|
|
||||||
namespace Ui::BotWebView {
|
namespace Ui::BotWebView {
|
||||||
class Panel;
|
class Panel;
|
||||||
|
struct DownloadsEntry;
|
||||||
} // namespace Ui::BotWebView
|
} // namespace Ui::BotWebView
|
||||||
|
|
||||||
namespace Main {
|
namespace Main {
|
||||||
|
@ -250,6 +251,11 @@ private:
|
||||||
-> Fn<void(Payments::NonPanelPaymentForm)>;
|
-> Fn<void(Payments::NonPanelPaymentForm)>;
|
||||||
|
|
||||||
Webview::ThemeParams botThemeParams() override;
|
Webview::ThemeParams botThemeParams() override;
|
||||||
|
auto botDownloads(bool forceCheck = false)
|
||||||
|
-> const std::vector<Ui::BotWebView::DownloadsEntry> & override;
|
||||||
|
void botDownloadsAction(
|
||||||
|
uint32 id,
|
||||||
|
Ui::BotWebView::DownloadsAction type) override;
|
||||||
bool botHandleLocalUri(QString uri, bool keepOpen) override;
|
bool botHandleLocalUri(QString uri, bool keepOpen) override;
|
||||||
void botHandleInvoice(QString slug) override;
|
void botHandleInvoice(QString slug) override;
|
||||||
void botHandleMenuButton(Ui::BotWebView::MenuButton button) override;
|
void botHandleMenuButton(Ui::BotWebView::MenuButton button) override;
|
||||||
|
|
|
@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
*/
|
*/
|
||||||
#include "inline_bots/inline_bot_downloads.h"
|
#include "inline_bots/inline_bot_downloads.h"
|
||||||
|
|
||||||
|
#include "core/file_utilities.h"
|
||||||
#include "data/data_document.h"
|
#include "data/data_document.h"
|
||||||
#include "data/data_peer_id.h"
|
#include "data/data_peer_id.h"
|
||||||
#include "data/data_user.h"
|
#include "data/data_user.h"
|
||||||
|
@ -15,7 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "storage/file_download_web.h"
|
#include "storage/file_download_web.h"
|
||||||
#include "storage/serialize_common.h"
|
#include "storage/serialize_common.h"
|
||||||
#include "storage/storage_account.h"
|
#include "storage/storage_account.h"
|
||||||
#include "ui/chat/attach/attach_bot_webview.h"
|
#include "ui/chat/attach/attach_bot_downloads.h"
|
||||||
#include "ui/layers/generic_box.h"
|
#include "ui/layers/generic_box.h"
|
||||||
#include "ui/text/text_utilities.h"
|
#include "ui/text/text_utilities.h"
|
||||||
#include "ui/widgets/labels.h"
|
#include "ui/widgets/labels.h"
|
||||||
|
@ -24,6 +25,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include <QtCore/QBuffer>
|
#include <QtCore/QBuffer>
|
||||||
#include <QtCore/QDataStream>
|
#include <QtCore/QDataStream>
|
||||||
|
|
||||||
|
#include "base/call_delayed.h"
|
||||||
|
|
||||||
namespace InlineBots {
|
namespace InlineBots {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
@ -37,7 +40,10 @@ Downloads::Downloads(not_null<Main::Session*> session)
|
||||||
: _session(session) {
|
: _session(session) {
|
||||||
}
|
}
|
||||||
|
|
||||||
Downloads::~Downloads() = default;
|
Downloads::~Downloads() {
|
||||||
|
base::take(_loaders);
|
||||||
|
base::take(_lists);
|
||||||
|
}
|
||||||
|
|
||||||
DownloadId Downloads::start(StartArgs &&args) {
|
DownloadId Downloads::start(StartArgs &&args) {
|
||||||
read();
|
read();
|
||||||
|
@ -50,14 +56,28 @@ DownloadId Downloads::start(StartArgs &&args) {
|
||||||
.url = std::move(args.url),
|
.url = std::move(args.url),
|
||||||
.path = std::move(args.path),
|
.path = std::move(args.path),
|
||||||
});
|
});
|
||||||
auto &entry = list.back();
|
load(botId, id, list.back());
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Downloads::load(
|
||||||
|
PeerId botId,
|
||||||
|
DownloadId id,
|
||||||
|
DownloadsEntry &entry) {
|
||||||
|
entry.loading = 1;
|
||||||
|
entry.failed = 0;
|
||||||
|
|
||||||
auto &loader = _loaders[id];
|
auto &loader = _loaders[id];
|
||||||
|
Assert(!loader.loader);
|
||||||
loader.botId = botId;
|
loader.botId = botId;
|
||||||
loader.loader = std::make_unique<webFileLoader>(
|
loader.loader = std::make_unique<webFileLoader>(
|
||||||
_session,
|
_session,
|
||||||
entry.url,
|
entry.url,
|
||||||
entry.path,
|
entry.path,
|
||||||
WebRequestType::FullLoad);
|
WebRequestType::FullLoad);
|
||||||
|
|
||||||
|
applyProgress(botId, id, 0, 0);
|
||||||
|
|
||||||
loader.loader->updates(
|
loader.loader->updates(
|
||||||
) | rpl::start_with_next_error_done([=] {
|
) | rpl::start_with_next_error_done([=] {
|
||||||
progress(botId, id);
|
progress(botId, id);
|
||||||
|
@ -66,9 +86,8 @@ DownloadId Downloads::start(StartArgs &&args) {
|
||||||
}, [=] {
|
}, [=] {
|
||||||
done(botId, id);
|
done(botId, id);
|
||||||
}, loader.loader->lifetime());
|
}, loader.loader->lifetime());
|
||||||
loader.loader->start();
|
|
||||||
|
|
||||||
return id;
|
loader.loader->start();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Downloads::progress(PeerId botId, DownloadId id) {
|
void Downloads::progress(PeerId botId, DownloadId id) {
|
||||||
|
@ -87,22 +106,18 @@ void Downloads::progress(PeerId botId, DownloadId id) {
|
||||||
&DownloadsEntry::id);
|
&DownloadsEntry::id);
|
||||||
Assert(j != end(list));
|
Assert(j != end(list));
|
||||||
|
|
||||||
if (total < 0
|
if (total < 0 || ready > total) {
|
||||||
|| ready > total
|
|
||||||
|| (j->total && j->total != total)) {
|
|
||||||
fail(botId, id);
|
|
||||||
return;
|
|
||||||
} else if (ready > total) {
|
|
||||||
fail(botId, id);
|
fail(botId, id);
|
||||||
return;
|
return;
|
||||||
} else if (ready == total) {
|
} else if (ready == total) {
|
||||||
// Wait for 'done' signal.
|
// Wait for 'done' signal.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
applyProgress(botId, id, total, ready);
|
applyProgress(botId, id, total, ready);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Downloads::fail(PeerId botId, DownloadId id) {
|
void Downloads::fail(PeerId botId, DownloadId id, bool cancel) {
|
||||||
const auto i = _loaders.find(id);
|
const auto i = _loaders.find(id);
|
||||||
if (i == end(_loaders)) {
|
if (i == end(_loaders)) {
|
||||||
return;
|
return;
|
||||||
|
@ -117,7 +132,16 @@ void Downloads::fail(PeerId botId, DownloadId id) {
|
||||||
id,
|
id,
|
||||||
&DownloadsEntry::id);
|
&DownloadsEntry::id);
|
||||||
Assert(k != end(list));
|
Assert(k != end(list));
|
||||||
k->ready = -1;
|
k->loading = 0;
|
||||||
|
k->failed = 1;
|
||||||
|
|
||||||
|
if (cancel) {
|
||||||
|
auto copy = *k;
|
||||||
|
list.erase(k);
|
||||||
|
applyProgress(botId, copy, 0, 0);
|
||||||
|
} else {
|
||||||
|
applyProgress(botId, *k, 0, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Downloads::done(PeerId botId, DownloadId id) {
|
void Downloads::done(PeerId botId, DownloadId id) {
|
||||||
|
@ -125,19 +149,20 @@ void Downloads::done(PeerId botId, DownloadId id) {
|
||||||
if (i == end(_loaders)) {
|
if (i == end(_loaders)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const auto total = i->second.loader->fullSize();
|
||||||
|
if (total <= 0) {
|
||||||
|
fail(botId, id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_loaders.erase(i);
|
||||||
|
|
||||||
auto &list = _lists[botId].list;
|
auto &list = _lists[botId].list;
|
||||||
const auto j = ranges::find(
|
const auto j = ranges::find(
|
||||||
list,
|
list,
|
||||||
id,
|
id,
|
||||||
&DownloadsEntry::id);
|
&DownloadsEntry::id);
|
||||||
Assert(j != end(list));
|
Assert(j != end(list));
|
||||||
|
j->loading = 0;
|
||||||
const auto total = i->second.loader->fullSize();
|
|
||||||
if (total <= 0 || (j->total && j->total != total)) {
|
|
||||||
fail(botId, id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_loaders.erase(i);
|
|
||||||
|
|
||||||
applyProgress(botId, id, total, total);
|
applyProgress(botId, id, total, total);
|
||||||
}
|
}
|
||||||
|
@ -147,7 +172,7 @@ void Downloads::applyProgress(
|
||||||
DownloadId id,
|
DownloadId id,
|
||||||
int64 total,
|
int64 total,
|
||||||
int64 ready) {
|
int64 ready) {
|
||||||
Expects(total > 0);
|
Expects(total >= 0);
|
||||||
Expects(ready >= 0 && ready <= total);
|
Expects(ready >= 0 && ready <= total);
|
||||||
|
|
||||||
auto &list = _lists[botId].list;
|
auto &list = _lists[botId].list;
|
||||||
|
@ -157,52 +182,117 @@ void Downloads::applyProgress(
|
||||||
&DownloadsEntry::id);
|
&DownloadsEntry::id);
|
||||||
Assert(j != end(list));
|
Assert(j != end(list));
|
||||||
|
|
||||||
|
applyProgress(botId, *j, total, ready);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Downloads::applyProgress(
|
||||||
|
PeerId botId,
|
||||||
|
DownloadsEntry &entry,
|
||||||
|
int64 total,
|
||||||
|
int64 ready) {
|
||||||
auto &progress = _progressView[botId];
|
auto &progress = _progressView[botId];
|
||||||
auto current = progress.current();
|
auto current = progress.current();
|
||||||
if (!j->total) {
|
auto subtract = int64(0);
|
||||||
j->total = total;
|
if (current.ready == current.total) {
|
||||||
current.total += total;
|
subtract = current.ready;
|
||||||
}
|
}
|
||||||
if (j->ready != ready) {
|
if (entry.total != total) {
|
||||||
const auto delta = ready - j->ready;
|
const auto delta = total - entry.total;
|
||||||
j->ready = ready;
|
entry.total = total;
|
||||||
|
current.total += delta;
|
||||||
|
}
|
||||||
|
if (entry.ready != ready) {
|
||||||
|
const auto delta = ready - entry.ready;
|
||||||
|
entry.ready = ready;
|
||||||
current.ready += delta;
|
current.ready += delta;
|
||||||
}
|
}
|
||||||
|
if (subtract > 0
|
||||||
|
&& current.ready >= subtract
|
||||||
|
&& current.total >= subtract) {
|
||||||
|
current.ready -= subtract;
|
||||||
|
current.total -= subtract;
|
||||||
|
}
|
||||||
|
if (entry.loading || current.ready < current.total) {
|
||||||
|
current.loading = 1;
|
||||||
|
} else {
|
||||||
|
current.loading = 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (total == ready) {
|
if (total > 0 && total == ready) {
|
||||||
write();
|
write();
|
||||||
}
|
}
|
||||||
|
|
||||||
progress = current;
|
progress = current;
|
||||||
if (current.ready == current.total) {
|
}
|
||||||
progress = DownloadsProgress();
|
|
||||||
|
void Downloads::action(
|
||||||
|
not_null<UserData*> bot,
|
||||||
|
DownloadId id,
|
||||||
|
DownloadsAction type) {
|
||||||
|
switch (type) {
|
||||||
|
case DownloadsAction::Open: {
|
||||||
|
const auto i = ranges::find(
|
||||||
|
_lists[bot->id].list,
|
||||||
|
id,
|
||||||
|
&DownloadsEntry::id);
|
||||||
|
if (i == end(_lists[bot->id].list)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
File::ShowInFolder(i->path);
|
||||||
|
} break;
|
||||||
|
case DownloadsAction::Cancel: {
|
||||||
|
const auto i = _loaders.find(id);
|
||||||
|
if (i == end(_loaders)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto botId = i->second.botId;
|
||||||
|
fail(botId, id, true);
|
||||||
|
} break;
|
||||||
|
case DownloadsAction::Retry: {
|
||||||
|
const auto i = ranges::find(
|
||||||
|
_lists[bot->id].list,
|
||||||
|
id,
|
||||||
|
&DownloadsEntry::id);
|
||||||
|
if (i == end(_lists[bot->id].list)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
load(bot->id, id, *i);
|
||||||
|
} break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Downloads::cancel(DownloadId id) {
|
[[nodiscard]] auto Downloads::progress(not_null<UserData*> bot)
|
||||||
const auto i = _loaders.find(id);
|
|
||||||
if (i == end(_loaders)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const auto botId = i->second.botId;
|
|
||||||
fail(botId, id);
|
|
||||||
|
|
||||||
auto &list = _lists[botId].list;
|
|
||||||
list.erase(
|
|
||||||
ranges::remove(list, id, &DownloadsEntry::id),
|
|
||||||
end(list));
|
|
||||||
|
|
||||||
auto &progress = _progressView[botId];
|
|
||||||
progress.force_assign(progress.current());
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] auto Downloads::downloadsProgress(not_null<UserData*> bot)
|
|
||||||
->rpl::producer<DownloadsProgress> {
|
->rpl::producer<DownloadsProgress> {
|
||||||
read();
|
read();
|
||||||
|
|
||||||
return _progressView[bot->id].value();
|
return _progressView[bot->id].value();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const std::vector<DownloadsEntry> &Downloads::list(
|
||||||
|
not_null<UserData*> bot,
|
||||||
|
bool forceCheck) {
|
||||||
|
read();
|
||||||
|
|
||||||
|
auto &entry = _lists[bot->id];
|
||||||
|
if (forceCheck) {
|
||||||
|
const auto was = int(entry.list.size());
|
||||||
|
for (auto i = begin(entry.list); i != end(entry.list);) {
|
||||||
|
if (i->loading || i->failed) {
|
||||||
|
++i;
|
||||||
|
} else if (auto info = QFileInfo(i->path)
|
||||||
|
; !info.exists() || info.size() != i->total) {
|
||||||
|
i = entry.list.erase(i);
|
||||||
|
} else {
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (int(entry.list.size()) != was) {
|
||||||
|
write();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entry.list;
|
||||||
|
}
|
||||||
|
|
||||||
void Downloads::read() {
|
void Downloads::read() {
|
||||||
auto bytes = _session->local().readInlineBotsDownloads();
|
auto bytes = _session->local().readInlineBotsDownloads();
|
||||||
if (bytes.isEmpty()) {
|
if (bytes.isEmpty()) {
|
||||||
|
@ -239,8 +329,9 @@ void Downloads::read() {
|
||||||
list.list.reserve(count);
|
list.list.reserve(count);
|
||||||
for (auto j = 0; j != count; ++j) {
|
for (auto j = 0; j != count; ++j) {
|
||||||
auto entry = DownloadsEntry();
|
auto entry = DownloadsEntry();
|
||||||
stream >> entry.url >> entry.path >> entry.total;
|
auto size = int64();
|
||||||
entry.ready = entry.total;
|
stream >> entry.url >> entry.path >> size;
|
||||||
|
entry.total = entry.ready = size;
|
||||||
entry.id = ++_autoIncrementId;
|
entry.id = ++_autoIncrementId;
|
||||||
list.list.push_back(std::move(entry));
|
list.list.push_back(std::move(entry));
|
||||||
}
|
}
|
||||||
|
@ -259,7 +350,7 @@ void Downloads::write() {
|
||||||
if (entry.total > 0 && entry.ready == entry.total) {
|
if (entry.total > 0 && entry.ready == entry.total) {
|
||||||
size += Serialize::stringSize(entry.url)
|
size += Serialize::stringSize(entry.url)
|
||||||
+ Serialize::stringSize(entry.path)
|
+ Serialize::stringSize(entry.path)
|
||||||
+ sizeof(quint64); // total
|
+ sizeof(quint64); // size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "ui/chat/attach/attach_bot_webview.h"
|
||||||
|
|
||||||
class webFileLoader;
|
class webFileLoader;
|
||||||
|
|
||||||
namespace Main {
|
namespace Main {
|
||||||
|
@ -17,23 +19,13 @@ namespace Ui {
|
||||||
class GenericBox;
|
class GenericBox;
|
||||||
} // namespace Ui
|
} // namespace Ui
|
||||||
|
|
||||||
namespace Ui::BotWebView {
|
|
||||||
struct DownloadsProgress;
|
|
||||||
} // namespace Ui::BotWebView
|
|
||||||
|
|
||||||
namespace InlineBots {
|
namespace InlineBots {
|
||||||
|
|
||||||
using DownloadId = uint32;
|
using DownloadId = uint32;
|
||||||
|
|
||||||
using ::Ui::BotWebView::DownloadsProgress;
|
using ::Ui::BotWebView::DownloadsProgress;
|
||||||
|
using ::Ui::BotWebView::DownloadsEntry;
|
||||||
struct DownloadsEntry {
|
using ::Ui::BotWebView::DownloadsAction;
|
||||||
DownloadId id = 0;
|
|
||||||
QString url;
|
|
||||||
QString path;
|
|
||||||
uint64 ready = 0;
|
|
||||||
uint64 total = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
class Downloads final {
|
class Downloads final {
|
||||||
public:
|
public:
|
||||||
|
@ -47,15 +39,20 @@ public:
|
||||||
};
|
};
|
||||||
uint32 start(StartArgs &&args); // Returns download id.
|
uint32 start(StartArgs &&args); // Returns download id.
|
||||||
|
|
||||||
void cancel(DownloadId id);
|
void action(
|
||||||
|
not_null<UserData*> bot,
|
||||||
|
DownloadId id,
|
||||||
|
DownloadsAction type);
|
||||||
|
|
||||||
[[nodiscard]] auto downloadsProgress(not_null<UserData*> bot)
|
[[nodiscard]] rpl::producer<DownloadsProgress> progress(
|
||||||
-> rpl::producer<DownloadsProgress>;
|
not_null<UserData*> bot);
|
||||||
|
[[nodiscard]] const std::vector<DownloadsEntry> &list(
|
||||||
|
not_null<UserData*> bot,
|
||||||
|
bool check = false);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct List {
|
struct List {
|
||||||
std::vector<DownloadsEntry> list;
|
std::vector<DownloadsEntry> list;
|
||||||
bool checked = false;
|
|
||||||
};
|
};
|
||||||
struct Loader {
|
struct Loader {
|
||||||
std::unique_ptr<webFileLoader> loader;
|
std::unique_ptr<webFileLoader> loader;
|
||||||
|
@ -65,14 +62,23 @@ private:
|
||||||
void read();
|
void read();
|
||||||
void write();
|
void write();
|
||||||
|
|
||||||
|
void load(
|
||||||
|
PeerId botId,
|
||||||
|
DownloadId id,
|
||||||
|
DownloadsEntry &entry);
|
||||||
void progress(PeerId botId, DownloadId id);
|
void progress(PeerId botId, DownloadId id);
|
||||||
void fail(PeerId botId, DownloadId id);
|
void fail(PeerId botId, DownloadId id, bool cancel = false);
|
||||||
void done(PeerId botId, DownloadId id);
|
void done(PeerId botId, DownloadId id);
|
||||||
void applyProgress(
|
void applyProgress(
|
||||||
PeerId botId,
|
PeerId botId,
|
||||||
DownloadId id,
|
DownloadId id,
|
||||||
int64 total,
|
int64 total,
|
||||||
int64 ready);
|
int64 ready);
|
||||||
|
void applyProgress(
|
||||||
|
PeerId botId,
|
||||||
|
DownloadsEntry &entry,
|
||||||
|
int64 total,
|
||||||
|
int64 ready);
|
||||||
|
|
||||||
const not_null<Main::Session*> _session;
|
const not_null<Main::Session*> _session;
|
||||||
|
|
||||||
|
|
|
@ -150,3 +150,7 @@ botWebViewBottomButton: RoundButton(paymentsPanelSubmit) {
|
||||||
}
|
}
|
||||||
textTop: 11px;
|
textTop: 11px;
|
||||||
}
|
}
|
||||||
|
botWebViewRadialStroke: 3px;
|
||||||
|
botWebViewMenu: PopupMenu(popupMenuWithIcons) {
|
||||||
|
maxHeight: 360px;
|
||||||
|
}
|
||||||
|
|
|
@ -321,7 +321,7 @@ void WebLoadManager::progress(
|
||||||
).arg(status));
|
).arg(status));
|
||||||
failed(id, reply);
|
failed(id, reply);
|
||||||
} else {
|
} else {
|
||||||
notify(id, reply, ready, total);
|
notify(id, reply, ready, std::max(ready, total));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
268
Telegram/SourceFiles/ui/chat/attach/attach_bot_downloads.cpp
Normal file
268
Telegram/SourceFiles/ui/chat/attach/attach_bot_downloads.cpp
Normal file
|
@ -0,0 +1,268 @@
|
||||||
|
/*
|
||||||
|
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 "ui/chat/attach/attach_bot_downloads.h"
|
||||||
|
|
||||||
|
#include "lang/lang_keys.h"
|
||||||
|
#include "ui/widgets/menu/menu_item_base.h"
|
||||||
|
#include "ui/widgets/labels.h"
|
||||||
|
#include "ui/widgets/popup_menu.h"
|
||||||
|
#include "ui/effects/ripple_animation.h"
|
||||||
|
#include "ui/text/format_values.h"
|
||||||
|
#include "ui/text/text_utilities.h"
|
||||||
|
#include "ui/painter.h"
|
||||||
|
#include "styles/style_chat.h"
|
||||||
|
|
||||||
|
namespace Ui::BotWebView {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
class Action final : public Menu::ItemBase {
|
||||||
|
public:
|
||||||
|
Action(
|
||||||
|
not_null<RpWidget*> parent,
|
||||||
|
const DownloadsEntry &entry,
|
||||||
|
Fn<void(DownloadsAction)> callback);
|
||||||
|
|
||||||
|
bool isEnabled() const override;
|
||||||
|
not_null<QAction*> action() const override { return _dummyAction; }
|
||||||
|
void handleKeyPress(not_null<QKeyEvent*> e) override;
|
||||||
|
|
||||||
|
void refresh(const DownloadsEntry &entry);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QPoint prepareRippleStartPosition() const override {
|
||||||
|
return mapFromGlobal(QCursor::pos());
|
||||||
|
}
|
||||||
|
QImage prepareRippleMask() const override {
|
||||||
|
return Ui::RippleAnimation::RectMask(size());
|
||||||
|
}
|
||||||
|
int contentHeight() const override { return _height; }
|
||||||
|
|
||||||
|
void prepare();
|
||||||
|
void paint(Painter &p);
|
||||||
|
|
||||||
|
const not_null<QAction*> _dummyAction;
|
||||||
|
const style::Menu &_st = st::defaultMenu;
|
||||||
|
|
||||||
|
DownloadsEntry _entry;
|
||||||
|
Text::String _name;
|
||||||
|
FlatLabel _progress;
|
||||||
|
IconButton _cancel;
|
||||||
|
int _textWidth = 0;
|
||||||
|
const int _height;
|
||||||
|
};
|
||||||
|
|
||||||
|
Action::Action(
|
||||||
|
not_null<RpWidget*> parent,
|
||||||
|
const DownloadsEntry &entry,
|
||||||
|
Fn<void(DownloadsAction)> callback)
|
||||||
|
: ItemBase(parent, st::defaultMenu)
|
||||||
|
, _dummyAction(new QAction(parent))
|
||||||
|
, _progress(this, st::botDownloadProgress)
|
||||||
|
, _cancel(this, st::botDownloadCancel)
|
||||||
|
, _height(st::ttlItemPadding.top()
|
||||||
|
+ _st.itemStyle.font->height
|
||||||
|
+ st::ttlItemTimerFont->height
|
||||||
|
+ st::ttlItemPadding.bottom()) {
|
||||||
|
setAcceptBoth(true);
|
||||||
|
initResizeHook(parent->sizeValue());
|
||||||
|
setClickedCallback([=] {
|
||||||
|
if (isEnabled()) {
|
||||||
|
callback(DownloadsAction::Open);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_cancel.setClickedCallback([=] {
|
||||||
|
callback(DownloadsAction::Cancel);
|
||||||
|
});
|
||||||
|
|
||||||
|
paintRequest(
|
||||||
|
) | rpl::start_with_next([=] {
|
||||||
|
Painter p(this);
|
||||||
|
paint(p);
|
||||||
|
}, lifetime());
|
||||||
|
|
||||||
|
widthValue() | rpl::start_with_next([=](int width) {
|
||||||
|
_progress.moveToLeft(
|
||||||
|
_st.itemPadding.left(),
|
||||||
|
st::ttlItemPadding.top() + _st.itemStyle.font->height,
|
||||||
|
width);
|
||||||
|
|
||||||
|
_cancel.moveToRight(
|
||||||
|
_st.itemPadding.right(),
|
||||||
|
(_height - _cancel.height()) / 2,
|
||||||
|
width);
|
||||||
|
}, lifetime());
|
||||||
|
|
||||||
|
_progress.setClickHandlerFilter([=](const auto &...) {
|
||||||
|
callback(DownloadsAction::Retry);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
enableMouseSelecting();
|
||||||
|
refresh(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Action::paint(Painter &p) {
|
||||||
|
const auto selected = isSelected();
|
||||||
|
if (selected && _st.itemBgOver->c.alpha() < 255) {
|
||||||
|
p.fillRect(0, 0, width(), _height, _st.itemBg);
|
||||||
|
}
|
||||||
|
p.fillRect(0, 0, width(), _height, selected ? _st.itemBgOver : _st.itemBg);
|
||||||
|
if (isEnabled()) {
|
||||||
|
paintRipple(p, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
p.setPen(selected ? _st.itemFgOver : _st.itemFg);
|
||||||
|
_name.drawLeftElided(
|
||||||
|
p,
|
||||||
|
_st.itemPadding.left(),
|
||||||
|
st::ttlItemPadding.top(),
|
||||||
|
_textWidth,
|
||||||
|
width());
|
||||||
|
|
||||||
|
_progress.setTextColorOverride(
|
||||||
|
selected ? _st.itemFgShortcutOver->c : _st.itemFgShortcut->c);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Action::prepare() {
|
||||||
|
const auto filenameWidth = _name.maxWidth();
|
||||||
|
const auto progressWidth = _progress.textMaxWidth();
|
||||||
|
const auto &padding = _st.itemPadding;
|
||||||
|
|
||||||
|
const auto goodWidth = std::max(filenameWidth, progressWidth);
|
||||||
|
|
||||||
|
// Example max width: "4000 / 4000 MB"
|
||||||
|
const auto countWidth = [&](const QString &text) {
|
||||||
|
return st::ttlItemTimerFont->width(text);
|
||||||
|
};
|
||||||
|
const auto maxProgressWidth = countWidth(tr::lng_media_save_progress(
|
||||||
|
tr::now,
|
||||||
|
lt_ready,
|
||||||
|
"0000",
|
||||||
|
lt_total,
|
||||||
|
"0000",
|
||||||
|
lt_mb,
|
||||||
|
"MB"));
|
||||||
|
const auto maxStartingWidth = countWidth(
|
||||||
|
tr::lng_bot_download_starting(tr::now));
|
||||||
|
const auto maxFailedWidth = countWidth(tr::lng_bot_download_failed(
|
||||||
|
tr::now,
|
||||||
|
lt_retry,
|
||||||
|
tr::lng_bot_download_retry(tr::now)));
|
||||||
|
|
||||||
|
const auto cancel = _cancel.width() + padding.right();
|
||||||
|
const auto paddings = padding.left() + padding.right() + cancel;
|
||||||
|
const auto w = std::clamp(
|
||||||
|
paddings + std::max({
|
||||||
|
goodWidth,
|
||||||
|
maxProgressWidth,
|
||||||
|
maxStartingWidth,
|
||||||
|
maxFailedWidth,
|
||||||
|
}),
|
||||||
|
_st.widthMin,
|
||||||
|
_st.widthMax);
|
||||||
|
_textWidth = w - paddings;
|
||||||
|
_progress.resizeToWidth(_textWidth);
|
||||||
|
setMinWidth(w);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Action::isEnabled() const {
|
||||||
|
return _entry.total > 0 && _entry.ready == _entry.total;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Action::handleKeyPress(not_null<QKeyEvent*> e) {
|
||||||
|
if (!isSelected()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto key = e->key();
|
||||||
|
if (key == Qt::Key_Enter || key == Qt::Key_Return) {
|
||||||
|
setClicked(Menu::TriggeredSource::Keyboard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Action::refresh(const DownloadsEntry &entry) {
|
||||||
|
_entry = entry;
|
||||||
|
const auto filename = entry.path.split('/').last();
|
||||||
|
_name.setMarkedText(_st.itemStyle, { filename }, kDefaultTextOptions);
|
||||||
|
|
||||||
|
const auto progressText = (entry.total && entry.total == entry.ready)
|
||||||
|
? TextWithEntities{ FormatSizeText(entry.total) }
|
||||||
|
: entry.loading
|
||||||
|
? (entry.total
|
||||||
|
? TextWithEntities{
|
||||||
|
FormatProgressText(entry.ready, entry.total),
|
||||||
|
}
|
||||||
|
: tr::lng_bot_download_starting(tr::now, Text::WithEntities))
|
||||||
|
: tr::lng_bot_download_failed(
|
||||||
|
tr::now,
|
||||||
|
lt_retry,
|
||||||
|
Text::Link(tr::lng_bot_download_retry(tr::now)),
|
||||||
|
Text::WithEntities);
|
||||||
|
_progress.setMarkedText(progressText);
|
||||||
|
|
||||||
|
const auto enabled = isEnabled();
|
||||||
|
setCursor(enabled ? style::cur_pointer : style::cur_default);
|
||||||
|
_cancel.setVisible(!enabled && _entry.loading);
|
||||||
|
_progress.setAttribute(Qt::WA_TransparentForMouseEvents, enabled);
|
||||||
|
|
||||||
|
prepare();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
FnMut<void(not_null<PopupMenu*>)> FillAttachBotDownloadsSubmenu(
|
||||||
|
rpl::producer<std::vector<DownloadsEntry>> content,
|
||||||
|
Fn<void(uint32, DownloadsAction)> callback) {
|
||||||
|
return [callback, moved = std::move(content)](
|
||||||
|
not_null<PopupMenu*> menu) mutable {
|
||||||
|
struct Row {
|
||||||
|
not_null<Action*> action;
|
||||||
|
uint32 id = 0;
|
||||||
|
};
|
||||||
|
struct State {
|
||||||
|
std::vector<Row> rows;
|
||||||
|
};
|
||||||
|
const auto state = menu->lifetime().make_state<State>();
|
||||||
|
std::move(
|
||||||
|
moved
|
||||||
|
) | rpl::start_with_next([=](
|
||||||
|
const std::vector<DownloadsEntry> &entries) {
|
||||||
|
auto found = base::flat_set<uint32>();
|
||||||
|
for (const auto &entry : entries | ranges::views::reverse) {
|
||||||
|
const auto id = entry.id;
|
||||||
|
const auto path = entry.path;
|
||||||
|
const auto i = ranges::find(state->rows, id, &Row::id);
|
||||||
|
found.emplace(id);
|
||||||
|
|
||||||
|
if (i != end(state->rows)) {
|
||||||
|
i->action->refresh(entry);
|
||||||
|
} else {
|
||||||
|
auto action = base::make_unique_q<Action>(
|
||||||
|
menu,
|
||||||
|
entry,
|
||||||
|
[=](DownloadsAction type) { callback(id, type); });
|
||||||
|
state->rows.push_back({
|
||||||
|
.action = action.get(),
|
||||||
|
.id = id,
|
||||||
|
});
|
||||||
|
menu->addAction(std::move(action));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (auto i = begin(state->rows); i != end(state->rows);) {
|
||||||
|
if (!found.contains(i->id)) {
|
||||||
|
menu->removeAction(i - begin(state->rows));
|
||||||
|
i = state->rows.erase(i);
|
||||||
|
} else {
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, menu->lifetime());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Ui::BotWebView
|
47
Telegram/SourceFiles/ui/chat/attach/attach_bot_downloads.h
Normal file
47
Telegram/SourceFiles/ui/chat/attach/attach_bot_downloads.h
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
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
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
class PopupMenu;
|
||||||
|
} // namespace Ui
|
||||||
|
|
||||||
|
namespace Ui::BotWebView {
|
||||||
|
|
||||||
|
struct DownloadsProgress {
|
||||||
|
uint64 ready = 0;
|
||||||
|
uint64 total : 63 = 0;
|
||||||
|
uint64 loading : 1 = 0;
|
||||||
|
|
||||||
|
friend inline bool operator==(
|
||||||
|
const DownloadsProgress &a,
|
||||||
|
const DownloadsProgress &b) = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DownloadsEntry {
|
||||||
|
uint32 id = 0;
|
||||||
|
QString url;
|
||||||
|
QString path;
|
||||||
|
uint64 ready : 63 = 0;
|
||||||
|
uint64 loading : 1 = 0;
|
||||||
|
uint64 total : 63 = 0;
|
||||||
|
uint64 failed : 1 = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class DownloadsAction {
|
||||||
|
Open,
|
||||||
|
Retry,
|
||||||
|
Cancel,
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] auto FillAttachBotDownloadsSubmenu(
|
||||||
|
rpl::producer<std::vector<DownloadsEntry>> content,
|
||||||
|
Fn<void(uint32, DownloadsAction)> callback)
|
||||||
|
-> FnMut<void(not_null<PopupMenu*>)>;
|
||||||
|
|
||||||
|
} // namespace Ui::BotWebView
|
|
@ -8,9 +8,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "ui/chat/attach/attach_bot_webview.h"
|
#include "ui/chat/attach/attach_bot_webview.h"
|
||||||
|
|
||||||
#include "core/file_utilities.h"
|
#include "core/file_utilities.h"
|
||||||
|
#include "ui/boxes/confirm_box.h"
|
||||||
#include "ui/effects/radial_animation.h"
|
#include "ui/effects/radial_animation.h"
|
||||||
#include "ui/effects/ripple_animation.h"
|
#include "ui/effects/ripple_animation.h"
|
||||||
#include "ui/layers/box_content.h"
|
#include "ui/layers/box_content.h"
|
||||||
|
#include "ui/style/style_core_palette.h"
|
||||||
#include "ui/text/text_utilities.h"
|
#include "ui/text/text_utilities.h"
|
||||||
#include "ui/widgets/separate_panel.h"
|
#include "ui/widgets/separate_panel.h"
|
||||||
#include "ui/widgets/buttons.h"
|
#include "ui/widgets/buttons.h"
|
||||||
|
@ -28,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "base/debug_log.h"
|
#include "base/debug_log.h"
|
||||||
#include "base/invoke_queued.h"
|
#include "base/invoke_queued.h"
|
||||||
#include "base/qt_signal_producer.h"
|
#include "base/qt_signal_producer.h"
|
||||||
|
#include "styles/style_chat.h"
|
||||||
#include "styles/style_payments.h"
|
#include "styles/style_payments.h"
|
||||||
#include "styles/style_layers.h"
|
#include "styles/style_layers.h"
|
||||||
#include "styles/style_menu_icons.h"
|
#include "styles/style_menu_icons.h"
|
||||||
|
@ -370,7 +373,9 @@ Panel::Panel(Args &&args)
|
||||||
: _storageId(args.storageId)
|
: _storageId(args.storageId)
|
||||||
, _delegate(args.delegate)
|
, _delegate(args.delegate)
|
||||||
, _menuButtons(args.menuButtons)
|
, _menuButtons(args.menuButtons)
|
||||||
, _widget(std::make_unique<SeparatePanel>())
|
, _widget(std::make_unique<SeparatePanel>(Ui::SeparatePanelArgs{
|
||||||
|
.menuSt = &st::botWebViewMenu,
|
||||||
|
}))
|
||||||
, _fullscreen(args.fullscreen)
|
, _fullscreen(args.fullscreen)
|
||||||
, _allowClipboardRead(args.allowClipboardRead) {
|
, _allowClipboardRead(args.allowClipboardRead) {
|
||||||
_widget->setWindowFlag(Qt::WindowStaysOnTopHint, false);
|
_widget->setWindowFlag(Qt::WindowStaysOnTopHint, false);
|
||||||
|
@ -425,7 +430,9 @@ Panel::Panel(Args &&args)
|
||||||
setTitle(std::move(args.title));
|
setTitle(std::move(args.title));
|
||||||
_widget->setTitleBadge(std::move(args.titleBadge));
|
_widget->setTitleBadge(std::move(args.titleBadge));
|
||||||
|
|
||||||
if (!showWebview(args.url, params, std::move(args.bottom))) {
|
if (showWebview(args.url, params, std::move(args.bottom))) {
|
||||||
|
setupDownloadsProgress(rpl::duplicate(args.downloadsProgress));
|
||||||
|
} else {
|
||||||
const auto available = Webview::Availability();
|
const auto available = Webview::Availability();
|
||||||
if (available.error != Webview::Available::Error::None) {
|
if (available.error != Webview::Available::Error::None) {
|
||||||
showWebviewError(tr::lng_bot_no_webview(tr::now), available);
|
showWebviewError(tr::lng_bot_no_webview(tr::now), available);
|
||||||
|
@ -441,6 +448,97 @@ Panel::~Panel() {
|
||||||
_widget = nullptr;
|
_widget = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Panel::setupDownloadsProgress(rpl::producer<DownloadsProgress> progress) {
|
||||||
|
Expects(_menuToggle != nullptr);
|
||||||
|
|
||||||
|
const auto widget = Ui::CreateChild<RpWidget>(_menuToggle.data());
|
||||||
|
widget->show();
|
||||||
|
widget->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||||
|
|
||||||
|
_menuToggle->sizeValue() | rpl::start_with_next([=](QSize size) {
|
||||||
|
widget->setGeometry(QRect(QPoint(), size));
|
||||||
|
}, widget->lifetime());
|
||||||
|
|
||||||
|
struct State {
|
||||||
|
State(QWidget *parent, Fn<float64()> progress)
|
||||||
|
: animation([=](crl::time now) {
|
||||||
|
const auto updated = animation.update(progress(), false, now);
|
||||||
|
if (!anim::Disabled() || updated) {
|
||||||
|
parent->update();
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
}
|
||||||
|
|
||||||
|
RadialAnimation animation;
|
||||||
|
Animations::Simple fade;
|
||||||
|
bool shown = false;
|
||||||
|
};
|
||||||
|
const auto state = widget->lifetime().make_state<State>(widget, [=] {
|
||||||
|
const auto total = _downloadsProgress.total;
|
||||||
|
return total ? (_downloadsProgress.ready / float64(total)) : 0.;
|
||||||
|
});
|
||||||
|
std::move(
|
||||||
|
progress
|
||||||
|
) | rpl::start_with_next([=](DownloadsProgress progress) {
|
||||||
|
const auto toggle = [&](bool shown) {
|
||||||
|
if (state->shown == shown) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state->shown = shown;
|
||||||
|
if (shown && !state->fade.animating()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state->fade.start([=] {
|
||||||
|
widget->update();
|
||||||
|
if (!state->shown
|
||||||
|
&& !state->fade.animating()
|
||||||
|
&& (!_downloadsProgress.total
|
||||||
|
|| (_downloadsProgress.ready
|
||||||
|
== _downloadsProgress.total))) {
|
||||||
|
state->animation.stop();
|
||||||
|
}
|
||||||
|
}, shown ? 0. : 2., shown ? 2. : 0., st::radialDuration * 2);
|
||||||
|
};
|
||||||
|
if (!state->shown && progress.loading) {
|
||||||
|
if (!state->animation.animating()) {
|
||||||
|
state->animation.start(0.);
|
||||||
|
}
|
||||||
|
toggle(true);
|
||||||
|
} else if ((_downloadsProgress.total && !progress.total)
|
||||||
|
|| (_downloadsProgress.ready < _downloadsProgress.total
|
||||||
|
&& progress.ready == progress.total)) {
|
||||||
|
state->animation.update(1., false, crl::now());
|
||||||
|
toggle(false);
|
||||||
|
}
|
||||||
|
_downloadsProgress = progress;
|
||||||
|
_downloadsUpdated.fire({});
|
||||||
|
}, widget->lifetime());
|
||||||
|
|
||||||
|
widget->paintRequest() | rpl::start_with_next([=] {
|
||||||
|
const auto opacity = std::clamp(
|
||||||
|
state->fade.value(state->shown ? 2. : 0.) - 1.,
|
||||||
|
0.,
|
||||||
|
1.);
|
||||||
|
if (!opacity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto p = QPainter(widget);
|
||||||
|
p.setOpacity(opacity);
|
||||||
|
const auto palette = _widget->titleOverridePalette();
|
||||||
|
const auto color = palette
|
||||||
|
? palette->boxTitleCloseFg()
|
||||||
|
: st::paymentsLoading.color;
|
||||||
|
const auto &st = st::separatePanelMenu;
|
||||||
|
const auto size = st.rippleAreaSize;
|
||||||
|
const auto rect = QRect(st.rippleAreaPosition, QSize(size, size));
|
||||||
|
const auto stroke = st::botWebViewRadialStroke;
|
||||||
|
const auto shift = stroke * 1.5;
|
||||||
|
const auto inner = QRectF(rect).marginsRemoved(
|
||||||
|
QMarginsF{ shift, shift, shift, shift });
|
||||||
|
state->animation.draw(p, inner, stroke, color);
|
||||||
|
}, widget->lifetime());
|
||||||
|
}
|
||||||
|
|
||||||
void Panel::requestActivate() {
|
void Panel::requestActivate() {
|
||||||
_widget->showAndActivate();
|
_widget->showAndActivate();
|
||||||
if (const auto widget = _webview ? _webview->window.widget() : nullptr) {
|
if (const auto widget = _webview ? _webview->window.widget() : nullptr) {
|
||||||
|
@ -582,7 +680,32 @@ bool Panel::showWebview(
|
||||||
updateThemeParams(params);
|
updateThemeParams(params);
|
||||||
_webview->window.navigate(url);
|
_webview->window.navigate(url);
|
||||||
_widget->setBackAllowed(allowBack);
|
_widget->setBackAllowed(allowBack);
|
||||||
_widget->setMenuAllowed([=](const Ui::Menu::MenuCallback &callback) {
|
|
||||||
|
_menuToggle = _widget->setMenuAllowed([=](
|
||||||
|
const Ui::Menu::MenuCallback &callback) {
|
||||||
|
auto list = _delegate->botDownloads(true);
|
||||||
|
if (!list.empty()) {
|
||||||
|
auto value = rpl::single(
|
||||||
|
std::move(list)
|
||||||
|
) | rpl::then(_downloadsUpdated.events(
|
||||||
|
) | rpl::map([=] {
|
||||||
|
return _delegate->botDownloads();
|
||||||
|
}));
|
||||||
|
const auto action = [=](uint32 id, DownloadsAction type) {
|
||||||
|
_delegate->botDownloadsAction(id, type);
|
||||||
|
};
|
||||||
|
callback(Ui::Menu::MenuCallback::Args{
|
||||||
|
.text = tr::lng_downloads_section(tr::now),
|
||||||
|
.icon = &st::menuIconDownload,
|
||||||
|
.fillSubmenu = FillAttachBotDownloadsSubmenu(
|
||||||
|
std::move(value),
|
||||||
|
action),
|
||||||
|
});
|
||||||
|
callback({
|
||||||
|
.separatorSt = &st::expandedMenuSeparator,
|
||||||
|
.isSeparator = true,
|
||||||
|
});
|
||||||
|
}
|
||||||
if (_webview && _webview->window.widget() && _hasSettingsButton) {
|
if (_webview && _webview->window.widget() && _hasSettingsButton) {
|
||||||
callback(tr::lng_bot_settings(tr::now), [=] {
|
callback(tr::lng_bot_settings(tr::now), [=] {
|
||||||
postEvent("settings_button_pressed");
|
postEvent("settings_button_pressed");
|
||||||
|
|
|
@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "base/object_ptr.h"
|
#include "base/object_ptr.h"
|
||||||
#include "base/weak_ptr.h"
|
#include "base/weak_ptr.h"
|
||||||
#include "base/flags.h"
|
#include "base/flags.h"
|
||||||
|
#include "ui/chat/attach/attach_bot_downloads.h"
|
||||||
#include "ui/rect_part.h"
|
#include "ui/rect_part.h"
|
||||||
#include "ui/round_rect.h"
|
#include "ui/round_rect.h"
|
||||||
#include "webview/webview_common.h"
|
#include "webview/webview_common.h"
|
||||||
|
@ -22,6 +23,7 @@ namespace Ui {
|
||||||
class BoxContent;
|
class BoxContent;
|
||||||
class RpWidget;
|
class RpWidget;
|
||||||
class SeparatePanel;
|
class SeparatePanel;
|
||||||
|
class IconButton;
|
||||||
enum class LayerOption;
|
enum class LayerOption;
|
||||||
using LayerOptions = base::flags<LayerOption>;
|
using LayerOptions = base::flags<LayerOption>;
|
||||||
} // namespace Ui
|
} // namespace Ui
|
||||||
|
@ -63,15 +65,6 @@ struct DownloadFileRequest {
|
||||||
Fn<void(bool)> callback;
|
Fn<void(bool)> callback;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct DownloadsProgress {
|
|
||||||
uint64 ready = 0;
|
|
||||||
uint64 total = 0;
|
|
||||||
|
|
||||||
friend inline bool operator==(
|
|
||||||
const DownloadsProgress &a,
|
|
||||||
const DownloadsProgress &b) = default;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct SendPreparedMessageRequest {
|
struct SendPreparedMessageRequest {
|
||||||
QString id = 0;
|
QString id = 0;
|
||||||
Fn<void(QString)> callback;
|
Fn<void(QString)> callback;
|
||||||
|
@ -79,7 +72,12 @@ struct SendPreparedMessageRequest {
|
||||||
|
|
||||||
class Delegate {
|
class Delegate {
|
||||||
public:
|
public:
|
||||||
virtual Webview::ThemeParams botThemeParams() = 0;
|
[[nodiscard]] virtual Webview::ThemeParams botThemeParams() = 0;
|
||||||
|
[[nodiscard]] virtual auto botDownloads(bool forceCheck = false)
|
||||||
|
-> const std::vector<DownloadsEntry> & = 0;
|
||||||
|
virtual void botDownloadsAction(
|
||||||
|
uint32 id,
|
||||||
|
Ui::BotWebView::DownloadsAction type) = 0;
|
||||||
virtual bool botHandleLocalUri(QString uri, bool keepOpen) = 0;
|
virtual bool botHandleLocalUri(QString uri, bool keepOpen) = 0;
|
||||||
virtual void botHandleInvoice(QString slug) = 0;
|
virtual void botHandleInvoice(QString slug) = 0;
|
||||||
virtual void botHandleMenuButton(MenuButton button) = 0;
|
virtual void botHandleMenuButton(MenuButton button) = 0;
|
||||||
|
@ -158,6 +156,7 @@ private:
|
||||||
void createWebviewBottom();
|
void createWebviewBottom();
|
||||||
void showWebviewProgress();
|
void showWebviewProgress();
|
||||||
void hideWebviewProgress();
|
void hideWebviewProgress();
|
||||||
|
void setupDownloadsProgress(rpl::producer<DownloadsProgress> progress);
|
||||||
void setTitle(rpl::producer<QString> title);
|
void setTitle(rpl::producer<QString> title);
|
||||||
void sendDataMessage(const QJsonObject &args);
|
void sendDataMessage(const QJsonObject &args);
|
||||||
void switchInlineQueryMessage(const QJsonObject &args);
|
void switchInlineQueryMessage(const QJsonObject &args);
|
||||||
|
@ -214,6 +213,7 @@ private:
|
||||||
bool _hasSettingsButton = false;
|
bool _hasSettingsButton = false;
|
||||||
MenuButtons _menuButtons = {};
|
MenuButtons _menuButtons = {};
|
||||||
std::unique_ptr<SeparatePanel> _widget;
|
std::unique_ptr<SeparatePanel> _widget;
|
||||||
|
QPointer<IconButton> _menuToggle;
|
||||||
std::unique_ptr<WebviewWithLifetime> _webview;
|
std::unique_ptr<WebviewWithLifetime> _webview;
|
||||||
std::unique_ptr<RpWidget> _webviewBottom;
|
std::unique_ptr<RpWidget> _webviewBottom;
|
||||||
rpl::variable<QString> _bottomText;
|
rpl::variable<QString> _bottomText;
|
||||||
|
@ -229,6 +229,8 @@ private:
|
||||||
rpl::lifetime _headerColorLifetime;
|
rpl::lifetime _headerColorLifetime;
|
||||||
rpl::lifetime _bodyColorLifetime;
|
rpl::lifetime _bodyColorLifetime;
|
||||||
rpl::lifetime _bottomBarColorLifetime;
|
rpl::lifetime _bottomBarColorLifetime;
|
||||||
|
DownloadsProgress _downloadsProgress;
|
||||||
|
rpl::event_stream<> _downloadsUpdated;
|
||||||
rpl::variable<bool> _fullscreen = false;
|
rpl::variable<bool> _fullscreen = false;
|
||||||
bool _layerShown : 1 = false;
|
bool _layerShown : 1 = false;
|
||||||
bool _webviewProgress : 1 = false;
|
bool _webviewProgress : 1 = false;
|
||||||
|
|
|
@ -1183,3 +1183,21 @@ botEmojiStatusEmoji: FlatLabel(botEmojiStatusName) {
|
||||||
}
|
}
|
||||||
|
|
||||||
botDownloadLabel: boxLabel;
|
botDownloadLabel: boxLabel;
|
||||||
|
botDownloadProgress: FlatLabel(defaultFlatLabel) {
|
||||||
|
textFg: windowSubTextFg;
|
||||||
|
style: TextStyle(defaultTextStyle) {
|
||||||
|
font: ttlItemTimerFont;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
botDownloadCancel: IconButton {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
|
||||||
|
icon: smallCloseIcon;
|
||||||
|
iconOver: smallCloseIconOver;
|
||||||
|
iconPosition: point(-1px, -1px);
|
||||||
|
|
||||||
|
rippleAreaPosition: point(0px, 0px);
|
||||||
|
rippleAreaSize: 20px;
|
||||||
|
ripple: defaultRippleAnimationBgOver;
|
||||||
|
}
|
||||||
|
|
|
@ -310,6 +310,8 @@ PRIVATE
|
||||||
ui/chat/attach/attach_album_preview.h
|
ui/chat/attach/attach_album_preview.h
|
||||||
ui/chat/attach/attach_album_thumbnail.cpp
|
ui/chat/attach/attach_album_thumbnail.cpp
|
||||||
ui/chat/attach/attach_album_thumbnail.h
|
ui/chat/attach/attach_album_thumbnail.h
|
||||||
|
ui/chat/attach/attach_bot_downloads.cpp
|
||||||
|
ui/chat/attach/attach_bot_downloads.h
|
||||||
ui/chat/attach/attach_bot_webview.cpp
|
ui/chat/attach/attach_bot_webview.cpp
|
||||||
ui/chat/attach/attach_bot_webview.h
|
ui/chat/attach/attach_bot_webview.h
|
||||||
ui/chat/attach/attach_controls.cpp
|
ui/chat/attach/attach_controls.cpp
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 967ae393e82eb52174fd25e3a5a15b8029e21938
|
Subproject commit 3b5ef7899e5edd544e37ecdf8b1e7e3ba0ca2dc0
|
Loading…
Add table
Reference in a new issue