Files
BusSchedule127/web/index.html
ua1zbe 6f591c10d5 Убрана надпись «Мурманская область», экран не гаснет
В шапке приложения убрана надпись «Мурманская область» вместе с её
стилем .route, который больше не используется. Заголовок
«Автобус №127» теперь начинает шапку.

В MainActivity добавлен флаг FLAG_KEEP_SCREEN_ON: экран телефона
не гаснет, пока открыто приложение с расписанием — не нужно
касаться экрана, чтобы посмотреть время отправления.

Также в этом же релизе переключатель дней в полном расписании
(Будни / Суббота / Воскресенье) из предыдущих коммитов.
2026-09-28 10:12:35 +00:00

354 lines
16 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Автобус №127 · Кандалакша — Полярные Зори — Зашеек</title>
<style>
:root{
--bg:#0d1220; --bg2:#131a2e; --card:#182238; --card2:#1e2a45;
--line:#2a3757; --txt:#e8edf7; --muted:#8b98b8;
--accent:#38d39f; --accent2:#4f9cff; --warn:#ffb648;
}
*{box-sizing:border-box}
body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Ubuntu,sans-serif;
background:radial-gradient(1200px 600px at 70% -10%,#1b2a4d 0%,var(--bg) 55%);
color:var(--txt);min-height:100vh;padding:24px 16px 48px}
.wrap{max-width:760px;margin:0 auto}
header{text-align:center;margin-bottom:18px}
h1{font-size:clamp(22px,4.5vw,30px);margin:0 0 4px;font-weight:700}
.sub{color:var(--muted);font-size:14px}
.clock{font-size:22px;font-weight:600;margin-top:10px;color:var(--accent)}
.clock small{color:var(--muted);font-weight:400;font-size:12px}
.day{display:inline-block;margin-top:8px;padding:4px 12px;border-radius:999px;
font-size:12px;border:1px solid var(--line);color:var(--muted)}
.day b{color:var(--accent2)}
.pickers{display:flex;gap:8px;margin:16px 0}
.pick{flex:1;padding:13px 8px;border-radius:12px;border:1px solid var(--line);
background:var(--card);color:var(--muted);font-size:14px;cursor:pointer;
transition:.15s;text-align:center;font-weight:600}
.pick:hover{border-color:var(--accent2);color:var(--txt)}
.pick.active{background:linear-gradient(135deg,var(--accent2),var(--accent));
color:#04101c;border-color:transparent;box-shadow:0 6px 20px rgba(79,156,255,.25)}
.where{text-align:center;color:var(--muted);font-size:13px;margin:2px 0 12px}
.dir{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.dir .card:only-child{grid-column:1 / -1} /* одна карточка — во всю ширину */
.card{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:14px 16px}
.card h3{margin:0 0 4px;font-size:15px;font-weight:700}
.card h3 .arr{color:var(--accent)}
.card .via{color:var(--muted);font-size:11px;margin-bottom:10px;letter-spacing:.03em}
.trip{display:flex;align-items:baseline;justify-content:space-between;padding:8px 0;
border-bottom:1px dashed var(--line);font-size:15px}
.trip:last-child{border-bottom:none}
.trip .t{font-variant-numeric:tabular-nums;font-weight:700;font-size:19px}
.trip .t small{font-weight:400;color:var(--muted);font-size:11px;display:block}
.trip .w{color:var(--accent);font-size:12px;font-weight:600;white-space:nowrap}
.trip.soon .w{color:var(--warn)}
.empty{color:var(--muted);font-size:13px;padding:6px 0}
/* кнопка полного расписания */
.fullbtn{display:block;width:100%;margin:16px 0 6px;padding:12px;border-radius:12px;
border:1px solid var(--line);background:var(--card2);color:var(--txt);
font-size:14px;font-weight:600;cursor:pointer;transition:.15s}
.fullbtn:hover{border-color:var(--accent2)}
.fullbtn .chev{float:right;transition:.2s;color:var(--muted)}
.fullbtn.open .chev{transform:rotate(180deg)}
.tablecard{background:var(--card);border:1px solid var(--line);border-radius:14px;
overflow:hidden;display:none;margin-bottom:8px}
/* переключатель типа дня в полном расписании */
.days{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;padding:12px 12px 10px;
border-bottom:1px solid var(--line)}
.days button{padding:9px 4px;border-radius:10px;border:1px solid var(--line);
background:var(--card2);color:var(--muted);font-size:13px;font-weight:600;
cursor:pointer;transition:.15s;font-family:inherit}
.days button:hover{border-color:var(--accent2);color:var(--txt)}
.days button.active{background:linear-gradient(135deg,var(--accent2),var(--accent));
color:#04101c;border-color:transparent}
.thead{display:grid;grid-template-columns:1fr 1fr 1fr 1fr;background:var(--card2);
font-size:11px;color:var(--muted);text-align:center}
.thead div{padding:11px 6px;border-left:1px solid var(--line)}
.thead div:first-child{border-left:none}
.thead b{display:block;color:var(--txt);font-size:12px;font-weight:600}
.row{display:grid;grid-template-columns:1fr 1fr 1fr 1fr;text-align:center;border-top:1px solid var(--line);
font-variant-numeric:tabular-nums;font-size:15px}
.row div{padding:10px 6px;border-left:1px solid var(--line)}
.row div:first-child{border-left:none}
.row:nth-child(even){background:#15203a}
footer{margin-top:22px;text-align:center;color:var(--muted);font-size:12px}
@media(max-width:560px){.dir{grid-template-columns:1fr}.pickers{gap:6px}}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>Автобус №127</h1>
<div class="sub">Кандалакша — Полярные Зори — Зашеек</div>
<div class="clock" id="clock">--:--:-- <small>МСК</small></div>
<div class="day" id="dayBadge"></div>
</header>
<div class="pickers">
<button class="pick" data-stop="Кандалакша">Кандалакша</button>
<button class="pick active" data-stop="Полярные Зори">Полярные Зори</button>
<button class="pick" data-stop="Зашеек">Зашеек</button>
</div>
<div class="where" id="whereLabel">Вы в Полярных Зорях — доступные направления:</div>
<section class="dir" id="dirBox"></section>
<button class="fullbtn" id="fullBtn">Полное расписание <span class="chev">▾</span></button>
<section class="tablecard" id="fullTable">
<div class="days" id="dayPick">
<button data-day="weekday" class="active">Будни</button>
<button data-day="saturday">Суббота</button>
<button data-day="sunday">Воскресенье</button>
</div>
<div class="thead">
<div><b>Кандалакша</b>отпр.</div>
<div><b>Полярные Зори</b>→ Зашеек</div>
<div><b>Зашеек</b>прибытие</div>
<div><b>Полярные Зори</b>→ Кандалакша</div>
</div>
<div id="tableBody"></div>
</section>
<footer>Расписание действует с 30.09.2020 · время московское (МСК)</footer>
</div>
<script>
// Расписание по типам дней недели.
// Колонки: отправление из Кандалакши · Полярные Зори (в Зашеек) ·
// прибытие в Зашеек · Полярные Зори (обратно).
//
// Будни — полная таблица маршрута. По субботе и воскресенью в репозитории
// хранились только отправления из Полярных Зорь и прибытия в Зашеек
// (списки в app/src/main/java/ru/ua1zbe/bus127/MainActivity.kt), поэтому
// первая и четвёртая колонки восстановлены по тем же правилам, что и для
// будней, и требуют сверки с официальным расписанием:
// кол0 = кол1 − 50 мин (Кандалакша → Полярные Зори занимает ровно 50 мин)
// кол3 = кол2 + 20 мин (так разность кол3−кол2 выглядит в 8 строках из 13)
const SCHEDULES = {
weekday: [
["05:20","06:20","06:30","06:50"],
["06:10","07:00","07:25","07:45"],
["07:40","08:30","09:00","09:20"],
["08:00","08:50","09:20","09:50"],
["09:45","10:35","11:05","11:25"],
["11:00","11:50","12:20","12:40"],
["12:30","13:20","13:50","14:10"],
["13:30","14:20","14:50","15:10"],
["14:00","14:50","15:25","15:50"],
["15:30","16:20","17:00","17:25"],
["16:50","17:40","18:10","18:35"],
["18:20","19:10","19:40","20:05"],
["20:20","21:10","21:40","22:05"]
],
saturday: [
["06:10","07:00","07:25","07:45"],
["07:30","08:20","08:45","09:05"],
["08:45","09:35","10:05","10:25"],
["10:10","11:00","11:30","11:50"],
["12:30","13:20","13:50","14:10"],
["14:00","14:50","15:45","16:05"],
["15:30","16:20","16:45","17:05"],
["17:00","17:50","18:20","18:40"],
["18:10","19:00","19:30","19:50"],
["20:20","21:10","21:40","22:00"]
],
sunday: [
["06:10","07:00","07:25","07:45"],
["08:45","09:35","10:05","10:25"],
["12:30","13:20","13:50","14:10"],
["14:00","14:50","15:25","15:45"],
["16:50","17:40","18:10","18:30"],
["20:20","21:10","21:40","22:00"]
]
};
const DAY_KEYS = ["weekday","saturday","sunday"];
const DAY_TITLES = {weekday:"Будни", saturday:"Суббота", sunday:"Воскресенье"};
const TRIPS_SHOWN = 5; // сколько ближайших рейсов показывать в карточке
// Время в пути Полярные Зори ↔ Кандалакша. В таблице расписания его нет:
// в 12 строках из 13 Кандалакша → ПЗ занимает ровно 50 минут
// (в первой строке 60 — по всей видимости опечатка).
const RIDE_PZ_KAND = 50;
// arrs — список прибытий: {label, col} — время из колонки таблицы,
// {label, offset} — «время в пути» от отправления (если col не задан).
const DIRECTIONS = {
// От Кандалакши и Зашейка все рейсы идут через Полярные Зори — одна карточка
"Кандалакша": [
{to:"Полярные Зори, Зашеек", dep:0, via:"через Полярные Зори",
arrs:[{label:"Полярные Зори", col:1},
{label:"Зашеек", col:2}]}
],
"Полярные Зори": [
{to:"Зашеек", dep:1, via:"≈ 30 мин в пути",
arrs:[{label:"Зашеек", col:2}]},
{to:"Кандалакша", dep:3, via:"≈ 50 мин в пути",
arrs:[{label:"Кандалакша", offset:RIDE_PZ_KAND}]}
],
"Зашеек": [
{to:"Полярные Зори, Кандалакша", dep:2, via:"через Полярные Зори",
arrs:[{label:"Полярные Зори", col:3},
{label:"Кандалакша", col:3, offset:RIDE_PZ_KAND}]}
]
};
const DAYS = ["воскресенье","понедельник","вторник","среда","четверг","пятница","суббота"];
function toMin(t){ const [h,m]=t.split(":").map(Number); return h*60+m; }
// Текущее время по МСК, независимо от часового пояса устройства
const MSK_TZ = "Europe/Moscow";
let mskFmt = null;
try {
mskFmt = new Intl.DateTimeFormat("en-US", {
timeZone: MSK_TZ, hourCycle: "h23",
year: "numeric", month: "2-digit", day: "2-digit",
hour: "2-digit", minute: "2-digit", second: "2-digit"
});
} catch(e){ mskFmt = null; }
function nowMs(){
if (mskFmt) {
try {
const parts = {};
mskFmt.formatToParts(new Date()).forEach(x => parts[x.type] = x.value);
const hour = Number(parts.hour) % 24; // на части движков полночь приходит как "24"
return {
hour: hour,
minute: Number(parts.minute),
second: Number(parts.second),
day: new Date(Date.UTC(Number(parts.year), Number(parts.month) - 1,
Number(parts.day))).getUTCDay()
};
} catch(e){ /* ниже — запасной вариант */ }
}
// Запасной вариант: МСК = UTC+3 (переход на летнее время отменён)
const d = new Date(Date.now() + 3*3600*1000);
return { hour: d.getUTCHours(), minute: d.getUTCMinutes(),
second: d.getUTCSeconds(), day: d.getUTCDay() };
}
function fmtHM(m){
const p = n=>String(n).padStart(2,"0");
m = ((m % 1440) + 1440) % 1440; // на случай перехода через полночь
return `${p(Math.floor(m/60))}:${p(m%60)}`;
}
function fmtWait(m){
if(m <= 0) return "отправление сейчас";
const h = Math.floor(m/60), mm = m%60;
if(h === 0) return `через ${mm} мин`;
if(mm === 0) return `через ${h} ч`;
return `через ${h} ч ${mm} мин`;
}
let currentStop = "Полярные Зори";
// Date#getDay(): 0 — воскресенье, 6 — суббота
function dayKeyFor(dow){ return dow===0 ? "sunday" : dow===6 ? "saturday" : "weekday"; }
function sched(){ return SCHEDULES[activeDay]; }
function render(){
const now = nowMs();
const curMin = now.hour*60 + now.minute;
const dirs = DIRECTIONS[currentStop];
const data = sched();
document.getElementById("dirBox").innerHTML = dirs.map(d=>{
const upcoming = data
.map((r,ri)=>{
const dep = toMin(r[d.dep]);
const arrs = d.arrs.map(a=>{
const base = a.col!==undefined ? toMin(r[a.col]) : dep;
return {label:a.label, t:fmtHM(base + (a.offset||0))};
});
return {dep, arrs, ri};
})
.filter(x=>x.dep >= curMin)
.sort((a,b)=>a.dep-b.dep)
.slice(0,TRIPS_SHOWN)
.map(x=>{
const w = x.dep - curMin;
// .trip .t small имеет display:block, поэтому каждый <small> — своя строка
const arr = x.arrs.map(a=>`<small>прибытие ${a.label} ${a.t}</small>`).join("");
return `<div class="trip ${w<15?'soon':''}">
<span class="t">${data[x.ri][d.dep]}${arr}</span>
<span class="w">${fmtWait(w)}</span>
</div>`;
}).join("") || `<div class="empty">на сегодня всё — первый завтра в ${data[0][d.dep]}</div>`;
return `<div class="card">
<h3>→ <span class="arr">${d.to}</span></h3>
<div class="via">${d.via}</div>
${upcoming}
</div>`;
}).join("");
}
function renderTable(){
document.getElementById("tableBody").innerHTML = sched().map(row =>
`<div class="row"><div>${row[0]}</div><div>${row[1]}</div><div>${row[2]}</div><div>${row[3]}</div></div>`
).join("");
}
function renderDay(){
const dow = nowMs().day;
document.getElementById("dayBadge").innerHTML = `Сегодня <b>${DAYS[dow]}</b>`;
}
// Подсветка выбранного типа дня и перерисовка расписания
function selectDay(key){
activeDay = key;
document.querySelectorAll("#dayPick button").forEach(b=>
b.classList.toggle("active", b.dataset.day === key));
render();
renderTable();
}
function tickClock(){
const d = nowMs();
const p = n=>String(n).padStart(2,"0");
document.getElementById("clock").innerHTML = `${p(d.hour)}:${p(d.minute)}:${p(d.second)} <small>МСК</small>`;
}
document.querySelectorAll(".pick").forEach(btn=>{
btn.addEventListener("click",()=>{
document.querySelectorAll(".pick").forEach(b=>b.classList.remove("active"));
btn.classList.add("active");
currentStop = btn.dataset.stop;
const w = {Кандалакша:"в Кандалакше", "Полярные Зори":"в Полярных Зорях", Зашеек:"в Зашейке"};
document.getElementById("whereLabel").textContent =
`Вы ${w[currentStop]} — доступные направления:`;
render();
});
});
document.querySelectorAll("#dayPick button").forEach(btn=>{
btn.addEventListener("click",()=>selectDay(btn.dataset.day));
});
document.getElementById("fullBtn").addEventListener("click",()=>{
const t = document.getElementById("fullTable");
const b = document.getElementById("fullBtn");
const open = t.style.display === "block";
t.style.display = open ? "none" : "block";
b.classList.toggle("open", !open);
});
let activeDay = dayKeyFor(nowMs().day); // по умолчанию — реальный день недели
selectDay(activeDay);
tickClock(); renderDay();
setInterval(tickClock,1000);
setInterval(render,30000);
setInterval(renderDay,60000);
</script>
</body>
</html>