mirror of https://github.com/procxx/kepka.git
Highlight timestamps in video captions.
This commit is contained in:
parent
e9620af6fb
commit
3e3e1d628c
|
@ -583,27 +583,36 @@ void Application::checkStartUrl() {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Application::openLocalUrl(const QString &url, QVariant context) {
|
bool Application::openLocalUrl(const QString &url, QVariant context) {
|
||||||
auto urlTrimmed = url.trimmed();
|
return openCustomUrl("tg://", LocalUrlHandlers(), url, context);
|
||||||
if (urlTrimmed.size() > 8192) urlTrimmed = urlTrimmed.mid(0, 8192);
|
}
|
||||||
|
|
||||||
const auto protocol = qstr("tg://");
|
bool Application::openInternalUrl(const QString &url, QVariant context) {
|
||||||
|
return openCustomUrl("internal:", InternalUrlHandlers(), url, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Application::openCustomUrl(
|
||||||
|
const QString &protocol,
|
||||||
|
const std::vector<LocalUrlHandler> &handlers,
|
||||||
|
const QString &url,
|
||||||
|
const QVariant &context) {
|
||||||
|
const auto urlTrimmed = url.trimmed();
|
||||||
if (!urlTrimmed.startsWith(protocol, Qt::CaseInsensitive) || locked()) {
|
if (!urlTrimmed.startsWith(protocol, Qt::CaseInsensitive) || locked()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
auto command = urlTrimmed.midRef(protocol.size());
|
const auto command = urlTrimmed.midRef(protocol.size(), 8192);
|
||||||
|
|
||||||
const auto session = activeAccount().sessionExists()
|
const auto session = activeAccount().sessionExists()
|
||||||
? &activeAccount().session()
|
? &activeAccount().session()
|
||||||
: nullptr;
|
: nullptr;
|
||||||
using namespace qthelp;
|
using namespace qthelp;
|
||||||
const auto options = RegExOption::CaseInsensitive;
|
const auto options = RegExOption::CaseInsensitive;
|
||||||
for (const auto &[expression, handler] : LocalUrlHandlers()) {
|
for (const auto &[expression, handler] : handlers) {
|
||||||
const auto match = regex_match(expression, command, options);
|
const auto match = regex_match(expression, command, options);
|
||||||
if (match) {
|
if (match) {
|
||||||
return handler(session, match, context);
|
return handler(session, match, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Application::lockByPasscode() {
|
void Application::lockByPasscode() {
|
||||||
|
|
|
@ -178,6 +178,7 @@ public:
|
||||||
QString createInternalLinkFull(const QString &query) const;
|
QString createInternalLinkFull(const QString &query) const;
|
||||||
void checkStartUrl();
|
void checkStartUrl();
|
||||||
bool openLocalUrl(const QString &url, QVariant context);
|
bool openLocalUrl(const QString &url, QVariant context);
|
||||||
|
bool openInternalUrl(const QString &url, QVariant context);
|
||||||
|
|
||||||
void forceLogOut(const TextWithEntities &explanation);
|
void forceLogOut(const TextWithEntities &explanation);
|
||||||
void checkLocalTime();
|
void checkLocalTime();
|
||||||
|
@ -241,6 +242,12 @@ private:
|
||||||
|
|
||||||
void clearPasscodeLock();
|
void clearPasscodeLock();
|
||||||
|
|
||||||
|
bool openCustomUrl(
|
||||||
|
const QString &protocol,
|
||||||
|
const std::vector<LocalUrlHandler> &handlers,
|
||||||
|
const QString &url,
|
||||||
|
const QVariant &context);
|
||||||
|
|
||||||
static Application *Instance;
|
static Application *Instance;
|
||||||
struct InstanceSetter {
|
struct InstanceSetter {
|
||||||
InstanceSetter(not_null<Application*> instance) {
|
InstanceSetter(not_null<Application*> instance) {
|
||||||
|
|
|
@ -43,7 +43,8 @@ void HiddenUrlClickHandler::Open(QString url, QVariant context) {
|
||||||
const auto open = [=] {
|
const auto open = [=] {
|
||||||
UrlClickHandler::Open(url, context);
|
UrlClickHandler::Open(url, context);
|
||||||
};
|
};
|
||||||
if (url.startsWith(qstr("tg://"), Qt::CaseInsensitive)) {
|
if (url.startsWith(qstr("tg://"), Qt::CaseInsensitive)
|
||||||
|
|| url.startsWith(qstr("internal:"), Qt::CaseInsensitive)) {
|
||||||
open();
|
open();
|
||||||
} else {
|
} else {
|
||||||
const auto parsedUrl = QUrl::fromUserInput(url);
|
const auto parsedUrl = QUrl::fromUserInput(url);
|
||||||
|
@ -55,7 +56,9 @@ void HiddenUrlClickHandler::Open(QString url, QVariant context) {
|
||||||
: url;
|
: url;
|
||||||
Ui::show(
|
Ui::show(
|
||||||
Box<ConfirmBox>(
|
Box<ConfirmBox>(
|
||||||
tr::lng_open_this_link(tr::now) + qsl("\n\n") + displayUrl,
|
(tr::lng_open_this_link(tr::now)
|
||||||
|
+ qsl("\n\n")
|
||||||
|
+ displayUrl),
|
||||||
tr::lng_open_link(tr::now),
|
tr::lng_open_link(tr::now),
|
||||||
[=] { Ui::hideLayer(); open(); }),
|
[=] { Ui::hideLayer(); open(); }),
|
||||||
Ui::LayerOption::KeepOther);
|
Ui::LayerOption::KeepOther);
|
||||||
|
|
|
@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "passport/passport_form_controller.h"
|
#include "passport/passport_form_controller.h"
|
||||||
#include "window/window_session_controller.h"
|
#include "window/window_session_controller.h"
|
||||||
#include "data/data_session.h"
|
#include "data/data_session.h"
|
||||||
|
#include "data/data_document.h"
|
||||||
#include "data/data_cloud_themes.h"
|
#include "data/data_cloud_themes.h"
|
||||||
#include "data/data_channel.h"
|
#include "data/data_channel.h"
|
||||||
#include "mainwindow.h"
|
#include "mainwindow.h"
|
||||||
|
@ -362,6 +363,38 @@ bool HandleUnknown(
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool OpenMediaTimestamp(
|
||||||
|
Main::Session *session,
|
||||||
|
const Match &match,
|
||||||
|
const QVariant &context) {
|
||||||
|
if (!session) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto time = match->captured(2).toInt();
|
||||||
|
if (time < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto base = match->captured(1);
|
||||||
|
if (base.startsWith(qstr("doc"))) {
|
||||||
|
const auto parts = base.mid(3).split('_');
|
||||||
|
const auto documentId = parts.value(0).toULongLong();
|
||||||
|
const auto itemId = FullMsgId(
|
||||||
|
parts.value(1).toInt(),
|
||||||
|
parts.value(2).toInt());
|
||||||
|
const auto document = session->data().document(documentId);
|
||||||
|
session->settings().setMediaLastPlaybackPosition(
|
||||||
|
documentId,
|
||||||
|
time * crl::time(1000));
|
||||||
|
if (!document->isNull()) {
|
||||||
|
Core::App().showDocument(
|
||||||
|
document,
|
||||||
|
session->data().message(itemId));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
const std::vector<LocalUrlHandler> &LocalUrlHandlers() {
|
const std::vector<LocalUrlHandler> &LocalUrlHandlers() {
|
||||||
|
@ -421,7 +454,17 @@ const std::vector<LocalUrlHandler> &LocalUrlHandlers() {
|
||||||
{
|
{
|
||||||
qsl("^([^\\?]+)(\\?|#|$)"),
|
qsl("^([^\\?]+)(\\?|#|$)"),
|
||||||
HandleUnknown
|
HandleUnknown
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const std::vector<LocalUrlHandler> &InternalUrlHandlers() {
|
||||||
|
static auto Result = std::vector<LocalUrlHandler>{
|
||||||
|
{
|
||||||
|
qsl("^media_timestamp/?\\?base=([a-zA-Z0-9\\.\\_\\-]+)&t=(\\d+)(&|$)"),
|
||||||
|
OpenMediaTimestamp
|
||||||
|
},
|
||||||
};
|
};
|
||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ struct LocalUrlHandler {
|
||||||
};
|
};
|
||||||
|
|
||||||
[[nodiscard]] const std::vector<LocalUrlHandler> &LocalUrlHandlers();
|
[[nodiscard]] const std::vector<LocalUrlHandler> &LocalUrlHandlers();
|
||||||
|
[[nodiscard]] const std::vector<LocalUrlHandler> &InternalUrlHandlers();
|
||||||
|
|
||||||
[[nodiscard]] QString TryConvertUrlToLocal(QString url);
|
[[nodiscard]] QString TryConvertUrlToLocal(QString url);
|
||||||
|
|
||||||
|
|
|
@ -126,6 +126,9 @@ bool UiIntegration::handleUrlClick(
|
||||||
} else if (local.startsWith(qstr("tg://"), Qt::CaseInsensitive)) {
|
} else if (local.startsWith(qstr("tg://"), Qt::CaseInsensitive)) {
|
||||||
Core::App().openLocalUrl(local, context);
|
Core::App().openLocalUrl(local, context);
|
||||||
return true;
|
return true;
|
||||||
|
} else if (local.startsWith(qstr("internal:"), Qt::CaseInsensitive)) {
|
||||||
|
Core::App().openInternalUrl(local, context);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
|
|
@ -76,7 +76,7 @@ Gif::Gif(
|
||||||
|
|
||||||
setStatusSize(FileStatusSizeReady);
|
setStatusSize(FileStatusSizeReady);
|
||||||
|
|
||||||
_caption = createCaption(realParent);
|
refreshCaption();
|
||||||
_data->loadThumbnail(realParent->fullId());
|
_data->loadThumbnail(realParent->fullId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1208,11 +1208,30 @@ bool Gif::isReadyForOpen() const {
|
||||||
}
|
}
|
||||||
|
|
||||||
void Gif::parentTextUpdated() {
|
void Gif::parentTextUpdated() {
|
||||||
_caption = (_parent->media() == this)
|
if (_parent->media() == this) {
|
||||||
? createCaption(_parent->data())
|
refreshCaption();
|
||||||
: Ui::Text::String();
|
|
||||||
history()->owner().requestViewResize(_parent);
|
history()->owner().requestViewResize(_parent);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Gif::refreshParentId(not_null<HistoryItem*> realParent) {
|
||||||
|
if (_parent->media() == this) {
|
||||||
|
refreshCaption();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Gif::refreshCaption() {
|
||||||
|
const auto timestampLinksDuration = _data->isVideoFile()
|
||||||
|
? _data->getDuration()
|
||||||
|
: 0;
|
||||||
|
const auto timestampLinkBase = timestampLinksDuration
|
||||||
|
? DocumentTimestampLinkBase(_data, _realParent->fullId())
|
||||||
|
: QString();
|
||||||
|
_caption = createCaption(
|
||||||
|
_parent->data(),
|
||||||
|
timestampLinksDuration,
|
||||||
|
timestampLinkBase);
|
||||||
|
}
|
||||||
|
|
||||||
int Gif::additionalWidth(const HistoryMessageVia *via, const HistoryMessageReply *reply, const HistoryMessageForwarded *forwarded) const {
|
int Gif::additionalWidth(const HistoryMessageVia *via, const HistoryMessageReply *reply, const HistoryMessageForwarded *forwarded) const {
|
||||||
int result = 0;
|
int result = 0;
|
||||||
|
|
|
@ -104,6 +104,8 @@ public:
|
||||||
stopAnimation();
|
stopAnimation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void refreshParentId(not_null<HistoryItem*> realParent) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct Streamed;
|
struct Streamed;
|
||||||
|
|
||||||
|
@ -111,6 +113,8 @@ private:
|
||||||
bool dataFinished() const override;
|
bool dataFinished() const override;
|
||||||
bool dataLoaded() const override;
|
bool dataLoaded() const override;
|
||||||
|
|
||||||
|
void refreshCaption();
|
||||||
|
|
||||||
[[nodiscard]] bool autoplayEnabled() const;
|
[[nodiscard]] bool autoplayEnabled() const;
|
||||||
|
|
||||||
void playAnimation(bool autoplay) override;
|
void playAnimation(bool autoplay) override;
|
||||||
|
|
|
@ -11,10 +11,96 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "history/view/history_view_element.h"
|
#include "history/view/history_view_element.h"
|
||||||
#include "history/view/history_view_cursor_state.h"
|
#include "history/view/history_view_cursor_state.h"
|
||||||
#include "storage/storage_shared_media.h"
|
#include "storage/storage_shared_media.h"
|
||||||
|
#include "data/data_document.h"
|
||||||
#include "ui/text_options.h"
|
#include "ui/text_options.h"
|
||||||
#include "styles/style_history.h"
|
#include "styles/style_history.h"
|
||||||
|
|
||||||
namespace HistoryView {
|
namespace HistoryView {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
[[nodiscard]] TimeId TimeFromMatch(
|
||||||
|
const QStringRef &hours,
|
||||||
|
const QStringRef &minutes1,
|
||||||
|
const QStringRef &minutes2,
|
||||||
|
const QStringRef &seconds) {
|
||||||
|
auto ok1 = true;
|
||||||
|
auto ok2 = true;
|
||||||
|
auto ok3 = true;
|
||||||
|
const auto result = (hours.isEmpty() ? 0 : hours.toInt(&ok1)) * 3600
|
||||||
|
+ (minutes1 + minutes2).toInt(&ok2) * 60
|
||||||
|
+ seconds.toInt(&ok3);
|
||||||
|
return (ok1 && ok2 && ok3) ? result : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] TextWithEntities AddTimestampLinks(
|
||||||
|
TextWithEntities text,
|
||||||
|
TimeId duration,
|
||||||
|
const QString &base) {
|
||||||
|
static const auto expression = QRegularExpression(
|
||||||
|
"(?<![^\\s])(?:(?:(\\d{1,2}):)?(\\d))?(\\d):(\\d\\d)(?![^\\s])");
|
||||||
|
const auto &string = text.text;
|
||||||
|
auto offset = 0;
|
||||||
|
while (true) {
|
||||||
|
const auto m = expression.match(string, offset);
|
||||||
|
if (!m.hasMatch()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto from = m.capturedStart();
|
||||||
|
const auto till = from + m.capturedLength();
|
||||||
|
offset = till;
|
||||||
|
|
||||||
|
const auto time = TimeFromMatch(
|
||||||
|
m.capturedRef(1),
|
||||||
|
m.capturedRef(2),
|
||||||
|
m.capturedRef(3),
|
||||||
|
m.capturedRef(4));
|
||||||
|
if (time < 0 || time > duration) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto &entities = text.entities;
|
||||||
|
const auto i = ranges::lower_bound(
|
||||||
|
entities,
|
||||||
|
from,
|
||||||
|
std::less<>(),
|
||||||
|
&EntityInText::offset);
|
||||||
|
if (i != entities.end() && i->offset() < till) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto intersects = [&](const EntityInText &entity) {
|
||||||
|
return entity.offset() + entity.length() > from;
|
||||||
|
};
|
||||||
|
auto j = std::make_reverse_iterator(i);
|
||||||
|
const auto e = std::make_reverse_iterator(entities.begin());
|
||||||
|
if (std::find_if(j, e, intersects) != e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
entities.insert(
|
||||||
|
i,
|
||||||
|
EntityInText(
|
||||||
|
EntityType::CustomUrl,
|
||||||
|
from,
|
||||||
|
till - from,
|
||||||
|
("internal:media_timestamp?base="
|
||||||
|
+ base
|
||||||
|
+ "&t="
|
||||||
|
+ QString::number(time))));
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
QString DocumentTimestampLinkBase(
|
||||||
|
not_null<DocumentData*> document,
|
||||||
|
FullMsgId context) {
|
||||||
|
return QString(
|
||||||
|
"doc%1_%2_%3"
|
||||||
|
).arg(document->id).arg(context.channel).arg(context.msg);
|
||||||
|
}
|
||||||
|
|
||||||
Storage::SharedMediaTypesMask Media::sharedMediaTypes() const {
|
Storage::SharedMediaTypesMask Media::sharedMediaTypes() const {
|
||||||
return {};
|
return {};
|
||||||
|
@ -32,7 +118,12 @@ QSize Media::countCurrentSize(int newWidth) {
|
||||||
return QSize(qMin(newWidth, maxWidth()), minHeight());
|
return QSize(qMin(newWidth, maxWidth()), minHeight());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ui::Text::String Media::createCaption(not_null<HistoryItem*> item) const {
|
Ui::Text::String Media::createCaption(
|
||||||
|
not_null<HistoryItem*> item,
|
||||||
|
TimeId timestampLinksDuration,
|
||||||
|
const QString ×tampLinkBase) const {
|
||||||
|
Expects(timestampLinksDuration >= 0);
|
||||||
|
|
||||||
if (item->emptyText()) {
|
if (item->emptyText()) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
@ -42,7 +133,12 @@ Ui::Text::String Media::createCaption(not_null<HistoryItem*> item) const {
|
||||||
auto result = Ui::Text::String(minResizeWidth);
|
auto result = Ui::Text::String(minResizeWidth);
|
||||||
result.setMarkedText(
|
result.setMarkedText(
|
||||||
st::messageTextStyle,
|
st::messageTextStyle,
|
||||||
|
(timestampLinksDuration
|
||||||
|
? AddTimestampLinks(
|
||||||
item->originalText(),
|
item->originalText(),
|
||||||
|
timestampLinksDuration,
|
||||||
|
timestampLinkBase)
|
||||||
|
: item->originalText()),
|
||||||
Ui::ItemTextOptions(item));
|
Ui::ItemTextOptions(item));
|
||||||
if (const auto width = _parent->skipBlockWidth()) {
|
if (const auto width = _parent->skipBlockWidth()) {
|
||||||
result.updateSkipBlock(width, _parent->skipBlockHeight());
|
result.updateSkipBlock(width, _parent->skipBlockHeight());
|
||||||
|
|
|
@ -40,6 +40,10 @@ enum class MediaInBubbleState {
|
||||||
Bottom,
|
Bottom,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] QString DocumentTimestampLinkBase(
|
||||||
|
not_null<DocumentData*> document,
|
||||||
|
FullMsgId context);
|
||||||
|
|
||||||
class Media : public Object {
|
class Media : public Object {
|
||||||
public:
|
public:
|
||||||
Media(not_null<Element*> parent) : _parent(parent) {
|
Media(not_null<Element*> parent) : _parent(parent) {
|
||||||
|
@ -237,8 +241,11 @@ public:
|
||||||
virtual ~Media() = default;
|
virtual ~Media() = default;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QSize countCurrentSize(int newWidth) override;
|
[[nodiscard]] QSize countCurrentSize(int newWidth) override;
|
||||||
Ui::Text::String createCaption(not_null<HistoryItem*> item) const;
|
[[nodiscard]] Ui::Text::String createCaption(
|
||||||
|
not_null<HistoryItem*> item,
|
||||||
|
TimeId timestampLinksDuration = 0,
|
||||||
|
const QString ×tampLinkBase = QString()) const;
|
||||||
|
|
||||||
virtual void playAnimation(bool autoplay) {
|
virtual void playAnimation(bool autoplay) {
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue