carrier info fix, right widget update, live tracking device upd, bug fixes
All checks were successful
release-tag / release-image (push) Successful in 55s
All checks were successful
release-tag / release-image (push) Successful in 55s
This commit is contained in:
@ -112,6 +112,24 @@ body {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
border: none;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
padding: 15px;
|
||||
color: #fff;
|
||||
background: rgb(187, 179, 170);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(187, 179, 170, 1) 0%,
|
||||
rgba(159, 148, 135, 1) 100%
|
||||
);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.stopdescription {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
@ -687,6 +705,7 @@ body {
|
||||
}
|
||||
|
||||
.dropdown-name {
|
||||
position: relative;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
@ -695,7 +714,6 @@ body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@ -1231,3 +1249,25 @@ li.checked {
|
||||
flex: 0 0 calc((100% - 24px) / 3);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.carrier-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.carrier-img {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.carrier-slogan {
|
||||
font-size: 18px;
|
||||
color: #fff;
|
||||
opacity: 70%;
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@ -74,6 +74,8 @@ export default {
|
||||
manualTramDirection: null,
|
||||
manualDirectionUntil: 0,
|
||||
inactivityTimer: null,
|
||||
isFollowingTram: false,
|
||||
followTramTimer: null,
|
||||
chooseTramDirection(canvasPt) {
|
||||
// canvasPt: [x, y] in PIXI coords
|
||||
let bestDir = "right";
|
||||
@ -141,6 +143,12 @@ export default {
|
||||
resetInactivity() {
|
||||
this.lastActivityTime = Date.now();
|
||||
if (this.videoOverlayVisible) this.hideVideoOverlay();
|
||||
clearTimeout(this.inactivityTimer);
|
||||
// stop following on any user activity
|
||||
this.isFollowingTram = false;
|
||||
this.stopFollowTram();
|
||||
// restart inactivity timer on any user activity
|
||||
this.resetInactivityTimer();
|
||||
},
|
||||
startInactivityCheck() {
|
||||
if (this.inactivityInterval) return;
|
||||
@ -325,20 +333,17 @@ export default {
|
||||
const minLat = Math.min(...lats);
|
||||
const maxLat = Math.max(...lats);
|
||||
const minLng = Math.min(...lngs);
|
||||
const maxLng = Math.max(...lngs);
|
||||
|
||||
const canvas = this.pixiApp.canvas;
|
||||
const width = canvas.width;
|
||||
const height = canvas.height;
|
||||
const padding = 40;
|
||||
|
||||
const scaleX = (width - padding * 2) / (maxLng - minLng || 1);
|
||||
const scaleY = (height - padding * 2) / (maxLat - minLat || 1);
|
||||
let scale = 30000;
|
||||
console.log(scaleX, scaleY, scale);
|
||||
const scale = 35000;
|
||||
const midLat = (minLat + maxLat) / 2;
|
||||
const latFactor = Math.cos((midLat * Math.PI) / 180);
|
||||
console.log(
|
||||
`Using fixed scale ${scale} and latFactor ${latFactor.toFixed(4)}`
|
||||
);
|
||||
|
||||
const toPoint = ([lat, lng]) => {
|
||||
const x = (lng - minLng) * scale + padding;
|
||||
const x = (lng - minLng) * scale * latFactor + padding;
|
||||
const y = (maxLat - lat) * scale + padding;
|
||||
return { x, y };
|
||||
};
|
||||
@ -1079,29 +1084,39 @@ export default {
|
||||
},
|
||||
// reset the inactivity timeout
|
||||
resetInactivityTimer() {
|
||||
console.log("[inactivity] scheduling follow in 15s");
|
||||
clearTimeout(this.inactivityTimer);
|
||||
this.inactivityTimer = setTimeout(() => {
|
||||
this.isFollowingTram = true;
|
||||
this.startFollowTram();
|
||||
}, 15000);
|
||||
},
|
||||
|
||||
startFollowTram() {
|
||||
console.log("[follow] startFollowTram() triggered");
|
||||
if (this.followTramTimer) return;
|
||||
this.followTramTimer = setInterval(() => {
|
||||
if (this.tramContainer) {
|
||||
// compute tram position in viewport world coordinates
|
||||
const localPoint = new PIXI.Point(
|
||||
const local = new PIXI.Point(
|
||||
this.tramContainer.x,
|
||||
this.tramContainer.y
|
||||
);
|
||||
const worldCenter = this.viewport.toLocal(
|
||||
localPoint,
|
||||
this.routeGroup
|
||||
);
|
||||
// smooth pan and zoom to tram
|
||||
const worldCenter = this.viewport.toLocal(local, this.routeGroup);
|
||||
this.viewport.animate({
|
||||
time: 600,
|
||||
position: { x: worldCenter.x, y: worldCenter.y },
|
||||
scale: 1,
|
||||
easing: "easeInOutSine",
|
||||
});
|
||||
// schedule next recenter
|
||||
this.resetInactivityTimer();
|
||||
}
|
||||
}, 15000);
|
||||
}, 500);
|
||||
},
|
||||
stopFollowTram() {
|
||||
console.log("[follow] stopFollowTram() triggered");
|
||||
if (this.followTramTimer) {
|
||||
clearInterval(this.followTramTimer);
|
||||
this.followTramTimer = null;
|
||||
}
|
||||
},
|
||||
|
||||
// start watching for user interactions
|
||||
@ -1125,6 +1140,10 @@ export default {
|
||||
clearInterval(this.inactivityInterval);
|
||||
this.inactivityInterval = null;
|
||||
}
|
||||
if (this.followTramTimer) {
|
||||
clearInterval(this.followTramTimer);
|
||||
this.followTramTimer = null;
|
||||
}
|
||||
["mousemove", "mousedown", "keydown", "touchstart", "scroll"].forEach(
|
||||
(evt) => window.removeEventListener(evt, this.resetInactivity)
|
||||
);
|
||||
|
@ -43,6 +43,26 @@
|
||||
class="watermark watermark-rd"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
v-if="showBackButton"
|
||||
class="back-button"
|
||||
@click="returnToNearestSight"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="12"
|
||||
height="21"
|
||||
fill="none"
|
||||
viewBox="0 0 12 21"
|
||||
>
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M3.53 10.574c.188.135.316.203.414.3 2.486 2.495 4.966 4.992 7.451 7.486.445.447.623.96.44 1.574a1.485 1.485 0 0 1-2.383.716c-.09-.076-.171-.16-.254-.244L.658 11.82c-.878-.885-.877-1.673.003-2.56C3.539 6.364 6.414 3.463 9.307.58c.252-.25.606-.46.948-.542.646-.154 1.26.184 1.563.75.307.566.22 1.274-.238 1.764-.352.378-.725.736-1.09 1.101-2.195 2.204-4.389 4.407-6.585 6.609-.082.083-.179.15-.374.312Z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{{ t("back") }}
|
||||
</button>
|
||||
<span class="stopname">{{ stopName }}</span>
|
||||
<span class="stopdescription">{{ selectedArticleBody }}</span>
|
||||
<div class="stoparticles">
|
||||
@ -261,6 +281,11 @@ export default {
|
||||
defaultImageUrl:
|
||||
"https://lh3.googleusercontent.com/gps-cs-s/AB5caB8lUwofb2NIg6n0-cEl8nIWsySAUc52KNj4XezuOdo-aeqTgQlD1kTVa5MaynL2Yg4ByoTYTKNTR7K59f7kjzU9yzpudstjRiT2F6M_ilxFYFpcvMZz6OwlRFF2MrsCPSwUa7vqew=s680-w680-h510",
|
||||
sightId: 17,
|
||||
manualSightId: null,
|
||||
nearestSightId: null,
|
||||
returnTimer: null,
|
||||
lastUserActivity: Date.now(),
|
||||
firstLoad: true,
|
||||
stopName: "",
|
||||
watermarkLU: "",
|
||||
watermarkRD: "",
|
||||
@ -291,10 +316,10 @@ export default {
|
||||
routeProgress: null,
|
||||
routeId: null,
|
||||
selectedLang: localStorage.getItem("selectedLangRight") || "ru",
|
||||
showLangToggle: true, // видна «глобус»-кнопка
|
||||
showLanguageOptions: false, // виден блок из трёх иконок
|
||||
languageOptionsTimer: null, // таймер на 10 с
|
||||
langRevertTimer: null, // таймер на 30 с
|
||||
showLangToggle: true,
|
||||
showLanguageOptions: false,
|
||||
languageOptionsTimer: null,
|
||||
langRevertTimer: null,
|
||||
icons: {
|
||||
ru: `<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="none" viewBox="0 0 28 28">
|
||||
<path fill="#fff" d="M14 0C6.27 0 0 6.27 0 14s6.27 14 14 14 14-6.27 14-14S21.73 0 14 0Zm.117 19.57H11.62l-2.117-4.135H7.646v4.136H5.32V8.27h4.2c1.336 0 2.363.298 3.092.893.729.595 1.085 1.435 1.085 2.52 0 .77-.17 1.412-.502 1.931-.332.513-.84.928-1.517 1.23l2.444 4.62v.112l-.005-.006Zm9.391-3.855c0 1.237-.385 2.217-1.16 2.934-.776.718-1.832 1.08-3.174 1.08-1.341 0-2.368-.35-3.144-1.05-.776-.7-1.173-1.657-1.19-2.882V8.272h2.328v7.46c0 .741.175 1.278.53 1.616.356.339.846.508 1.47.508 1.307 0 1.972-.689 1.995-2.065V8.27h2.334v7.444h.011Z"/>
|
||||
@ -310,7 +335,7 @@ export default {
|
||||
},
|
||||
translations: {
|
||||
sights: { ru: "Достопримечательности", en: "Landmarks", zh: "景点" },
|
||||
// при необходимости — другие подписи
|
||||
back: { ru: "Вернуться назад", en: "Back", zh: "返回" },
|
||||
},
|
||||
};
|
||||
},
|
||||
@ -352,6 +377,9 @@ export default {
|
||||
Object.entries(this.nextStopTransfers).filter(([, value]) => value)
|
||||
);
|
||||
},
|
||||
showBackButton() {
|
||||
return this.sightId !== this.nearestSightId;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
t(key) {
|
||||
@ -513,86 +541,14 @@ export default {
|
||||
}
|
||||
},
|
||||
async openSightCardDetails(id) {
|
||||
// закрыть, если нажали повторно
|
||||
if (this.selectedSightCardId === id) {
|
||||
this.selectedSightCardId = null;
|
||||
this.cardDetail = null;
|
||||
return;
|
||||
}
|
||||
this.selectedSightCardId = id;
|
||||
this.manualSightId = id;
|
||||
this.sightId = id;
|
||||
this.showSightsList = false;
|
||||
this.selectedSightCardId = null;
|
||||
this.cardDetail = null;
|
||||
this.resetSightsInactivityTimer();
|
||||
|
||||
try {
|
||||
const [detailRes, articlesRes] = await Promise.all([
|
||||
axios.get(this.addLangParam(`${API_URL}/sight/${id}`)),
|
||||
axios.get(this.addLangParam(`${API_URL}/sight/${id}/article`)),
|
||||
]);
|
||||
|
||||
// Fetch media for every article
|
||||
const articlesWithMedia = await Promise.all(
|
||||
articlesRes.data.map(async (article) => {
|
||||
try {
|
||||
const mediaRes = await axios.get(
|
||||
this.addLangParam(`${API_URL}/article/${article.id}/media`)
|
||||
);
|
||||
return { ...article, media: mediaRes.data };
|
||||
} catch (mediaErr) {
|
||||
console.error(
|
||||
`Failed to fetch media for article ${article.id}:`,
|
||||
mediaErr
|
||||
);
|
||||
return { ...article, media: [] };
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Console output: sight name, articles, and their media
|
||||
console.log("Sight clicked:", {
|
||||
name: detailRes.data.name,
|
||||
articles: articlesWithMedia,
|
||||
});
|
||||
|
||||
// Choose preview image: prefer the first article's first media, otherwise fall back to sight thumbnail
|
||||
let imageUrl = "";
|
||||
if (
|
||||
articlesWithMedia.length > 0 &&
|
||||
articlesWithMedia[0].media &&
|
||||
articlesWithMedia[0].media.length > 0
|
||||
) {
|
||||
const firstMediaId = articlesWithMedia[0].media[0].id;
|
||||
try {
|
||||
imageUrl = await this.getMediaBlobUrl(firstMediaId);
|
||||
} catch {
|
||||
imageUrl = this.addLangParam(
|
||||
`${API_URL}/media/${firstMediaId}/download`
|
||||
);
|
||||
}
|
||||
} else if (detailRes.data.thumbnail) {
|
||||
try {
|
||||
imageUrl = await this.getMediaBlobUrl(detailRes.data.thumbnail);
|
||||
} catch {
|
||||
imageUrl = this.addLangParam(
|
||||
`${API_URL}/media/${detailRes.data.thumbnail}/download`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.cardDetail = {
|
||||
name: detailRes.data.name,
|
||||
imageUrl,
|
||||
articles: articlesWithMedia,
|
||||
};
|
||||
if (articlesWithMedia.length > 0) {
|
||||
this.selectedSightArticleId = articlesWithMedia[0].id;
|
||||
this.selectedSightArticleBody = articlesWithMedia[0].body;
|
||||
} else {
|
||||
this.selectedSightArticleId = null;
|
||||
this.selectedSightArticleBody = "";
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to load sight card details:", err);
|
||||
}
|
||||
this.resetReturnTimer();
|
||||
await this.fetchSightInfo();
|
||||
await this.fetchArticles();
|
||||
},
|
||||
async selectSightArticle(id) {
|
||||
this.selectedSightArticleId = id;
|
||||
@ -657,10 +613,12 @@ export default {
|
||||
}, 300_000); // 5 m
|
||||
},
|
||||
handleUserActivity() {
|
||||
this.lastUserActivity = Date.now();
|
||||
if (this.showSightsList || this.cardDetail)
|
||||
this.resetSightsInactivityTimer();
|
||||
this.resetArticleInactivityTimer();
|
||||
this.resetLangRevertTimer();
|
||||
if (this.returnTimer) this.resetReturnTimer();
|
||||
},
|
||||
selectArticle(id) {
|
||||
this.resetArticleInactivityTimer();
|
||||
@ -738,23 +696,41 @@ export default {
|
||||
newSightId = this.sights[1].id;
|
||||
}
|
||||
}
|
||||
if (newSightId && newSightId !== this.sightId) {
|
||||
this.sightId = newSightId;
|
||||
await this.fetchSightInfo();
|
||||
await this.fetchArticles();
|
||||
if (newSightId) {
|
||||
if (this.firstLoad) {
|
||||
this.nearestSightId = newSightId;
|
||||
this.sightId = newSightId;
|
||||
await this.fetchSightInfo();
|
||||
await this.fetchArticles();
|
||||
this.firstLoad = false;
|
||||
this.clearReturnTimer();
|
||||
return;
|
||||
}
|
||||
if (this.nearestSightId !== newSightId) {
|
||||
this.nearestSightId = newSightId;
|
||||
}
|
||||
if (!this.manualSightId) {
|
||||
if (this.sightId !== this.nearestSightId) {
|
||||
const userActive = Date.now() - this.lastUserActivity < 15_000; // 15-сек. окно
|
||||
if (userActive) {
|
||||
this.resetReturnTimer();
|
||||
} else {
|
||||
this.clearReturnTimer();
|
||||
this.sightId = this.nearestSightId;
|
||||
await this.fetchSightInfo();
|
||||
await this.fetchArticles();
|
||||
}
|
||||
} else {
|
||||
this.clearReturnTimer();
|
||||
}
|
||||
}
|
||||
}
|
||||
const nextStopId = response.data.routeProgress?.endStopId;
|
||||
// console.log("Fetched next stop ID:", nextStopId);
|
||||
// console.log("Stops:", this.stops);
|
||||
if (nextStopId && this.stops) {
|
||||
const nextStop = this.stops.find((stop) => stop.id == nextStopId);
|
||||
// console.log("Fetched next stop ID:", nextStopId);
|
||||
// console.log("Matching stop:", nextStop);
|
||||
if (nextStop && nextStop.transfers) {
|
||||
// console.log("Transfers at next stop:", nextStop.transfers);
|
||||
this.nextStopTransfers = nextStop.transfers;
|
||||
} else {
|
||||
// console.log("No transfers found at next stop");
|
||||
this.nextStopTransfers = null;
|
||||
}
|
||||
}
|
||||
@ -762,6 +738,29 @@ export default {
|
||||
console.error("Error fetching geolocation context:", error);
|
||||
}
|
||||
},
|
||||
resetReturnTimer() {
|
||||
this.clearReturnTimer();
|
||||
this.returnTimer = setTimeout(() => {
|
||||
this.returnToNearestSight();
|
||||
}, 90_000);
|
||||
},
|
||||
clearReturnTimer() {
|
||||
if (this.returnTimer) {
|
||||
clearTimeout(this.returnTimer);
|
||||
this.returnTimer = null;
|
||||
}
|
||||
},
|
||||
returnToNearestSight() {
|
||||
if (!this.nearestSightId) return;
|
||||
this.manualSightId = null;
|
||||
this.sightId = this.nearestSightId;
|
||||
this.clearReturnTimer();
|
||||
this.showSightsList = false;
|
||||
this.selectedSightCardId = null;
|
||||
this.cardDetail = null;
|
||||
this.fetchSightInfo();
|
||||
this.fetchArticles();
|
||||
},
|
||||
selectLetter(letter) {
|
||||
const anchorArr = this.$refs[`letter-${letter}`];
|
||||
const anchor = anchorArr ? anchorArr[0] : null;
|
||||
@ -818,6 +817,7 @@ export default {
|
||||
async mounted() {
|
||||
await this.fetchSights();
|
||||
await this.fetchGeolocationContext();
|
||||
this.nearestSightId = this.sightId;
|
||||
this.geolocationInterval = setInterval(() => {
|
||||
this.fetchGeolocationContext();
|
||||
}, 1000);
|
||||
|
373
src/icons/spb-gerb.svg
Normal file
373
src/icons/spb-gerb.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 176 KiB |
Reference in New Issue
Block a user