2023-08-24 16:04:32 +00:00
|
|
|
/*
|
|
|
|
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 "iv/iv_controller.h"
|
|
|
|
|
2023-12-01 14:47:24 +00:00
|
|
|
#include "base/platform/base_platform_info.h"
|
2023-12-02 18:43:16 +00:00
|
|
|
#include "base/invoke_queued.h"
|
2023-08-24 16:04:32 +00:00
|
|
|
#include "iv/iv_data.h"
|
2023-12-02 18:43:16 +00:00
|
|
|
#include "lang/lang_keys.h"
|
2023-12-04 11:48:17 +00:00
|
|
|
#include "ui/platform/ui_platform_window_title.h"
|
2023-08-24 16:04:32 +00:00
|
|
|
#include "ui/widgets/rp_window.h"
|
2023-12-05 13:25:26 +00:00
|
|
|
#include "ui/widgets/popup_menu.h"
|
|
|
|
#include "ui/basic_click_handlers.h"
|
2023-12-04 11:48:17 +00:00
|
|
|
#include "ui/painter.h"
|
2023-08-24 16:04:32 +00:00
|
|
|
#include "webview/webview_data_stream_memory.h"
|
|
|
|
#include "webview/webview_embed.h"
|
|
|
|
#include "webview/webview_interface.h"
|
2023-12-01 14:47:24 +00:00
|
|
|
#include "styles/palette.h"
|
2023-12-04 11:48:17 +00:00
|
|
|
#include "styles/style_iv.h"
|
2023-12-05 13:25:26 +00:00
|
|
|
#include "styles/style_menu_icons.h"
|
2023-12-04 11:48:17 +00:00
|
|
|
#include "styles/style_widgets.h"
|
|
|
|
#include "styles/style_window.h"
|
2023-12-02 18:43:16 +00:00
|
|
|
|
2023-08-24 16:04:32 +00:00
|
|
|
#include <QtCore/QRegularExpression>
|
2023-12-01 14:47:24 +00:00
|
|
|
#include <QtCore/QJsonDocument>
|
|
|
|
#include <QtCore/QJsonObject>
|
|
|
|
#include <QtCore/QJsonValue>
|
2023-08-24 16:04:32 +00:00
|
|
|
#include <QtCore/QFile>
|
2023-12-01 14:47:24 +00:00
|
|
|
#include <QtGui/QPainter>
|
2023-12-07 10:37:58 +00:00
|
|
|
#include <charconv>
|
2023-08-24 16:04:32 +00:00
|
|
|
|
|
|
|
namespace Iv {
|
2023-12-02 18:43:16 +00:00
|
|
|
namespace {
|
|
|
|
|
|
|
|
[[nodiscard]] QByteArray ComputeStyles() {
|
|
|
|
static const auto map = base::flat_map<QByteArray, const style::color*>{
|
|
|
|
{ "scroll-bg", &st::scrollBg },
|
|
|
|
{ "scroll-bg-over", &st::scrollBgOver },
|
|
|
|
{ "scroll-bar-bg", &st::scrollBarBg },
|
|
|
|
{ "scroll-bar-bg-over", &st::scrollBarBgOver },
|
|
|
|
{ "window-bg", &st::windowBg },
|
|
|
|
{ "window-bg-over", &st::windowBgOver },
|
|
|
|
{ "window-bg-ripple", &st::windowBgRipple },
|
2023-12-04 11:48:17 +00:00
|
|
|
{ "window-bg-active", &st::windowBgActive },
|
2023-12-02 18:43:16 +00:00
|
|
|
{ "window-fg", &st::windowFg },
|
|
|
|
{ "window-sub-text-fg", &st::windowSubTextFg },
|
|
|
|
{ "window-active-text-fg", &st::windowActiveTextFg },
|
2023-12-04 11:48:17 +00:00
|
|
|
{ "window-shadow-fg", &st::windowShadowFg },
|
2023-12-02 18:43:16 +00:00
|
|
|
{ "box-divider-bg", &st::boxDividerBg },
|
|
|
|
{ "box-divider-fg", &st::boxDividerFg },
|
2023-12-04 11:48:17 +00:00
|
|
|
{ "menu-icon-fg", &st::menuIconFg },
|
|
|
|
{ "menu-icon-fg-over", &st::menuIconFgOver },
|
|
|
|
{ "menu-bg", &st::menuBg },
|
|
|
|
{ "menu-bg-over", &st::menuBgOver },
|
|
|
|
{ "history-to-down-fg", &st::historyToDownFg },
|
|
|
|
{ "history-to-down-fg-over", &st::historyToDownFgOver },
|
|
|
|
{ "history-to-down-bg", &st::historyToDownBg },
|
|
|
|
{ "history-to-down-bg-over", &st::historyToDownBgOver },
|
|
|
|
{ "history-to-down-bg-ripple", &st::historyToDownBgRipple },
|
|
|
|
{ "history-to-down-shadow", &st::historyToDownShadow },
|
2023-12-04 19:06:44 +00:00
|
|
|
{ "toast-bg", &st::toastBg },
|
|
|
|
{ "toast-fg", &st::toastFg },
|
2023-12-02 18:43:16 +00:00
|
|
|
};
|
|
|
|
static const auto phrases = base::flat_map<QByteArray, tr::phrase<>>{
|
|
|
|
{ "group-call-join", tr::lng_group_call_join },
|
|
|
|
};
|
|
|
|
static const auto serialize = [](const style::color *color) {
|
|
|
|
const auto qt = (*color)->c;
|
|
|
|
if (qt.alpha() == 255) {
|
|
|
|
return '#'
|
|
|
|
+ QByteArray::number(qt.red(), 16).right(2)
|
|
|
|
+ QByteArray::number(qt.green(), 16).right(2)
|
|
|
|
+ QByteArray::number(qt.blue(), 16).right(2);
|
|
|
|
}
|
|
|
|
return "rgba("
|
|
|
|
+ QByteArray::number(qt.red()) + ","
|
|
|
|
+ QByteArray::number(qt.green()) + ","
|
|
|
|
+ QByteArray::number(qt.blue()) + ","
|
|
|
|
+ QByteArray::number(qt.alpha() / 255.) + ")";
|
|
|
|
};
|
|
|
|
static const auto escape = [](tr::phrase<> phrase) {
|
|
|
|
const auto text = phrase(tr::now);
|
2023-08-24 16:04:32 +00:00
|
|
|
|
2023-12-02 18:43:16 +00:00
|
|
|
auto result = QByteArray();
|
|
|
|
for (auto i = 0; i != text.size(); ++i) {
|
|
|
|
uint ucs4 = text[i].unicode();
|
|
|
|
if (QChar::isHighSurrogate(ucs4) && i + 1 != text.size()) {
|
|
|
|
ushort low = text[i + 1].unicode();
|
|
|
|
if (QChar::isLowSurrogate(low)) {
|
|
|
|
ucs4 = QChar::surrogateToUcs4(ucs4, low);
|
|
|
|
++i;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (ucs4 == '\'' || ucs4 == '\"' || ucs4 == '\\') {
|
|
|
|
result.append('\\').append(char(ucs4));
|
|
|
|
} else if (ucs4 < 32 || ucs4 > 127) {
|
|
|
|
result.append('\\' + QByteArray::number(ucs4, 16) + ' ');
|
|
|
|
} else {
|
|
|
|
result.append(char(ucs4));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
};
|
|
|
|
auto result = QByteArray();
|
|
|
|
for (const auto &[name, phrase] : phrases) {
|
|
|
|
result += "--td-lng-" + name + ":'" + escape(phrase) + "'; ";
|
|
|
|
}
|
|
|
|
for (const auto &[name, color] : map) {
|
|
|
|
result += "--td-" + name + ':' + serialize(color) + ';';
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
[[nodiscard]] QByteArray EscapeForAttribute(QByteArray value) {
|
|
|
|
return value
|
|
|
|
.replace('&', "&")
|
|
|
|
.replace('"', """)
|
|
|
|
.replace('\'', "'")
|
|
|
|
.replace('<', "<")
|
|
|
|
.replace('>', ">");
|
|
|
|
}
|
|
|
|
|
|
|
|
[[nodiscard]] QByteArray EscapeForScriptString(QByteArray value) {
|
|
|
|
return value
|
|
|
|
.replace('\\', "\\\\")
|
|
|
|
.replace('"', "\\\"")
|
|
|
|
.replace('\'', "\\\'");
|
|
|
|
}
|
|
|
|
|
2023-12-07 10:37:58 +00:00
|
|
|
[[nodiscard]] QByteArray WrapPage(const Prepared &page) {
|
2023-12-04 19:06:44 +00:00
|
|
|
#ifdef Q_OS_MAC
|
|
|
|
const auto classAttribute = ""_q;
|
|
|
|
#else // Q_OS_MAC
|
|
|
|
const auto classAttribute = " class=\"custom_scroll\""_q;
|
|
|
|
#endif // Q_OS_MAC
|
|
|
|
|
|
|
|
const auto js = QByteArray()
|
|
|
|
+ (page.hasCode ? "IV.initPreBlocks();" : "")
|
|
|
|
+ (page.hasEmbeds ? "IV.initEmbedBlocks();" : "")
|
|
|
|
+ "IV.init();"
|
2023-12-07 10:37:58 +00:00
|
|
|
+ page.script;
|
2023-12-04 19:06:44 +00:00
|
|
|
|
|
|
|
const auto contentAttributes = page.rtl
|
|
|
|
? " dir=\"rtl\" class=\"rtl\""_q
|
|
|
|
: QByteArray();
|
|
|
|
|
|
|
|
return R"(<!DOCTYPE html>
|
|
|
|
<html)"_q
|
|
|
|
+ classAttribute
|
|
|
|
+ R"("" style=")"
|
|
|
|
+ EscapeForAttribute(ComputeStyles())
|
|
|
|
+ R"(">
|
|
|
|
<head>
|
|
|
|
<meta charset="utf-8">
|
|
|
|
<meta name="robots" content="noindex, nofollow">
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
<script src="/iv/page.js"></script>
|
|
|
|
<script src="/iv/highlight.js"></script>
|
|
|
|
<link rel="stylesheet" href="/iv/page.css" />
|
|
|
|
<link rel="stylesheet" href="/iv/highlight.css">
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<button class="fixed_button hidden" id="top_back" onclick="IV.back();">
|
|
|
|
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
|
|
<line x1="5.37464142" y1="12" x2="18.5" y2="12"></line>
|
|
|
|
<path d="M11.5,18.3 L5.27277119,12.0707223 C5.23375754,12.0316493 5.23375754,11.9683507 5.27277119,11.9292777 L11.5,5.7 L11.5,5.7"></path>
|
|
|
|
</svg>
|
|
|
|
</button>
|
2023-12-05 13:25:26 +00:00
|
|
|
<button class="fixed_button" id="top_menu" onclick="IV.menu(this);">
|
2023-12-04 19:06:44 +00:00
|
|
|
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
|
|
<circle cx="12" cy="17.4" r="1.7"></circle>
|
|
|
|
<circle cx="12" cy="12" r="1.7"></circle>
|
|
|
|
<circle cx="12" cy="6.6" r="1.7"></circle>
|
|
|
|
</svg>
|
|
|
|
</button>
|
2023-12-05 07:54:46 +00:00
|
|
|
<button class="fixed_button hidden" id="bottom_up" onclick="IV.scrollTo(0);">
|
2023-12-04 19:06:44 +00:00
|
|
|
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
|
|
<path d="M14.9972363,18 L9.13865768,12.1414214 C9.06055283,12.0633165 9.06055283,11.9366835 9.13865768,11.8585786 L14.9972363,6 L14.9972363,6" transform="translate(11.997236, 12.000000) scale(-1, -1) rotate(-90.000000) translate(-11.997236, -12.000000) "></path>
|
|
|
|
</svg>
|
|
|
|
</button>
|
2023-12-07 10:37:58 +00:00
|
|
|
<div class="page-scroll"><div class="page-slide">
|
|
|
|
<article)"_q + contentAttributes + ">"_q + page.content + R"(</article>
|
|
|
|
</div></div>
|
2023-12-04 19:06:44 +00:00
|
|
|
<script>)"_q + js + R"(</script>
|
|
|
|
</body>
|
|
|
|
</html>
|
|
|
|
)"_q;
|
|
|
|
}
|
|
|
|
|
2023-12-02 18:43:16 +00:00
|
|
|
} // namespace
|
2023-12-04 19:06:44 +00:00
|
|
|
|
2023-12-02 18:43:16 +00:00
|
|
|
Controller::Controller()
|
|
|
|
: _updateStyles([=] {
|
|
|
|
const auto str = EscapeForScriptString(ComputeStyles());
|
|
|
|
if (_webview) {
|
2023-12-04 19:06:44 +00:00
|
|
|
_webview->eval("IV.updateStyles('" + str + "');");
|
2023-12-02 18:43:16 +00:00
|
|
|
}
|
|
|
|
}) {
|
2023-12-07 10:37:58 +00:00
|
|
|
createWindow();
|
2023-12-02 18:43:16 +00:00
|
|
|
}
|
2023-08-24 16:04:32 +00:00
|
|
|
|
|
|
|
Controller::~Controller() {
|
2024-02-15 09:48:49 +00:00
|
|
|
if (_window) {
|
|
|
|
_window->hide();
|
|
|
|
}
|
2023-12-04 19:06:44 +00:00
|
|
|
_ready = false;
|
2023-08-24 16:04:32 +00:00
|
|
|
_webview = nullptr;
|
2023-12-04 11:48:17 +00:00
|
|
|
_title = nullptr;
|
2023-08-24 16:04:32 +00:00
|
|
|
_window = nullptr;
|
|
|
|
}
|
|
|
|
|
2023-12-07 10:37:58 +00:00
|
|
|
bool Controller::showFast(const QString &url, const QString &hash) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2023-12-04 19:06:44 +00:00
|
|
|
void Controller::show(
|
|
|
|
const QString &dataPath,
|
|
|
|
Prepared page,
|
|
|
|
base::flat_map<QByteArray, rpl::producer<bool>> inChannelValues) {
|
2023-12-07 10:37:58 +00:00
|
|
|
page.script = fillInChannelValuesScript(std::move(inChannelValues));
|
2023-12-04 11:48:17 +00:00
|
|
|
_titleText.setText(st::ivTitle.style, page.title);
|
2023-12-02 18:43:16 +00:00
|
|
|
InvokeQueued(_container, [=, page = std::move(page)]() mutable {
|
2023-12-07 10:37:58 +00:00
|
|
|
showInWindow(dataPath, std::move(page));
|
2023-12-02 18:43:16 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-12-04 19:06:44 +00:00
|
|
|
QByteArray Controller::fillInChannelValuesScript(
|
|
|
|
base::flat_map<QByteArray, rpl::producer<bool>> inChannelValues) {
|
|
|
|
auto result = QByteArray();
|
|
|
|
for (auto &[id, in] : inChannelValues) {
|
2023-12-07 10:37:58 +00:00
|
|
|
if (_inChannelSubscribed.emplace(id).second) {
|
|
|
|
std::move(in) | rpl::start_with_next([=](bool in) {
|
|
|
|
if (_ready) {
|
|
|
|
_webview->eval(toggleInChannelScript(id, in));
|
|
|
|
} else {
|
|
|
|
_inChannelChanged[id] = in;
|
|
|
|
}
|
|
|
|
}, _lifetime);
|
|
|
|
}
|
2023-12-04 19:06:44 +00:00
|
|
|
}
|
|
|
|
for (const auto &[id, in] : base::take(_inChannelChanged)) {
|
|
|
|
result += toggleInChannelScript(id, in);
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
QByteArray Controller::toggleInChannelScript(
|
|
|
|
const QByteArray &id,
|
|
|
|
bool in) const {
|
|
|
|
const auto value = in ? "true" : "false";
|
|
|
|
return "IV.toggleChannelJoined('" + id + "', " + value + ");";
|
|
|
|
}
|
|
|
|
|
2023-12-04 11:48:17 +00:00
|
|
|
void Controller::updateTitleGeometry() {
|
|
|
|
_title->setGeometry(0, 0, _window->width(), st::ivTitle.height);
|
|
|
|
}
|
|
|
|
|
|
|
|
void Controller::paintTitle(Painter &p, QRect clip) {
|
|
|
|
const auto active = _window->isActiveWindow();
|
|
|
|
const auto full = _title->width();
|
|
|
|
p.setPen(active ? st::ivTitle.fgActive : st::ivTitle.fg);
|
|
|
|
const auto available = QRect(
|
|
|
|
_titleLeftSkip,
|
|
|
|
0,
|
|
|
|
full - _titleLeftSkip - _titleRightSkip,
|
|
|
|
_title->height());
|
|
|
|
const auto use = std::min(available.width(), _titleText.maxWidth());
|
|
|
|
const auto center = full
|
|
|
|
- 2 * std::max(_titleLeftSkip, _titleRightSkip);
|
|
|
|
const auto left = (use <= center)
|
|
|
|
? ((full - use) / 2)
|
|
|
|
: (use < available.width() && _titleLeftSkip < _titleRightSkip)
|
|
|
|
? (available.x() + available.width() - use)
|
|
|
|
: available.x();
|
|
|
|
const auto titleTextHeight = st::ivTitle.style.font->height;
|
|
|
|
const auto top = (st::ivTitle.height - titleTextHeight) / 2;
|
|
|
|
_titleText.drawLeftElided(p, left, top, available.width(), full);
|
|
|
|
}
|
|
|
|
|
2023-12-02 18:43:16 +00:00
|
|
|
void Controller::createWindow() {
|
2023-08-24 16:04:32 +00:00
|
|
|
_window = std::make_unique<Ui::RpWindow>();
|
2023-12-04 11:48:17 +00:00
|
|
|
_window->setTitleStyle(st::ivTitle);
|
2023-08-24 16:04:32 +00:00
|
|
|
const auto window = _window.get();
|
|
|
|
|
2023-12-04 11:48:17 +00:00
|
|
|
_title = std::make_unique<Ui::RpWidget>(window);
|
|
|
|
_title->setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
|
|
_title->paintRequest() | rpl::start_with_next([=](QRect clip) {
|
|
|
|
auto p = Painter(_title.get());
|
|
|
|
paintTitle(p, clip);
|
|
|
|
}, _title->lifetime());
|
|
|
|
window->widthValue() | rpl::start_with_next([=] {
|
|
|
|
updateTitleGeometry();
|
|
|
|
}, _title->lifetime());
|
2023-08-24 16:04:32 +00:00
|
|
|
|
2023-12-04 11:48:17 +00:00
|
|
|
#ifdef Q_OS_MAC
|
|
|
|
_titleLeftSkip = 8 + 12 + 8 + 12 + 8 + 12 + 8;
|
|
|
|
_titleRightSkip = st::ivTitle.style.font->spacew;
|
|
|
|
#else // Q_OS_MAC
|
|
|
|
using namespace Ui::Platform;
|
|
|
|
TitleControlsLayoutValue(
|
|
|
|
) | rpl::start_with_next([=](TitleControls::Layout layout) {
|
|
|
|
const auto accumulate = [](const auto &list) {
|
|
|
|
auto result = 0;
|
|
|
|
for (const auto control : list) {
|
|
|
|
switch (control) {
|
|
|
|
case TitleControl::Close:
|
|
|
|
result += st::ivTitle.close.width;
|
|
|
|
break;
|
|
|
|
case TitleControl::Minimize:
|
|
|
|
result += st::ivTitle.minimize.width;
|
|
|
|
break;
|
|
|
|
case TitleControl::Maximize:
|
|
|
|
result += st::ivTitle.maximize.width;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
};
|
|
|
|
const auto space = st::ivTitle.style.font->spacew;
|
|
|
|
_titleLeftSkip = accumulate(layout.left) + space;
|
|
|
|
_titleRightSkip = accumulate(layout.right) + space;
|
|
|
|
_title->update();
|
|
|
|
}, _title->lifetime());
|
|
|
|
#endif // Q_OS_MAC
|
|
|
|
|
|
|
|
window->setGeometry({ 200, 200, 600, 800 });
|
|
|
|
window->setMinimumSize({ st::windowMinWidth, st::windowMinHeight });
|
2023-12-02 18:43:16 +00:00
|
|
|
|
|
|
|
_container = Ui::CreateChild<Ui::RpWidget>(window->body().get());
|
|
|
|
rpl::combine(
|
|
|
|
window->body()->sizeValue(),
|
2023-12-04 11:48:17 +00:00
|
|
|
_title->heightValue()
|
|
|
|
) | rpl::start_with_next([=](QSize size, int title) {
|
|
|
|
title -= window->body()->y();
|
|
|
|
_container->setGeometry(QRect(QPoint(), size).marginsRemoved(
|
|
|
|
{ 0, title, 0, 0 }));
|
2023-12-02 18:43:16 +00:00
|
|
|
}, _container->lifetime());
|
|
|
|
|
|
|
|
_container->paintRequest() | rpl::start_with_next([=](QRect clip) {
|
|
|
|
QPainter(_container).fillRect(clip, st::windowBg);
|
|
|
|
}, _container->lifetime());
|
|
|
|
|
|
|
|
_container->show();
|
|
|
|
window->show();
|
|
|
|
}
|
|
|
|
|
2023-12-07 10:37:58 +00:00
|
|
|
void Controller::createWebview(const QString &dataPath) {
|
|
|
|
Expects(!_webview);
|
2023-08-24 16:04:32 +00:00
|
|
|
|
2023-12-02 18:43:16 +00:00
|
|
|
const auto window = _window.get();
|
2023-08-24 16:04:32 +00:00
|
|
|
_webview = std::make_unique<Webview::Window>(
|
2023-12-02 18:43:16 +00:00
|
|
|
_container,
|
|
|
|
Webview::WindowConfig{
|
|
|
|
.opaqueBg = st::windowBg->c,
|
|
|
|
.userDataPath = dataPath,
|
|
|
|
});
|
2023-08-24 16:04:32 +00:00
|
|
|
const auto raw = _webview.get();
|
|
|
|
|
|
|
|
window->lifetime().add([=] {
|
2023-12-04 19:06:44 +00:00
|
|
|
_ready = false;
|
2023-08-24 16:04:32 +00:00
|
|
|
_webview = nullptr;
|
|
|
|
});
|
2023-12-07 10:37:58 +00:00
|
|
|
|
2023-12-01 14:47:24 +00:00
|
|
|
window->events(
|
|
|
|
) | rpl::start_with_next([=](not_null<QEvent*> e) {
|
|
|
|
if (e->type() == QEvent::Close) {
|
|
|
|
close();
|
|
|
|
} else if (e->type() == QEvent::KeyPress) {
|
|
|
|
const auto event = static_cast<QKeyEvent*>(e.get());
|
|
|
|
if (event->key() == Qt::Key_Escape) {
|
|
|
|
escape();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, window->lifetime());
|
2023-08-24 16:04:32 +00:00
|
|
|
raw->widget()->show();
|
|
|
|
|
2023-12-02 18:43:16 +00:00
|
|
|
_container->sizeValue(
|
|
|
|
) | rpl::start_with_next([=](QSize size) {
|
|
|
|
raw->widget()->setGeometry(QRect(QPoint(), size));
|
|
|
|
}, _container->lifetime());
|
2023-08-24 16:04:32 +00:00
|
|
|
|
|
|
|
raw->setNavigationStartHandler([=](const QString &uri, bool newWindow) {
|
|
|
|
return true;
|
|
|
|
});
|
|
|
|
raw->setNavigationDoneHandler([=](bool success) {
|
|
|
|
});
|
2023-12-01 14:47:24 +00:00
|
|
|
raw->setMessageHandler([=](const QJsonDocument &message) {
|
|
|
|
crl::on_main(_window.get(), [=] {
|
|
|
|
const auto object = message.object();
|
|
|
|
const auto event = object.value("event").toString();
|
|
|
|
if (event == u"keydown"_q) {
|
|
|
|
const auto key = object.value("key").toString();
|
|
|
|
const auto modifier = object.value("modifier").toString();
|
2023-12-04 19:06:44 +00:00
|
|
|
processKey(key, modifier);
|
2023-12-04 11:48:17 +00:00
|
|
|
} else if (event == u"mouseenter"_q) {
|
|
|
|
window->overrideSystemButtonOver({});
|
|
|
|
} else if (event == u"mouseup"_q) {
|
|
|
|
window->overrideSystemButtonDown({});
|
2023-12-04 19:06:44 +00:00
|
|
|
} else if (event == u"link_click"_q) {
|
|
|
|
const auto url = object.value("url").toString();
|
|
|
|
const auto context = object.value("context").toString();
|
|
|
|
processLink(url, context);
|
|
|
|
} else if (event == u"ready"_q) {
|
|
|
|
_ready = true;
|
|
|
|
auto script = QByteArray();
|
|
|
|
for (const auto &[id, in] : base::take(_inChannelChanged)) {
|
|
|
|
script += toggleInChannelScript(id, in);
|
|
|
|
}
|
2023-12-07 10:37:58 +00:00
|
|
|
if (_navigateToIndexWhenReady >= 0) {
|
|
|
|
script += navigateScript(
|
|
|
|
std::exchange(_navigateToIndexWhenReady, -1),
|
|
|
|
base::take(_navigateToHashWhenReady));
|
|
|
|
}
|
2023-12-04 19:06:44 +00:00
|
|
|
if (!script.isEmpty()) {
|
|
|
|
_webview->eval(script);
|
|
|
|
}
|
2023-12-05 13:25:26 +00:00
|
|
|
} else if (event == u"menu"_q) {
|
2023-12-07 10:37:58 +00:00
|
|
|
menu(
|
|
|
|
object.value("index").toInt(),
|
|
|
|
object.value("hash").toString());
|
2023-12-01 14:47:24 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
2023-08-24 16:04:32 +00:00
|
|
|
raw->setDataRequestHandler([=](Webview::DataRequest request) {
|
2024-02-15 09:48:49 +00:00
|
|
|
const auto pos = request.id.find('#');
|
|
|
|
if (pos != request.id.npos) {
|
|
|
|
request.id = request.id.substr(0, pos);
|
|
|
|
}
|
2023-08-24 16:04:32 +00:00
|
|
|
if (!request.id.starts_with("iv/")) {
|
|
|
|
_dataRequests.fire(std::move(request));
|
|
|
|
return Webview::DataResult::Pending;
|
|
|
|
}
|
|
|
|
const auto finishWith = [&](QByteArray data, std::string mime) {
|
|
|
|
request.done({
|
|
|
|
.stream = std::make_unique<Webview::DataStreamFromMemory>(
|
|
|
|
std::move(data),
|
|
|
|
std::move(mime)),
|
2023-12-02 18:43:16 +00:00
|
|
|
});
|
2023-08-24 16:04:32 +00:00
|
|
|
return Webview::DataResult::Done;
|
|
|
|
};
|
|
|
|
const auto id = std::string_view(request.id).substr(3);
|
2023-12-07 10:37:58 +00:00
|
|
|
if (id.starts_with("page") && id.ends_with(".html")) {
|
2023-12-02 18:43:16 +00:00
|
|
|
if (!_subscribedToColors) {
|
|
|
|
_subscribedToColors = true;
|
|
|
|
|
|
|
|
rpl::merge(
|
|
|
|
Lang::Updated(),
|
|
|
|
style::PaletteChanged()
|
|
|
|
) | rpl::start_with_next([=] {
|
|
|
|
_updateStyles.call();
|
|
|
|
}, _webview->lifetime());
|
|
|
|
}
|
2023-12-07 10:37:58 +00:00
|
|
|
auto index = 0;
|
|
|
|
const auto result = std::from_chars(
|
|
|
|
id.data() + 4,
|
|
|
|
id.data() + id.size() - 5,
|
|
|
|
index);
|
|
|
|
if (result.ec != std::errc()
|
|
|
|
|| index < 0
|
|
|
|
|| index >= _pages.size()) {
|
|
|
|
return Webview::DataResult::Failed;
|
|
|
|
}
|
|
|
|
return finishWith(WrapPage(_pages[index]), "text/html");
|
|
|
|
} else if (id.starts_with("page") && id.ends_with(".json")) {
|
|
|
|
auto index = 0;
|
|
|
|
const auto result = std::from_chars(
|
|
|
|
id.data() + 4,
|
|
|
|
id.data() + id.size() - 5,
|
|
|
|
index);
|
|
|
|
if (result.ec != std::errc()
|
|
|
|
|| index < 0
|
|
|
|
|| index >= _pages.size()) {
|
|
|
|
return Webview::DataResult::Failed;
|
|
|
|
}
|
|
|
|
auto &page = _pages[index];
|
|
|
|
return finishWith(QJsonDocument(QJsonObject{
|
|
|
|
{ "html", QJsonValue(QString::fromUtf8(page.content)) },
|
|
|
|
{ "js", QJsonValue(QString::fromUtf8(page.script)) },
|
|
|
|
}).toJson(QJsonDocument::Compact), "application/json");
|
2023-08-24 16:04:32 +00:00
|
|
|
}
|
|
|
|
const auto css = id.ends_with(".css");
|
|
|
|
const auto js = !css && id.ends_with(".js");
|
|
|
|
if (!css && !js) {
|
|
|
|
return Webview::DataResult::Failed;
|
|
|
|
}
|
|
|
|
const auto qstring = QString::fromUtf8(id.data(), id.size());
|
|
|
|
const auto pattern = u"^[a-zA-Z\\.\\-_0-9]+$"_q;
|
|
|
|
if (QRegularExpression(pattern).match(qstring).hasMatch()) {
|
|
|
|
auto file = QFile(u":/iv/"_q + qstring);
|
|
|
|
if (file.open(QIODevice::ReadOnly)) {
|
|
|
|
const auto mime = css ? "text/css" : "text/javascript";
|
|
|
|
return finishWith(file.readAll(), mime);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return Webview::DataResult::Failed;
|
|
|
|
});
|
|
|
|
|
2023-12-04 19:06:44 +00:00
|
|
|
raw->init(R"()");
|
2023-12-07 10:37:58 +00:00
|
|
|
}
|
2023-12-05 07:54:46 +00:00
|
|
|
|
2023-12-07 10:37:58 +00:00
|
|
|
void Controller::showInWindow(const QString &dataPath, Prepared page) {
|
|
|
|
Expects(_container != nullptr);
|
|
|
|
|
|
|
|
const auto url = page.url;
|
|
|
|
const auto hash = page.hash;
|
|
|
|
auto i = _indices.find(url);
|
|
|
|
if (i == end(_indices)) {
|
|
|
|
_pages.push_back(std::move(page));
|
|
|
|
i = _indices.emplace(url, int(_pages.size() - 1)).first;
|
|
|
|
}
|
|
|
|
const auto index = i->second;
|
|
|
|
if (!_webview) {
|
|
|
|
createWebview(dataPath);
|
|
|
|
if (_webview && _webview->widget()) {
|
|
|
|
auto id = u"iv/page%1.html"_q.arg(index);
|
|
|
|
if (!hash.isEmpty()) {
|
|
|
|
id += '#' + hash;
|
|
|
|
}
|
|
|
|
_webview->navigateToData(id);
|
|
|
|
} else {
|
|
|
|
_events.fire({ Event::Type::Close });
|
|
|
|
}
|
|
|
|
} else if (_ready) {
|
|
|
|
_webview->eval(navigateScript(index, hash));
|
2024-02-15 09:48:49 +00:00
|
|
|
_window->raise();
|
2023-12-07 10:37:58 +00:00
|
|
|
_window->activateWindow();
|
|
|
|
_window->setFocus();
|
|
|
|
} else {
|
|
|
|
_navigateToIndexWhenReady = index;
|
|
|
|
_navigateToHashWhenReady = hash;
|
2024-02-15 09:48:49 +00:00
|
|
|
_window->raise();
|
2023-12-07 10:37:58 +00:00
|
|
|
_window->activateWindow();
|
|
|
|
_window->setFocus();
|
2023-12-05 07:54:46 +00:00
|
|
|
}
|
2023-12-07 10:37:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
QByteArray Controller::navigateScript(int index, const QString &hash) {
|
|
|
|
return "IV.navigateTo("
|
|
|
|
+ QByteArray::number(index)
|
|
|
|
+ ", '"
|
|
|
|
+ EscapeForScriptString(hash.toUtf8())
|
|
|
|
+ "');";
|
2023-08-24 16:04:32 +00:00
|
|
|
}
|
|
|
|
|
2023-12-04 19:06:44 +00:00
|
|
|
void Controller::processKey(const QString &key, const QString &modifier) {
|
|
|
|
const auto ctrl = Platform::IsMac() ? u"cmd"_q : u"ctrl"_q;
|
|
|
|
if (key == u"escape"_q) {
|
|
|
|
escape();
|
|
|
|
} else if (key == u"w"_q && modifier == ctrl) {
|
|
|
|
close();
|
|
|
|
} else if (key == u"m"_q && modifier == ctrl) {
|
|
|
|
minimize();
|
|
|
|
} else if (key == u"q"_q && modifier == ctrl) {
|
|
|
|
quit();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Controller::processLink(const QString &url, const QString &context) {
|
|
|
|
const auto channelPrefix = u"channel"_q;
|
|
|
|
const auto joinPrefix = u"join_link"_q;
|
2023-12-05 09:17:07 +00:00
|
|
|
const auto webpagePrefix = u"webpage"_q;
|
2024-02-13 07:53:40 +00:00
|
|
|
const auto viewerPrefix = u"viewer"_q;
|
2023-12-04 19:06:44 +00:00
|
|
|
if (context.startsWith(channelPrefix)) {
|
|
|
|
_events.fire({
|
2023-12-05 09:17:07 +00:00
|
|
|
.type = Event::Type::OpenChannel,
|
|
|
|
.context = context.mid(channelPrefix.size()),
|
2023-12-04 19:06:44 +00:00
|
|
|
});
|
|
|
|
} else if (context.startsWith(joinPrefix)) {
|
|
|
|
_events.fire({
|
2023-12-05 09:17:07 +00:00
|
|
|
.type = Event::Type::JoinChannel,
|
|
|
|
.context = context.mid(joinPrefix.size()),
|
|
|
|
});
|
|
|
|
} else if (context.startsWith(webpagePrefix)) {
|
|
|
|
_events.fire({
|
|
|
|
.type = Event::Type::OpenPage,
|
|
|
|
.url = url,
|
|
|
|
.context = context.mid(webpagePrefix.size()),
|
2023-12-04 19:06:44 +00:00
|
|
|
});
|
2024-02-13 07:53:40 +00:00
|
|
|
} else if (context.startsWith(viewerPrefix)) {
|
|
|
|
_events.fire({
|
|
|
|
.type = Event::Type::OpenMedia,
|
|
|
|
.url = url,
|
|
|
|
.context = context.mid(viewerPrefix.size()),
|
|
|
|
});
|
2023-12-05 07:54:46 +00:00
|
|
|
} else if (context.isEmpty()) {
|
2023-12-05 09:17:07 +00:00
|
|
|
_events.fire({ .type = Event::Type::OpenLink, .url = url });
|
2023-12-04 19:06:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-01 14:47:24 +00:00
|
|
|
bool Controller::active() const {
|
|
|
|
return _window && _window->isActiveWindow();
|
|
|
|
}
|
|
|
|
|
2023-12-04 19:06:44 +00:00
|
|
|
void Controller::showJoinedTooltip() {
|
|
|
|
if (_webview) {
|
|
|
|
_webview->eval("IV.showTooltip('"
|
|
|
|
+ EscapeForScriptString(
|
|
|
|
tr::lng_action_you_joined(tr::now).toUtf8())
|
|
|
|
+ "');");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-01 14:47:24 +00:00
|
|
|
void Controller::minimize() {
|
|
|
|
if (_window) {
|
|
|
|
_window->setWindowState(_window->windowState()
|
|
|
|
| Qt::WindowMinimized);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-07 10:37:58 +00:00
|
|
|
void Controller::menu(int index, const QString &hash) {
|
|
|
|
if (!_webview || _menu || index < 0 || index > _pages.size()) {
|
2023-12-05 13:25:26 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
_menu = base::make_unique_q<Ui::PopupMenu>(
|
|
|
|
_window.get(),
|
|
|
|
st::popupMenuWithIcons);
|
|
|
|
_menu->setDestroyedCallback(crl::guard(_window.get(), [
|
|
|
|
this,
|
|
|
|
menu = _menu.get()] {
|
|
|
|
if (_webview) {
|
|
|
|
_webview->eval("IV.clearFrozenRipple();");
|
|
|
|
}
|
|
|
|
}));
|
|
|
|
|
2023-12-07 10:37:58 +00:00
|
|
|
const auto url = _pages[index].url
|
|
|
|
+ (hash.isEmpty() ? u""_q : ('#' + hash));
|
2023-12-05 13:25:26 +00:00
|
|
|
const auto openInBrowser = crl::guard(_window.get(), [=] {
|
|
|
|
_events.fire({ .type = Event::Type::OpenLinkExternal, .url = url });
|
|
|
|
});
|
|
|
|
_menu->addAction(
|
|
|
|
tr::lng_iv_open_in_browser(tr::now),
|
|
|
|
openInBrowser,
|
|
|
|
&st::menuIconIpAddress);
|
|
|
|
|
|
|
|
_menu->addAction(tr::lng_iv_share(tr::now), [=] {
|
|
|
|
}, &st::menuIconShare);
|
|
|
|
|
|
|
|
_menu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight);
|
|
|
|
_menu->popup(_window->body()->mapToGlobal(
|
|
|
|
QPoint(_window->body()->width(), 0) + st::ivMenuPosition));
|
|
|
|
}
|
|
|
|
|
2023-12-01 14:47:24 +00:00
|
|
|
void Controller::escape() {
|
|
|
|
close();
|
|
|
|
}
|
|
|
|
|
|
|
|
void Controller::close() {
|
2023-12-04 19:06:44 +00:00
|
|
|
_events.fire({ Event::Type::Close });
|
2023-12-01 14:47:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Controller::quit() {
|
2023-12-04 19:06:44 +00:00
|
|
|
_events.fire({ Event::Type::Quit });
|
2023-08-24 16:04:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
rpl::lifetime &Controller::lifetime() {
|
|
|
|
return _lifetime;
|
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace Iv
|