17 Commits
v2.0 ... v2.1

Author SHA1 Message Date
5fb1462af6 Сборка: удалён дубликат ресурсов из apk-webview/res/ (перенесены в app/src/main/res/) 2026-09-28 05:18:27 +00:00
e90ea69d44 Сборка: удалён дубликат ресурсов из apk-webview/res/ (перенесены в app/src/main/res/) 2026-09-28 05:18:26 +00:00
297d61b065 Сборка: удалён дубликат ресурсов из apk-webview/res/ (перенесены в app/src/main/res/) 2026-09-28 05:18:25 +00:00
af8107b34c Сборка: удалён дубликат ресурсов из apk-webview/res/ (перенесены в app/src/main/res/) 2026-09-28 05:18:24 +00:00
0f72977cb9 Релиз 2.1: versionCode 3, versionName 2.1 (исправление часового пояса) 2026-09-28 05:18:23 +00:00
886849b949 Сборка: иконки перенесены в app/src/main/res
Ресурсы лежали в apk-webview/res/, откуда плагин Android их не читает,
из-за чего main не собирался: AAPT error: resource mipmap/ic_launcher not found.
Перенесены в стандартный путь app/src/main/res/.
2026-09-28 05:18:22 +00:00
5f8a797a58 Сборка: иконки перенесены в app/src/main/res
Ресурсы лежали в apk-webview/res/, откуда плагин Android их не читает,
из-за чего main не собирался: AAPT error: resource mipmap/ic_launcher not found.
Перенесены в стандартный путь app/src/main/res/.
2026-09-28 05:18:21 +00:00
3859c22086 Сборка: иконки перенесены в app/src/main/res
Ресурсы лежали в apk-webview/res/, откуда плагин Android их не читает,
из-за чего main не собирался: AAPT error: resource mipmap/ic_launcher not found.
Перенесены в стандартный путь app/src/main/res/.
2026-09-28 05:18:20 +00:00
0b9af4b067 Сборка: иконки перенесены в app/src/main/res
Ресурсы лежали в apk-webview/res/, откуда плагин Android их не читает,
из-за чего main не собирался: AAPT error: resource mipmap/ic_launcher not found.
Перенесены в стандартный путь app/src/main/res/.
2026-09-28 05:18:19 +00:00
20ae4b0606 Часовой пояс: время по МСК независимо от часового пояса устройства
Было: nowMs() прибавляла 3 часа к epoch, а getHours()/getDay()
читали результат в местном поясе телефона. На устройстве в МСК
приложение показывало время на 3 часа вперёд.

Стало: Europe/Moscow через Intl.DateTimeFormat, с запасным вариантом
UTC+3, если Intl недоступен.
2026-09-28 05:05:45 +00:00
0dcaf486c0 Часовой пояс: время по МСК независимо от часового пояса устройства
Было: nowMs() прибавляла 3 часа к epoch, а getHours()/getDay()
читали результат в местном поясе телефона. На устройстве в МСК
приложение показывало время на 3 часа вперёд.

Стало: Europe/Moscow через Intl.DateTimeFormat, с запасным вариантом
UTC+3, если Intl недоступен.
2026-09-28 05:05:43 +00:00
588b826b42 AndroidManifest: подключена иконка 2026-09-27 09:13:37 +00:00
5d78f40c3e Иконка приложения (adaptive icon) 2026-09-27 09:13:36 +00:00
362906b1ba Иконка приложения (adaptive icon) 2026-09-27 09:13:35 +00:00
93e4b2b065 Иконка приложения (adaptive icon) 2026-09-27 09:13:34 +00:00
35a43cb829 Иконка приложения (adaptive icon) 2026-09-27 09:13:33 +00:00
3e61ae7e74 README: обновлено описание v2.0 (расширенный маршрут, WebView) 2026-09-27 09:07:46 +00:00
9 changed files with 164 additions and 68 deletions

View File

@@ -1,78 +1,55 @@
# Автобус №127 — Расписание (Android)
[![Лицензия: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](#лицензия)
Приложение для Android с расписанием автобуса **№127** по маршруту
**Полярные Зори ⇄ Зашеек** (Мурманская область, Кандалакшский район).
Написано на **Kotlin** с использованием **Android SDK** и Material Components.
Тёмная тема, аккуратные карточки и «чипы» расписания.
Приложение с расписанием автобуса **№127** по маршруту
**Кандалакша — Полярные Зори — Зашеек** (Мурманская область).
## Возможности
- Расписание на **будни**, **субботу** и **воскресенье** (переключатель вкладок).
- Блок **«Ближайшие рейсы»** — три ближайших отправления с подсчётом
«через N минут» (время по **МСК**, обновляется каждые 30 сек).
- Полные списки рейсов **туда** (ПЗ → Зашеек) и **обратно** (Зашеек → ПЗ),
аккуратно переносящиеся по 5 в строке.
- Тёмная тема (#121212) с оранжево-красным акцентом.
- Адаптивная иконка-автобус (Adaptive Icon).
- Живые часы местного времени (МСК) в шапке.
## Скриншот
<sub>Иконка приложения — стилизованный автобус на градиентном фоне.</sub>
- **Выбор остановки** — Кандалакша / Полярные Зори / Зашеек. При выборе
показываются доступные направления (куда можно уехать).
- **Ближайшие рейсы** — по 3 на каждое направление, с подсчётом
«через N ч M мин» (время по **МСК**, обновляется каждые 30 сек).
- **Автоопределение дня недели** — будни/выходные.
- **Полное расписание** — по кнопке раскрывается вся таблица
(4 остановки: Кандалакша · ПЗ→Зашеек · Зашеек · ПЗ→Кандалакша).
- Тёмная тема, живые часы МСК, подсветка ближайших рейсов (< 15 мин).
## Технические данные
| Параметр | Значение |
|-----------------|-----------------------------------|
| Язык | Kotlin 1.9.22 |
|-----------------|------------------------------|
| Тип | WebView-обёртка (HTML/JS) |
| Минимальный SDK | 26 (Android 8.0) |
| Целевой SDK | 34 (Android 14) |
| Сборка | Gradle 8.6 + Android Gradle Plugin 8.2.2 |
| UI | Material Components, ConstraintLayout |
## Сборка
Проект собирается системным Gradle (на хосте `192.168.1.46` gradle распакован
в `~/BusSchedule127/gradle-8.6`):
```bash
export ANDROID_HOME=~/Android/Sdk
export JAVA_HOME=/usr
export PATH=$PATH:~/BusSchedule127/gradle-8.6/bin
cd BusSchedule127
gradle clean assembleRelease --no-daemon
```
Подписанный APK появится в:
`app/build/outputs/apk/release/app-release.apk`
> Для подписи используется keystore `~/bus127.keystore` (alias `bus127`).
> Секретные ключи в репозиторий не включены — генерируйте свои
> через `keytool` перед релизной сборкой.
| Сборка | Gradle 8.6 + AGP 8.2.2 |
| Подпись | keystore `~/bus127.keystore` (alias `bus127`) |
## Структура
```
BusSchedule127/
apk-webview/ # исходники APK (WebView)
├── app/
│ ├── build.gradle
│ └── src/main/
│ ├── AndroidManifest.xml
│ ├── java/ru/ua1zbe/bus127/MainActivity.kt
│ └── res/
│ ├── drawable/ # фоны, чипы, иконка-автобус
│ ├── layout/ # activity_main.xml
│ ├── mipmap-anydpi-v26/ # адаптивная иконка
│ └── values/ # colors, strings, themes
├── build.gradle
├── settings.gradle
├── gradle.properties
└── README.md
│ ├── java/ru/ua1zbe/bus127/MainActivity.java
│ ├── assets/index.html # интерфейс (HTML/JS)
│ └── res/values/strings.xml
└── settings.gradle, build.gradle, gradle.properties
web/index.html # веб-версия (та же страница, для браузера)
```
## Сборка
```bash
export JAVA_HOME=$HOME/jdk-17.0.20.1+1
export ANDROID_HOME=$HOME/Android/Sdk
cd apk-webview
~/gradle-8.6/bin/gradle assembleRelease --no-daemon
```
Готовый APK — в [релизах](../../releases) и в репозитории `android_apk`.
## Лицензия
MIT — свободное использование и модификация.
MIT.

View File

@@ -19,8 +19,8 @@ android {
applicationId "ru.ua1zbe.bus127"
minSdk 26
targetSdk 34
versionCode 2
versionName "2.0"
versionCode 3
versionName "2.1"
}
buildTypes {

View File

@@ -3,6 +3,8 @@
<application
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:usesCleartextTraffic="true"
android:theme="@android:style/Theme.Material.NoActionBar">

View File

@@ -140,7 +140,38 @@ const DIRECTIONS = {
const DAYS = ["воскресенье","понедельник","вторник","среда","четверг","пятница","суббота"];
function toMin(t){ const [h,m]=t.split(":").map(Number); return h*60+m; }
function nowMs(){ return new Date(Date.now()+3*3600*1000); } // МСК = UTC+3
// Текущее время по МСК, независимо от часового пояса устройства
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 fmtWait(m){
if(m <= 0) return "отправление сейчас";
@@ -154,7 +185,7 @@ let currentStop = "Полярные Зори";
function render(){
const now = nowMs();
const curMin = now.getHours()*60 + now.getMinutes();
const curMin = now.hour*60 + now.minute;
const dirs = DIRECTIONS[currentStop];
document.getElementById("dirBox").innerHTML = dirs.map(d=>{
const upcoming = SCHEDULE
@@ -185,14 +216,14 @@ function renderTable(){
}
function renderDay(){
const dow = nowMs().getDay();
const dow = nowMs().day;
document.getElementById("dayBadge").innerHTML = `Сегодня <b>${DAYS[dow]}</b>`;
}
function tickClock(){
const d = nowMs();
const p = n=>String(n).padStart(2,"0");
document.getElementById("clock").innerHTML = `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())} <small>МСК</small>`;
document.getElementById("clock").innerHTML = `${p(d.hour)}:${p(d.minute)}:${p(d.second)} <small>МСК</small>`;
}
document.querySelectorAll(".pick").forEach(btn=>{

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#1E1E1E" />
</shape>

View File

@@ -0,0 +1,40 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<!-- bus body -->
<path
android:fillColor="#FF8A65"
android:pathData="M22,44 L82,44 a8,8 0 0 1 8,8 L90,80 a6,6 0 0 1 -6,6 L24,86 a6,6 0 0 1 -6,-6 L18,52 a8,8 0 0 1 8,-8 Z" />
<!-- windshield -->
<path
android:fillColor="#1E1E1E"
android:pathData="M74,48 L84,48 a4,4 0 0 1 4,4 L88,62 L74,62 Z" />
<!-- side windows -->
<path
android:fillColor="#1E1E1E"
android:pathData="M24,48 L70,48 L70,62 L24,62 Z" />
<!-- window dividers -->
<path
android:fillColor="#FF8A65"
android:pathData="M38,48 L38,62 M52,48 L52,62 M62,48 L62,62" />
<!-- headlight -->
<path
android:fillColor="#FFE0D6"
android:pathData="M84,70 L88,70 L88,76 L84,76 Z" />
<!-- wheels -->
<path
android:fillColor="#1E1E1E"
android:pathData="M34,86 m-8,0 a8,8 0 1,0 16,0 a8,8 0 1,0 -16,0" />
<path
android:fillColor="#1E1E1E"
android:pathData="M72,86 m-8,0 a8,8 0 1,0 16,0 a8,8 0 1,0 -16,0" />
<!-- wheel hubs -->
<path
android:fillColor="#FF8A65"
android:pathData="M34,86 m-3,0 a3,3 0 1,0 6,0 a3,3 0 1,0 -6,0" />
<path
android:fillColor="#FF8A65"
android:pathData="M72,86 m-3,0 a3,3 0 1,0 6,0 a3,3 0 1,0 -6,0" />
</vector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -140,7 +140,38 @@ const DIRECTIONS = {
const DAYS = ["воскресенье","понедельник","вторник","среда","четверг","пятница","суббота"];
function toMin(t){ const [h,m]=t.split(":").map(Number); return h*60+m; }
function nowMs(){ return new Date(Date.now()+3*3600*1000); } // МСК = UTC+3
// Текущее время по МСК, независимо от часового пояса устройства
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 fmtWait(m){
if(m <= 0) return "отправление сейчас";
@@ -154,7 +185,7 @@ let currentStop = "Полярные Зори";
function render(){
const now = nowMs();
const curMin = now.getHours()*60 + now.getMinutes();
const curMin = now.hour*60 + now.minute;
const dirs = DIRECTIONS[currentStop];
document.getElementById("dirBox").innerHTML = dirs.map(d=>{
const upcoming = SCHEDULE
@@ -185,14 +216,14 @@ function renderTable(){
}
function renderDay(){
const dow = nowMs().getDay();
const dow = nowMs().day;
document.getElementById("dayBadge").innerHTML = `Сегодня <b>${DAYS[dow]}</b>`;
}
function tickClock(){
const d = nowMs();
const p = n=>String(n).padStart(2,"0");
document.getElementById("clock").innerHTML = `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())} <small>МСК</small>`;
document.getElementById("clock").innerHTML = `${p(d.hour)}:${p(d.minute)}:${p(d.second)} <small>МСК</small>`;
}
document.querySelectorAll(".pick").forEach(btn=>{