feat: initial Android port of tiennm99/loto

Native Android port of the SvelteKit Lô tô hội chợ Tân Tân web app.

Stack:
- Kotlin 2.1 + Jetpack Compose + Material 3
- Single Activity, single Gradle module (:app)
- Audio: AndroidX Media3 ExoPlayer with bundled MP3 voice clips
- Settings: DataStore Preferences with legacy masterMode migration
- minSdk 24, targetSdk 35, JDK 17

Game logic:
- 9x9 player card with exactly 5 numbers per row AND per column
- Soft constraint: no 3 consecutive filled columns (rejection-sampled)
- 11x9 ones-digit-aligned master tracking board
- Bingo / Cho ("waiting") state machine ported from PlayerBoard.svelte
- Forward-only auto-tick from MasterPanel via app-scoped CallBus

Audio:
- 184 MP3s pre-generated with Microsoft Edge TTS (Hoai My, Nam Minh)
- ExoPlayer playlist for cho + N gapless sequence
- Token-based cancellation matching the web voice.js semantics

CI:
- build-debug.yml: lint + test + assembleDebug on push/PR
- release.yml: signed AAB+APK on v* tag, env-driven keystore
This commit is contained in:
miti99 committed 2026-04-27 19:33:16 +07:00
1 parent 36f2f41986
commit 90a9dd1d1a
255 files changed
+4790

No files matched your search

@@ -0,0 +1,46 @@
package com.miti99.loto
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
/**
* Compose UI smoke test — verifies the full cold-start → generate-grid flow.
*
* Requires a connected device or emulator. Run via:
* ./gradlew :app:connectedDebugAndroidTest
*
* Phase 11 spec: launch MainActivity, assert "Tạo bảng mới" shown,
* click it, assert all 81 player_cell nodes are rendered.
*/
@RunWith(AndroidJUnit4::class)
class MainActivityComposeSmokeTest {
@get:Rule
val rule = createAndroidComposeRule<MainActivity>()
@Test
fun generate_button_is_visible_on_cold_start() {
rule.onNodeWithText("Tạo bảng mới").assertExists()
}
@Test
fun generate_button_creates_81_player_cells() {
// Tap the generate button — no confirmation dialog on first tap (grid is null)
rule.onNodeWithText("Tạo bảng mới").performClick()
// Wait up to 5 s for all 81 cells to appear (grid generation is synchronous
// but Compose may take a frame to recompose)
rule.waitUntil(timeoutMillis = 5_000) {
rule.onAllNodesWithTag("player_cell").fetchSemanticsNodes().size == 81
}
rule.onAllNodesWithTag("player_cell").assertCountEquals(81)
}
}
+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.VIBRATE" />
<application
android:name=".LotoApp"
android:allowBackup="false"
android:dataExtractionRules="@xml/backup_rules"
android:fullBackupContent="false"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="false"
android:theme="@style/Theme.Loto">
<activity
android:name=".MainActivity"
android:exported="true"
android:configChanges="orientation|screenSize|keyboardHidden|uiMode"
android:screenOrientation="portrait"
android:theme="@style/Theme.Loto">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,16 @@
{
"voices": [
{
"id": "hoai-my",
"edgeName": "vi-VN-HoaiMyNeural",
"label": "Hoai My (nữ)",
"gender": "female"
},
{
"id": "nam-minh",
"edgeName": "vi-VN-NamMinhNeural",
"label": "Nam Minh (nam)",
"gender": "male"
}
]
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loaded 100 of 255 files, more files were not shown because too many files have changed in this diff. Show more