mirror of
https://github.com/telegramdesktop/tdesktop
synced 2025-01-29 19:03:03 +00:00
Search for venues by location.
This commit is contained in:
parent
de52ac6b28
commit
a130bb1be6
@ -1,7 +1,5 @@
|
||||
:root {
|
||||
--font-sans: -apple-system, BlinkMacSystemFont, avenir next, avenir, Segoe UI Variable Text, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, tahoma, arial, sans-serif;
|
||||
--font-serif: Iowan Old Style, Apple Garamond, Baskerville, Georgia, Times New Roman, Droid Serif, Times, Source Serif Pro, serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
|
||||
--font-mono: Menlo, Cascadia Code, Consolas, Monaco, Liberation Mono, Lucida Console, monospace;
|
||||
}
|
||||
|
||||
html {
|
||||
@ -13,8 +11,6 @@ html {
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 17px;
|
||||
line-height: 25px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
@ -68,3 +64,57 @@ html.custom_scroll ::-webkit-scrollbar-thumb:hover {
|
||||
#marker_shadow {
|
||||
position: absolute;
|
||||
}
|
||||
#search_venues {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 2;
|
||||
top: -30px;
|
||||
transition: top 200ms ease-in-out;
|
||||
}
|
||||
#search_venues.shown {
|
||||
top: 6px;
|
||||
}
|
||||
#search_venues_inner {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
background: var(--td-window-bg);
|
||||
color: var(--td-window-active-text-fg);
|
||||
cursor: pointer;
|
||||
border-radius: 14px;
|
||||
padding: 5px 12px 6px;
|
||||
box-shadow: 0 0 3px 0px var(--td-history-to-down-shadow);
|
||||
}
|
||||
#search_venues_inner:hover {
|
||||
background: var(--td-window-bg-over);
|
||||
}
|
||||
#search_venues_content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
#search_venues_content:before {
|
||||
content: var(--td-lng-maps-places-in-area);
|
||||
}
|
||||
#search_venues_inner .ripple .inner {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
transform: scale(0);
|
||||
opacity: 1;
|
||||
animation: ripple 650ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
||||
background-color: var(--td-window-bg-ripple);
|
||||
}
|
||||
#search_venues_inner .ripple.hiding {
|
||||
animation: fadeOut 200ms linear forwards;
|
||||
}
|
||||
@keyframes ripple {
|
||||
to {
|
||||
transform: scale(2);
|
||||
}
|
||||
}
|
||||
@keyframes fadeOut {
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
@ -72,6 +72,7 @@ var LocationPicker = {
|
||||
LocationPicker.map = new mapboxgl.Map(options);
|
||||
LocationPicker.createMarker(center);
|
||||
LocationPicker.trackMovement();
|
||||
LocationPicker.initSearchVenueRipple();
|
||||
},
|
||||
marker: function() {
|
||||
return document.getElementById('marker_drop');
|
||||
@ -93,13 +94,14 @@ var LocationPicker = {
|
||||
LocationPicker.map.on('movestart', function() {
|
||||
LocationPicker.marker().classList.add('moving');
|
||||
LocationPicker.clearMovingTimer();
|
||||
LocationPicker.notify({ event: 'movestart' });
|
||||
LocationPicker.toggleSearchVenues(false);
|
||||
LocationPicker.notify({ event: 'move_start' });
|
||||
});
|
||||
LocationPicker.map.on('moveend', function() {
|
||||
LocationPicker.startMovingTimer(function() {
|
||||
LocationPicker.marker().classList.remove('moving');
|
||||
LocationPicker.notify({
|
||||
event: 'moveend',
|
||||
event: 'move_end',
|
||||
latitude: LocationPicker.map.getCenter().lat,
|
||||
longitude: LocationPicker.map.getCenter().lng
|
||||
});
|
||||
@ -119,5 +121,79 @@ var LocationPicker = {
|
||||
latitude: LocationPicker.map.getCenter().lat,
|
||||
longitude: LocationPicker.map.getCenter().lng
|
||||
});
|
||||
},
|
||||
addRipple: function (button, x, y) {
|
||||
const ripple = document.createElement('span');
|
||||
ripple.classList.add('ripple');
|
||||
|
||||
const inner = document.createElement('span');
|
||||
inner.classList.add('inner');
|
||||
|
||||
var rect = button.getBoundingClientRect();
|
||||
x -= rect.x;
|
||||
y -= rect.y;
|
||||
|
||||
const mx = button.clientWidth - x;
|
||||
const my = button.clientHeight - y;
|
||||
const sq1 = x * x + y * y;
|
||||
const sq2 = mx * mx + y * y;
|
||||
const sq3 = x * x + my * my;
|
||||
const sq4 = mx * mx + my * my;
|
||||
const radius = Math.sqrt(Math.max(sq1, sq2, sq3, sq4));
|
||||
|
||||
inner.style.width = inner.style.height = `${2 * radius}px`;
|
||||
inner.style.left = `${x - radius}px`;
|
||||
inner.style.top = `${y - radius}px`;
|
||||
inner.classList.add('inner');
|
||||
|
||||
ripple.addEventListener('animationend', function (e) {
|
||||
if (e.animationName === 'fadeOut') {
|
||||
ripple.remove();
|
||||
}
|
||||
});
|
||||
|
||||
ripple.appendChild(inner);
|
||||
button.appendChild(ripple);
|
||||
},
|
||||
stopRipples: function (button) {
|
||||
const id = button.id ? button.id : button;
|
||||
button = document.getElementById(id);
|
||||
const ripples = button.getElementsByClassName('ripple');
|
||||
for (var i = 0; i < ripples.length; ++i) {
|
||||
const ripple = ripples[i];
|
||||
if (!ripple.classList.contains('hiding')) {
|
||||
ripple.classList.add('hiding');
|
||||
}
|
||||
}
|
||||
},
|
||||
initSearchVenueRipple: function() {
|
||||
var button = document.getElementById('search_venues_inner');
|
||||
button.addEventListener('mousedown', function (e) {
|
||||
LocationPicker.addRipple(e.currentTarget, e.clientX, e.clientY);
|
||||
LocationPicker.searchVenuesPressed = true;
|
||||
});
|
||||
button.addEventListener('mouseup', function (e) {
|
||||
const id = e.currentTarget.id;
|
||||
setTimeout(function () {
|
||||
LocationPicker.stopRipples(id);
|
||||
}, 0);
|
||||
if (LocationPicker.searchVenuesPressed) {
|
||||
LocationPicker.searchVenuesPressed = false;
|
||||
LocationPicker.toggleSearchVenues(false);
|
||||
LocationPicker.notify({
|
||||
event: 'search_venues',
|
||||
latitude: LocationPicker.map.getCenter().lat,
|
||||
longitude: LocationPicker.map.getCenter().lng
|
||||
});
|
||||
}
|
||||
});
|
||||
button.addEventListener('mouseleave', function (e) {
|
||||
LocationPicker.stopRipples(e.currentTarget);
|
||||
LocationPicker.searchVenuesPressed = false;
|
||||
});
|
||||
},
|
||||
toggleSearchVenues: function(shown) {
|
||||
var button = document.getElementById('search_venues');
|
||||
button.classList.toggle('shown', shown);
|
||||
},
|
||||
};
|
||||
|
@ -148,7 +148,7 @@ void ResolveLocationAddressGeneric(
|
||||
}
|
||||
}
|
||||
};
|
||||
add({ u"address"_q, u"street"_q, u"neighborhood"_q });
|
||||
add({ /*u"address"_q, u"street"_q, */u"neighborhood"_q });
|
||||
add({ u"place"_q, u"region"_q });
|
||||
add({ u"country"_q });
|
||||
finishWith({ .name = names.join(", ") });
|
||||
@ -215,4 +215,29 @@ void ResolveLocationAddress(
|
||||
Platform::ResolveLocationAddress(location, language, std::move(done));
|
||||
}
|
||||
|
||||
bool AreTheSame(const GeoLocation &a, const GeoLocation &b) {
|
||||
if (a.accuracy != GeoLocationAccuracy::Exact
|
||||
|| b.accuracy != GeoLocationAccuracy::Exact) {
|
||||
return false;
|
||||
}
|
||||
const auto normalize = [](float64 value) {
|
||||
value = std::fmod(value + 180., 360.);
|
||||
return (value + (value < 0. ? 360. : 0.)) - 180.;
|
||||
};
|
||||
constexpr auto kEpsilon = 0.0001;
|
||||
const auto lon1 = normalize(a.point.y());
|
||||
const auto lon2 = normalize(b.point.y());
|
||||
const auto diffLat = std::abs(a.point.x() - b.point.x());
|
||||
if (std::abs(a.point.x()) >= (90. - kEpsilon)
|
||||
|| std::abs(b.point.x()) >= (90. - kEpsilon)) {
|
||||
return diffLat <= kEpsilon;
|
||||
}
|
||||
auto diffLon = std::abs(lon1 - lon2);
|
||||
if (diffLon > 180.) {
|
||||
diffLon = 360. - diffLon;
|
||||
}
|
||||
|
||||
return diffLat <= kEpsilon && diffLon <= kEpsilon;
|
||||
}
|
||||
|
||||
} // namespace Core
|
||||
|
@ -33,12 +33,10 @@ struct GeoLocation {
|
||||
explicit operator bool() const {
|
||||
return !failed();
|
||||
}
|
||||
|
||||
friend inline bool operator==(
|
||||
const GeoLocation&,
|
||||
const GeoLocation&) = default;
|
||||
};
|
||||
|
||||
[[nodiscard]] bool AreTheSame(const GeoLocation &a, const GeoLocation &b);
|
||||
|
||||
struct GeoAddress {
|
||||
QString name;
|
||||
|
||||
|
@ -102,6 +102,7 @@ VenueRow::VenueRow(
|
||||
void VenueRow::update(const VenueData &data) {
|
||||
_data = data;
|
||||
setCustomStatus(data.address);
|
||||
refreshName(st::pickLocationVenueItem);
|
||||
}
|
||||
|
||||
VenueData VenueRow::data() const {
|
||||
@ -128,12 +129,12 @@ PaintRoundImageCallback VenueRow::generatePaintUserpicCallback(
|
||||
};
|
||||
}
|
||||
|
||||
class LinksController final
|
||||
class VenuesController final
|
||||
: public PeerListController
|
||||
, public VenueRowDelegate
|
||||
, public base::has_weak_ptr {
|
||||
public:
|
||||
LinksController(
|
||||
VenuesController(
|
||||
not_null<Main::Session*> session,
|
||||
rpl::producer<std::vector<VenueData>> content);
|
||||
|
||||
@ -176,21 +177,21 @@ private:
|
||||
return query.trimmed().toLower();
|
||||
}
|
||||
|
||||
LinksController::LinksController(
|
||||
VenuesController::VenuesController(
|
||||
not_null<Main::Session*> session,
|
||||
rpl::producer<std::vector<VenueData>> content)
|
||||
: _session(session)
|
||||
, _rows(std::move(content)) {
|
||||
}
|
||||
|
||||
void LinksController::prepare() {
|
||||
void VenuesController::prepare() {
|
||||
_rows.value(
|
||||
) | rpl::start_with_next([=](const std::vector<VenueData> &rows) {
|
||||
rebuild(rows);
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void LinksController::rebuild(const std::vector<VenueData> &rows) {
|
||||
void VenuesController::rebuild(const std::vector<VenueData> &rows) {
|
||||
auto i = 0;
|
||||
auto count = delegate()->peerListFullRowsCount();
|
||||
while (i < rows.size()) {
|
||||
@ -209,24 +210,24 @@ void LinksController::rebuild(const std::vector<VenueData> &rows) {
|
||||
delegate()->peerListRefreshRows();
|
||||
}
|
||||
|
||||
void LinksController::rowClicked(not_null<PeerListRow*> row) {
|
||||
void VenuesController::rowClicked(not_null<PeerListRow*> row) {
|
||||
const auto venue = static_cast<VenueRow*>(row.get())->data();
|
||||
venue;
|
||||
}
|
||||
|
||||
void LinksController::rowRightActionClicked(not_null<PeerListRow*> row) {
|
||||
void VenuesController::rowRightActionClicked(not_null<PeerListRow*> row) {
|
||||
delegate()->peerListShowRowMenu(row, true);
|
||||
}
|
||||
|
||||
Main::Session &LinksController::session() const {
|
||||
Main::Session &VenuesController::session() const {
|
||||
return *_session;
|
||||
}
|
||||
|
||||
void LinksController::appendRow(const VenueData &data) {
|
||||
void VenuesController::appendRow(const VenueData &data) {
|
||||
delegate()->peerListAppendRow(std::make_unique<VenueRow>(this, data));
|
||||
}
|
||||
|
||||
void LinksController::rowPaintIcon(
|
||||
void VenuesController::rowPaintIcon(
|
||||
QPainter &p,
|
||||
int x,
|
||||
int y,
|
||||
@ -331,6 +332,7 @@ void LinksController::rowPaintIcon(
|
||||
{ "window-bg-over", &st::windowBgOver },
|
||||
{ "window-bg-ripple", &st::windowBgRipple },
|
||||
{ "window-active-text-fg", &st::windowActiveTextFg },
|
||||
{ "history-to-down-shadow", &st::historyToDownShadow },
|
||||
};
|
||||
static const auto phrases = base::flat_map<QByteArray, tr::phrase<>>{
|
||||
{ "maps-places-in-area", tr::lng_maps_places_in_area },
|
||||
@ -358,6 +360,9 @@ void LinksController::rowPaintIcon(
|
||||
<link href='https://api.mapbox.com/mapbox-gl-js/v3.4.0/mapbox-gl.css' rel='stylesheet' />
|
||||
</head>
|
||||
<body>
|
||||
<div id="search_venues">
|
||||
<div id="search_venues_inner"><span id="search_venues_content"></span></div>
|
||||
</div>
|
||||
<div id="marker">
|
||||
<div id="marker_shadow" style="transform: translate(0px, -14px);">
|
||||
<svg display="block" height="41px" width="27px" viewBox="0 0 27 41">
|
||||
@ -467,7 +472,7 @@ void SetupVenues(
|
||||
auto &lifetime = container->lifetime();
|
||||
const auto delegate = lifetime.make_state<PeerListContentDelegateShow>(
|
||||
show);
|
||||
const auto controller = lifetime.make_state<LinksController>(
|
||||
const auto controller = lifetime.make_state<VenuesController>(
|
||||
&show->session(),
|
||||
std::move(value));
|
||||
controller->setStyleOverrides(&st::pickLocationVenueList);
|
||||
@ -687,18 +692,41 @@ void LocationPicker::setupWebview(const Descriptor &descriptor) {
|
||||
const auto lon = object.value("longitude").toDouble();
|
||||
_callback({ lat, lon });
|
||||
close();
|
||||
} else if (event == u"movestart"_q) {
|
||||
} else if (event == u"move_start"_q) {
|
||||
if (const auto now = _geocoderAddress.current()
|
||||
; !now.isEmpty()) {
|
||||
_geocoderSavedAddress = now;
|
||||
_geocoderAddress = QString();
|
||||
}
|
||||
base::take(_geocoderResolvePostponed);
|
||||
_geocoderResolveTimer.cancel();
|
||||
} else if (event == u"moveend"_q) {
|
||||
} else if (event == u"move_end"_q) {
|
||||
const auto lat = object.value("latitude").toDouble();
|
||||
const auto lon = object.value("longitude").toDouble();
|
||||
_geocoderResolvePostponed = Core::GeoLocation{
|
||||
const auto location = Core::GeoLocation{
|
||||
.point = { lat, lon },
|
||||
.accuracy = Core::GeoLocationAccuracy::Exact,
|
||||
};
|
||||
if (AreTheSame(_geocoderResolvingFor, location)
|
||||
&& !_geocoderSavedAddress.isEmpty()) {
|
||||
_geocoderAddress = base::take(_geocoderSavedAddress);
|
||||
_geocoderResolveTimer.cancel();
|
||||
} else {
|
||||
_geocoderResolvePostponed = location;
|
||||
_geocoderResolveTimer.callOnce(kResolveAddressDelay);
|
||||
}
|
||||
if (!AreTheSame(_venuesRequestLocation, location)) {
|
||||
_webview->eval(
|
||||
"LocationPicker.toggleSearchVenues(true);");
|
||||
}
|
||||
} else if (event == u"search_venues"_q) {
|
||||
const auto lat = object.value("latitude").toDouble();
|
||||
const auto lon = object.value("longitude").toDouble();
|
||||
venuesRequest({
|
||||
.point = { lat, lon },
|
||||
.accuracy = Core::GeoLocationAccuracy::Exact,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
raw->setDataRequestHandler([=](Webview::DataRequest request) {
|
||||
@ -759,12 +787,12 @@ void LocationPicker::resolveAddressByTimer() {
|
||||
}
|
||||
|
||||
void LocationPicker::resolveAddress(Core::GeoLocation location) {
|
||||
if (_geocoderResolvingFor == location) {
|
||||
if (AreTheSame(_geocoderResolvingFor, location)) {
|
||||
return;
|
||||
}
|
||||
_geocoderResolvingFor = location;
|
||||
const auto done = [=](Core::GeoAddress address) {
|
||||
if (_geocoderResolvingFor != location) {
|
||||
if (!AreTheSame(_geocoderResolvingFor, location)) {
|
||||
return;
|
||||
} else if (address) {
|
||||
_geocoderAddress = address.name;
|
||||
@ -809,14 +837,13 @@ void LocationPicker::venuesRequest(
|
||||
QString query) {
|
||||
query = NormalizeVenuesQuery(query);
|
||||
auto &cache = _venuesCache[query];
|
||||
const auto i = ranges::find(
|
||||
cache,
|
||||
location,
|
||||
&VenuesCacheEntry::location);
|
||||
const auto i = ranges::find_if(cache, [&](const VenuesCacheEntry &v) {
|
||||
return AreTheSame(v.location, location);
|
||||
});
|
||||
if (i != end(cache)) {
|
||||
_venueState = i->result;
|
||||
return;
|
||||
} else if (_venuesRequestLocation == location
|
||||
} else if (AreTheSame(_venuesRequestLocation, location)
|
||||
&& _venuesRequestQuery == query) {
|
||||
return;
|
||||
} else if (const auto oldRequestId = base::take(_venuesRequestId)) {
|
||||
@ -888,7 +915,7 @@ void LocationPicker::resolveCurrentLocation() {
|
||||
using namespace Core;
|
||||
const auto window = _window.get();
|
||||
ResolveCurrentGeoLocation(crl::guard(window, [=](GeoLocation location) {
|
||||
const auto changed = (LastExactLocation != location);
|
||||
const auto changed = !AreTheSame(LastExactLocation, location);
|
||||
if (location.accuracy != GeoLocationAccuracy::Exact || !changed) {
|
||||
return;
|
||||
}
|
||||
|
@ -129,6 +129,7 @@ private:
|
||||
base::Timer _geocoderResolveTimer;
|
||||
Core::GeoLocation _geocoderResolvePostponed;
|
||||
Core::GeoLocation _geocoderResolvingFor;
|
||||
QString _geocoderSavedAddress;
|
||||
rpl::variable<QString> _geocoderAddress;
|
||||
|
||||
rpl::variable<PickerVenueState> _venueState;
|
||||
|
Loading…
Reference in New Issue
Block a user