diff --git a/.github/workflows/android-kotlin-ci.yml b/.github/workflows/android-kotlin-ci.yml new file mode 100644 index 0000000..3ac4d76 --- /dev/null +++ b/.github/workflows/android-kotlin-ci.yml @@ -0,0 +1,47 @@ +name: Android Kotlin CI + +on: + push: + paths: + - 'mobile/kotlin-app/**' + - '.github/workflows/android-kotlin-ci.yml' + pull_request: + paths: + - 'mobile/kotlin-app/**' + - '.github/workflows/android-kotlin-ci.yml' + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + defaults: + run: + working-directory: mobile/kotlin-app + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + cache: gradle + + - name: Validate Gradle wrapper + uses: gradle/actions/wrapper-validation@v4 + + - name: Build debug APK + run: ./gradlew clean assembleDebug + + - name: Build release APK + run: ./gradlew assembleRelease + + - name: Upload APK artifacts + uses: actions/upload-artifact@v4 + with: + name: kotlin-apk + path: | + mobile/kotlin-app/app/build/outputs/apk/debug/*.apk + mobile/kotlin-app/app/build/outputs/apk/release/*.apk diff --git a/mobile/kotlin-app/README.md b/mobile/kotlin-app/README.md index d3320ff..2594399 100644 --- a/mobile/kotlin-app/README.md +++ b/mobile/kotlin-app/README.md @@ -1,16 +1,18 @@ -# Kotlin Android wrapper app +# Kotlin Android app (mobile-optimized web wrapper) -This is a native Kotlin Android app that replicates the existing Sunset web project on phone by loading the deployed web app in a full-screen `WebView`. +This Android app preserves the same Sunset product idea by loading the existing web experience, but adds mobile-first navigation and interaction patterns: -## Steps +- Bottom tab navigation to key sections (Home, Courses, About, AI Lab) +- Pull-to-refresh support +- Top loading progress indicator +- Tuned WebView settings and custom Android user-agent suffix -1. Set your production URL in `app/build.gradle.kts`: - - `WEB_APP_URL = "https://YOUR_DEPLOYED_WEB_APP_URL"` +## Setup + +1. Set `WEB_APP_URL` in `app/build.gradle.kts` to your deployed site URL. 2. Open `mobile/kotlin-app` in Android Studio. 3. Build and run. -## Why this approach +## CI -- Reuses your existing Next.js codebase. -- Fastest way to get parity between web and Android. -- Keeps one product surface while still shipping through Play Store. +A GitHub Actions workflow is included at `.github/workflows/android-kotlin-ci.yml` to build debug and release APKs automatically on pushes/PRs that touch the Kotlin Android app. diff --git a/mobile/kotlin-app/app/build.gradle.kts b/mobile/kotlin-app/app/build.gradle.kts index 71304de..5b3ce64 100644 --- a/mobile/kotlin-app/app/build.gradle.kts +++ b/mobile/kotlin-app/app/build.gradle.kts @@ -46,4 +46,5 @@ dependencies { implementation("androidx.core:core-ktx:1.13.1") implementation("androidx.appcompat:appcompat:1.7.0") implementation("com.google.android.material:material:1.12.0") + implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") } diff --git a/mobile/kotlin-app/app/src/main/java/com/sunset/replica/MainActivity.kt b/mobile/kotlin-app/app/src/main/java/com/sunset/replica/MainActivity.kt index 94b5915..593a08d 100644 --- a/mobile/kotlin-app/app/src/main/java/com/sunset/replica/MainActivity.kt +++ b/mobile/kotlin-app/app/src/main/java/com/sunset/replica/MainActivity.kt @@ -1,16 +1,30 @@ package com.sunset.replica import android.annotation.SuppressLint +import android.graphics.Bitmap import android.os.Bundle import android.webkit.WebChromeClient import android.webkit.WebResourceRequest +import android.webkit.WebSettings import android.webkit.WebView import android.webkit.WebViewClient +import android.widget.ProgressBar import androidx.appcompat.app.AppCompatActivity +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import com.google.android.material.bottomnavigation.BottomNavigationView class MainActivity : AppCompatActivity() { private lateinit var webView: WebView + private lateinit var pullToRefresh: SwipeRefreshLayout + private lateinit var loadingIndicator: ProgressBar + + private val pages = mapOf( + R.id.nav_home to "", + R.id.nav_courses to "/courses", + R.id.nav_about to "/about", + R.id.nav_ai to "/new-design" + ) @SuppressLint("SetJavaScriptEnabled") override fun onCreate(savedInstanceState: Bundle?) { @@ -18,23 +32,72 @@ class MainActivity : AppCompatActivity() { setContentView(R.layout.activity_main) webView = findViewById(R.id.webView) - webView.webChromeClient = WebChromeClient() - webView.webViewClient = object : WebViewClient() { - override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { - return false - } + pullToRefresh = findViewById(R.id.pullToRefresh) + loadingIndicator = findViewById(R.id.loadingIndicator) + + configureWebView() + configureBottomNavigation() + + pullToRefresh.setOnRefreshListener { + webView.reload() + } + + if (savedInstanceState == null) { + navigateTo(R.id.nav_home) + } else { + webView.restoreState(savedInstanceState) + } + } + + private fun configureBottomNavigation() { + val bottomNavigation: BottomNavigationView = findViewById(R.id.bottomNavigation) + bottomNavigation.setOnItemSelectedListener { item -> + navigateTo(item.itemId) + true + } + } + + private fun navigateTo(navItemId: Int) { + val path = pages[navItemId] ?: "" + val destination = "${BuildConfig.WEB_APP_URL.trimEnd('/')}$path" + if (webView.url != destination) { + webView.loadUrl(destination) } + } + @SuppressLint("SetJavaScriptEnabled") + private fun configureWebView() { with(webView.settings) { javaScriptEnabled = true domStorageEnabled = true mediaPlaybackRequiresUserGesture = false + cacheMode = WebSettings.LOAD_DEFAULT useWideViewPort = true loadWithOverviewMode = true + builtInZoomControls = false + displayZoomControls = false + userAgentString = "$userAgentString SunsetAndroid/1.1" } - if (savedInstanceState == null) { - webView.loadUrl(BuildConfig.WEB_APP_URL) + webView.webChromeClient = object : WebChromeClient() { + override fun onProgressChanged(view: WebView?, newProgress: Int) { + loadingIndicator.progress = newProgress + loadingIndicator.visibility = if (newProgress < 100) ProgressBar.VISIBLE else ProgressBar.GONE + } + } + + webView.webViewClient = object : WebViewClient() { + override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean = false + + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + pullToRefresh.isRefreshing = true + } + + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + pullToRefresh.isRefreshing = false + } } } @@ -43,16 +106,8 @@ class MainActivity : AppCompatActivity() { super.onSaveInstanceState(outState) } - override fun onRestoreInstanceState(savedInstanceState: Bundle) { - super.onRestoreInstanceState(savedInstanceState) - webView.restoreState(savedInstanceState) - } - + @Deprecated("Deprecated in Java") override fun onBackPressed() { - if (webView.canGoBack()) { - webView.goBack() - } else { - super.onBackPressed() - } + if (webView.canGoBack()) webView.goBack() else super.onBackPressed() } } diff --git a/mobile/kotlin-app/app/src/main/res/layout/activity_main.xml b/mobile/kotlin-app/app/src/main/res/layout/activity_main.xml index 08ec4b5..9acc04b 100644 --- a/mobile/kotlin-app/app/src/main/res/layout/activity_main.xml +++ b/mobile/kotlin-app/app/src/main/res/layout/activity_main.xml @@ -1,11 +1,34 @@ - + android:layout_height="match_parent" + android:orientation="vertical"> - + android:layout_height="4dp" + android:max="100" /> - + + + + + + + + diff --git a/mobile/kotlin-app/app/src/main/res/menu/bottom_navigation_menu.xml b/mobile/kotlin-app/app/src/main/res/menu/bottom_navigation_menu.xml new file mode 100644 index 0000000..37bd085 --- /dev/null +++ b/mobile/kotlin-app/app/src/main/res/menu/bottom_navigation_menu.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/mobile/kotlin-app/app/src/main/res/values/strings.xml b/mobile/kotlin-app/app/src/main/res/values/strings.xml index 169853e..ab88d1f 100644 --- a/mobile/kotlin-app/app/src/main/res/values/strings.xml +++ b/mobile/kotlin-app/app/src/main/res/values/strings.xml @@ -1,3 +1,7 @@ - Sunset Replica + Sunset Mobile + Home + Courses + About + AI Lab