carrier info fix, right widget update, live tracking device upd, bug fixes
All checks were successful
release-tag / release-image (push) Successful in 55s

This commit is contained in:
2025-07-13 14:43:24 +03:00
parent 17b95707e7
commit 49576a0be7
5 changed files with 602 additions and 716 deletions

View File

@ -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

View File

@ -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)
);

View File

@ -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.manualSightId = id;
this.sightId = id;
this.showSightsList = false;
this.selectedSightCardId = null;
this.cardDetail = null;
return;
}
this.selectedSightCardId = id;
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) {
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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 176 KiB