commit 43c21a0cd599903ae95567a6e2036c4b7c08d690 Author: beetzung Date: Sat Feb 28 14:26:16 2026 +0100 Initial commit diff --git a/.cursor/rules/app-recreation-core.mdc b/.cursor/rules/app-recreation-core.mdc new file mode 100644 index 0000000..2cf8f76 --- /dev/null +++ b/.cursor/rules/app-recreation-core.mdc @@ -0,0 +1,29 @@ +--- +description: Core constraints for AIRMQ recreation project +alwaysApply: true +--- + +# AIRMQ Recreation Core Rules + +## Repository boundaries + +1. Do not modify anything under `C:\Users\sysop\Desktop\airmq-android`. +2. Treat `airmq-android` as read-only reference only. +3. Create and apply all code/config/build changes only under `C:\Users\sysop\Desktop\airmq-android-2026`. + +## Working policy + +- When referencing legacy implementation details, copy behavior intentionally into the new project rather than editing old files. +- If a task appears to require modifying the old project, stop and propose an equivalent change in `airmq-android-2026` instead. + +## Baseline architecture and platform stack + +Use this stack as the default foundation for all implementation work in `airmq-android-2026`: + +1. Jetpack Compose for UI. +2. Compose Navigation 3 as the in-app navigation system. +3. Firebase for mobile backend capabilities (for example auth, analytics, crash reporting, messaging, or remote config as needed). +4. Google Maps platform for map rendering and map-related interactions. +5. Apollo GraphQL for GraphQL schema integration, client generation, and network operations. + +If a requested change conflicts with this baseline, ask for explicit approval before introducing an alternative. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..e29a1fd --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +AirMQ \ No newline at end of file diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..cdbc250 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..74dd639 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..e9827ba --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,66 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "org.db3.airmq" + compileSdk { + version = release(36) { + minorApiLevel = 1 + } + } + + defaultConfig { + applicationId = "org.db3.airmq" + minSdk = 24 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + buildFeatures { + compose = true + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.navigation.compose) + implementation(platform(libs.firebase.bom)) + implementation(libs.firebase.analytics) + implementation(libs.firebase.crashlytics) + implementation(libs.firebase.messaging) + implementation(libs.google.maps.compose) + implementation(libs.play.services.maps) + implementation(libs.apollo.runtime) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.compose.ui.test.junit4) + debugImplementation(libs.androidx.compose.ui.tooling) + debugImplementation(libs.androidx.compose.ui.test.manifest) +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/org/db3/airmq/ExampleInstrumentedTest.kt b/app/src/androidTest/java/org/db3/airmq/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..6353ae9 --- /dev/null +++ b/app/src/androidTest/java/org/db3/airmq/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package org.db3.airmq + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("org.db3.airmq", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b310721 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/kotlin/org/db3/airmq/MainActivity.kt b/app/src/main/kotlin/org/db3/airmq/MainActivity.kt new file mode 100644 index 0000000..fbdb927 --- /dev/null +++ b/app/src/main/kotlin/org/db3/airmq/MainActivity.kt @@ -0,0 +1,22 @@ +package org.db3.airmq + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.ui.Modifier +import org.db3.airmq.features.navigation.AirMqNavGraph +import org.db3.airmq.ui.theme.AirMQTheme + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + AirMQTheme { + AirMqNavGraph(modifier = Modifier.fillMaxSize()) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/org/db3/airmq/features/city/CityScreen.kt b/app/src/main/kotlin/org/db3/airmq/features/city/CityScreen.kt new file mode 100644 index 0000000..70f415f --- /dev/null +++ b/app/src/main/kotlin/org/db3/airmq/features/city/CityScreen.kt @@ -0,0 +1,14 @@ +package org.db3.airmq.features.city + +import androidx.compose.runtime.Composable +import org.db3.airmq.features.common.MockScreenScaffold +import org.db3.airmq.features.common.ScreenAction + +@Composable +fun CityScreen(onBackToDashboard: () -> Unit) { + MockScreenScaffold( + title = "City", + subtitle = "Mock city management screen.", + actions = listOf(ScreenAction("Back to Dashboard", onBackToDashboard)) + ) +} diff --git a/app/src/main/kotlin/org/db3/airmq/features/common/MockScreenScaffold.kt b/app/src/main/kotlin/org/db3/airmq/features/common/MockScreenScaffold.kt new file mode 100644 index 0000000..2c356ac --- /dev/null +++ b/app/src/main/kotlin/org/db3/airmq/features/common/MockScreenScaffold.kt @@ -0,0 +1,69 @@ +package org.db3.airmq.features.common + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +data class ScreenAction( + val label: String, + val onClick: () -> Unit +) + +@Composable +fun MockScreenScaffold( + title: String, + subtitle: String? = null, + actions: List = emptyList(), + content: @Composable (() -> Unit)? = null +) { + Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + ScreenContent( + innerPadding = innerPadding, + title = title, + subtitle = subtitle, + actions = actions, + content = content + ) + } +} + +@Composable +private fun ScreenContent( + innerPadding: PaddingValues, + title: String, + subtitle: String?, + actions: List, + content: @Composable (() -> Unit)? +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .padding(16.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text(text = title, style = MaterialTheme.typography.headlineMedium) + if (subtitle != null) { + Text(text = subtitle, style = MaterialTheme.typography.bodyLarge) + } + content?.invoke() + actions.forEach { action -> + Button(onClick = action.onClick, modifier = Modifier.fillMaxWidth()) { + Text(text = action.label) + } + } + } +} diff --git a/app/src/main/kotlin/org/db3/airmq/features/constructor/ChartConstructorScreen.kt b/app/src/main/kotlin/org/db3/airmq/features/constructor/ChartConstructorScreen.kt new file mode 100644 index 0000000..52f2f5a --- /dev/null +++ b/app/src/main/kotlin/org/db3/airmq/features/constructor/ChartConstructorScreen.kt @@ -0,0 +1,14 @@ +package org.db3.airmq.features.constructor + +import androidx.compose.runtime.Composable +import org.db3.airmq.features.common.MockScreenScaffold +import org.db3.airmq.features.common.ScreenAction + +@Composable +fun ChartConstructorScreen(onBackToWidgetConstructor: () -> Unit) { + MockScreenScaffold( + title = "Chart Constructor", + subtitle = "Mock chart widget constructor variant.", + actions = listOf(ScreenAction("Back to Widget Constructor", onBackToWidgetConstructor)) + ) +} diff --git a/app/src/main/kotlin/org/db3/airmq/features/constructor/MapConstructorScreen.kt b/app/src/main/kotlin/org/db3/airmq/features/constructor/MapConstructorScreen.kt new file mode 100644 index 0000000..93dd456 --- /dev/null +++ b/app/src/main/kotlin/org/db3/airmq/features/constructor/MapConstructorScreen.kt @@ -0,0 +1,14 @@ +package org.db3.airmq.features.constructor + +import androidx.compose.runtime.Composable +import org.db3.airmq.features.common.MockScreenScaffold +import org.db3.airmq.features.common.ScreenAction + +@Composable +fun MapConstructorScreen(onBackToWidgetConstructor: () -> Unit) { + MockScreenScaffold( + title = "Map Constructor", + subtitle = "Mock map widget constructor variant.", + actions = listOf(ScreenAction("Back to Widget Constructor", onBackToWidgetConstructor)) + ) +} diff --git a/app/src/main/kotlin/org/db3/airmq/features/constructor/NewsConstructorScreen.kt b/app/src/main/kotlin/org/db3/airmq/features/constructor/NewsConstructorScreen.kt new file mode 100644 index 0000000..319e852 --- /dev/null +++ b/app/src/main/kotlin/org/db3/airmq/features/constructor/NewsConstructorScreen.kt @@ -0,0 +1,14 @@ +package org.db3.airmq.features.constructor + +import androidx.compose.runtime.Composable +import org.db3.airmq.features.common.MockScreenScaffold +import org.db3.airmq.features.common.ScreenAction + +@Composable +fun NewsConstructorScreen(onBackToWidgetConstructor: () -> Unit) { + MockScreenScaffold( + title = "News Constructor", + subtitle = "Mock news widget constructor variant.", + actions = listOf(ScreenAction("Back to Widget Constructor", onBackToWidgetConstructor)) + ) +} diff --git a/app/src/main/kotlin/org/db3/airmq/features/constructor/SelectMapWidgetLocationScreen.kt b/app/src/main/kotlin/org/db3/airmq/features/constructor/SelectMapWidgetLocationScreen.kt new file mode 100644 index 0000000..6b459c6 --- /dev/null +++ b/app/src/main/kotlin/org/db3/airmq/features/constructor/SelectMapWidgetLocationScreen.kt @@ -0,0 +1,14 @@ +package org.db3.airmq.features.constructor + +import androidx.compose.runtime.Composable +import org.db3.airmq.features.common.MockScreenScaffold +import org.db3.airmq.features.common.ScreenAction + +@Composable +fun SelectMapWidgetLocationScreen(onDone: () -> Unit) { + MockScreenScaffold( + title = "Select Map Widget Location", + subtitle = "Mock map location picker for widget.", + actions = listOf(ScreenAction("Done", onDone)) + ) +} diff --git a/app/src/main/kotlin/org/db3/airmq/features/constructor/WidgetConstructorScreen.kt b/app/src/main/kotlin/org/db3/airmq/features/constructor/WidgetConstructorScreen.kt new file mode 100644 index 0000000..82ed4f8 --- /dev/null +++ b/app/src/main/kotlin/org/db3/airmq/features/constructor/WidgetConstructorScreen.kt @@ -0,0 +1,26 @@ +package org.db3.airmq.features.constructor + +import androidx.compose.runtime.Composable +import org.db3.airmq.features.common.MockScreenScaffold +import org.db3.airmq.features.common.ScreenAction + +@Composable +fun WidgetConstructorScreen( + onOpenSelectMapWidgetLocation: () -> Unit, + onOpenMapConstructor: () -> Unit, + onOpenChartConstructor: () -> Unit, + onOpenNewsConstructor: () -> Unit, + onBackToManage: () -> Unit +) { + MockScreenScaffold( + title = "Widget Constructor", + subtitle = "Select constructor variant.", + actions = listOf( + ScreenAction("Select Map Widget Location", onOpenSelectMapWidgetLocation), + ScreenAction("Open Map Constructor", onOpenMapConstructor), + ScreenAction("Open Chart Constructor", onOpenChartConstructor), + ScreenAction("Open News Constructor", onOpenNewsConstructor), + ScreenAction("Back to Manage", onBackToManage) + ) + ) +} diff --git a/app/src/main/kotlin/org/db3/airmq/features/dashboard/DashboardScreen.kt b/app/src/main/kotlin/org/db3/airmq/features/dashboard/DashboardScreen.kt new file mode 100644 index 0000000..8565ec3 --- /dev/null +++ b/app/src/main/kotlin/org/db3/airmq/features/dashboard/DashboardScreen.kt @@ -0,0 +1,28 @@ +package org.db3.airmq.features.dashboard + +import androidx.compose.runtime.Composable +import org.db3.airmq.features.common.MockScreenScaffold +import org.db3.airmq.features.common.ScreenAction + +@Composable +fun DashboardScreen( + onOpenMap: () -> Unit, + onOpenManage: () -> Unit, + onOpenCity: () -> Unit, + onOpenDevice: () -> Unit, + onOpenNews: () -> Unit, + onOpenWidgetConstructor: () -> Unit +) { + MockScreenScaffold( + title = "Dashboard", + subtitle = "Bottom-tab equivalent: dashboard", + actions = listOf( + ScreenAction("Open Map", onOpenMap), + ScreenAction("Open Manage", onOpenManage), + ScreenAction("Open City", onOpenCity), + ScreenAction("Open Device", onOpenDevice), + ScreenAction("Open News", onOpenNews), + ScreenAction("Open Widget Constructor", onOpenWidgetConstructor) + ) + ) +} diff --git a/app/src/main/kotlin/org/db3/airmq/features/debug/DebugScreen.kt b/app/src/main/kotlin/org/db3/airmq/features/debug/DebugScreen.kt new file mode 100644 index 0000000..e075c42 --- /dev/null +++ b/app/src/main/kotlin/org/db3/airmq/features/debug/DebugScreen.kt @@ -0,0 +1,14 @@ +package org.db3.airmq.features.debug + +import androidx.compose.runtime.Composable +import org.db3.airmq.features.common.MockScreenScaffold +import org.db3.airmq.features.common.ScreenAction + +@Composable +fun DebugScreen(onBackToSettings: () -> Unit) { + MockScreenScaffold( + title = "Debug", + subtitle = "Debug-only tools placeholder.", + actions = listOf(ScreenAction("Back to Settings", onBackToSettings)) + ) +} diff --git a/app/src/main/kotlin/org/db3/airmq/features/device/DeviceScreen.kt b/app/src/main/kotlin/org/db3/airmq/features/device/DeviceScreen.kt new file mode 100644 index 0000000..e011321 --- /dev/null +++ b/app/src/main/kotlin/org/db3/airmq/features/device/DeviceScreen.kt @@ -0,0 +1,21 @@ +package org.db3.airmq.features.device + +import androidx.compose.runtime.Composable +import org.db3.airmq.features.common.MockScreenScaffold +import org.db3.airmq.features.common.ScreenAction + +@Composable +fun DeviceScreen( + deviceId: String, + onOpenLocation: () -> Unit, + onShowOnMap: () -> Unit +) { + MockScreenScaffold( + title = "Device", + subtitle = "Mock deviceId: $deviceId", + actions = listOf( + ScreenAction("Select Location", onOpenLocation), + ScreenAction("Show on Map", onShowOnMap) + ) + ) +} diff --git a/app/src/main/kotlin/org/db3/airmq/features/entry/SplashScreen.kt b/app/src/main/kotlin/org/db3/airmq/features/entry/SplashScreen.kt new file mode 100644 index 0000000..bc0ee6c --- /dev/null +++ b/app/src/main/kotlin/org/db3/airmq/features/entry/SplashScreen.kt @@ -0,0 +1,14 @@ +package org.db3.airmq.features.entry + +import androidx.compose.runtime.Composable +import org.db3.airmq.features.common.MockScreenScaffold +import org.db3.airmq.features.common.ScreenAction + +@Composable +fun SplashScreen(onContinue: () -> Unit) { + MockScreenScaffold( + title = "Splash", + subtitle = "Entry flow starting point.", + actions = listOf(ScreenAction(label = "Continue to Wizard", onClick = onContinue)) + ) +} diff --git a/app/src/main/kotlin/org/db3/airmq/features/entry/WizardScreen.kt b/app/src/main/kotlin/org/db3/airmq/features/entry/WizardScreen.kt new file mode 100644 index 0000000..c101d41 --- /dev/null +++ b/app/src/main/kotlin/org/db3/airmq/features/entry/WizardScreen.kt @@ -0,0 +1,14 @@ +package org.db3.airmq.features.entry + +import androidx.compose.runtime.Composable +import org.db3.airmq.features.common.MockScreenScaffold +import org.db3.airmq.features.common.ScreenAction + +@Composable +fun WizardScreen(onFinish: () -> Unit) { + MockScreenScaffold( + title = "Wizard", + subtitle = "Mock onboarding/wizard flow.", + actions = listOf(ScreenAction(label = "Finish Wizard", onClick = onFinish)) + ) +} diff --git a/app/src/main/kotlin/org/db3/airmq/features/location/LocationScreen.kt b/app/src/main/kotlin/org/db3/airmq/features/location/LocationScreen.kt new file mode 100644 index 0000000..19c9ae1 --- /dev/null +++ b/app/src/main/kotlin/org/db3/airmq/features/location/LocationScreen.kt @@ -0,0 +1,14 @@ +package org.db3.airmq.features.location + +import androidx.compose.runtime.Composable +import org.db3.airmq.features.common.MockScreenScaffold +import org.db3.airmq.features.common.ScreenAction + +@Composable +fun LocationScreen(onBackToManage: () -> Unit) { + MockScreenScaffold( + title = "Location", + subtitle = "Mock location picker/editor screen.", + actions = listOf(ScreenAction("Back to Manage", onBackToManage)) + ) +} diff --git a/app/src/main/kotlin/org/db3/airmq/features/login/LoginScreen.kt b/app/src/main/kotlin/org/db3/airmq/features/login/LoginScreen.kt new file mode 100644 index 0000000..a3030a2 --- /dev/null +++ b/app/src/main/kotlin/org/db3/airmq/features/login/LoginScreen.kt @@ -0,0 +1,14 @@ +package org.db3.airmq.features.login + +import androidx.compose.runtime.Composable +import org.db3.airmq.features.common.MockScreenScaffold +import org.db3.airmq.features.common.ScreenAction + +@Composable +fun LoginScreen(onLogInToManage: () -> Unit) { + MockScreenScaffold( + title = "Login", + subtitle = "Mock account sign-in screen.", + actions = listOf(ScreenAction("Log In to Manage", onLogInToManage)) + ) +} diff --git a/app/src/main/kotlin/org/db3/airmq/features/manage/ManageScreen.kt b/app/src/main/kotlin/org/db3/airmq/features/manage/ManageScreen.kt new file mode 100644 index 0000000..96e209f --- /dev/null +++ b/app/src/main/kotlin/org/db3/airmq/features/manage/ManageScreen.kt @@ -0,0 +1,30 @@ +package org.db3.airmq.features.manage + +import androidx.compose.runtime.Composable +import org.db3.airmq.features.common.MockScreenScaffold +import org.db3.airmq.features.common.ScreenAction + +@Composable +fun ManageScreen( + onOpenDevice: () -> Unit, + onOpenSetup: () -> Unit, + onOpenSettings: () -> Unit, + onOpenLogin: () -> Unit, + onOpenLocation: () -> Unit, + onOpenWidgetConstructor: () -> Unit, + onBackToDashboard: () -> Unit +) { + MockScreenScaffold( + title = "Manage", + subtitle = "Bottom-tab equivalent: manage", + actions = listOf( + ScreenAction("Open Device", onOpenDevice), + ScreenAction("Start Setup", onOpenSetup), + ScreenAction("Open Settings", onOpenSettings), + ScreenAction("Open Login", onOpenLogin), + ScreenAction("Select Location", onOpenLocation), + ScreenAction("Open Widget Constructor", onOpenWidgetConstructor), + ScreenAction("Back to Dashboard", onBackToDashboard) + ) + ) +} diff --git a/app/src/main/kotlin/org/db3/airmq/features/map/MapScreen.kt b/app/src/main/kotlin/org/db3/airmq/features/map/MapScreen.kt new file mode 100644 index 0000000..0c1b037 --- /dev/null +++ b/app/src/main/kotlin/org/db3/airmq/features/map/MapScreen.kt @@ -0,0 +1,20 @@ +package org.db3.airmq.features.map + +import androidx.compose.runtime.Composable +import org.db3.airmq.features.common.MockScreenScaffold +import org.db3.airmq.features.common.ScreenAction + +@Composable +fun MapScreen( + onOpenDevice: () -> Unit, + onBackToDashboard: () -> Unit +) { + MockScreenScaffold( + title = "Map", + subtitle = "Bottom-tab equivalent: map", + actions = listOf( + ScreenAction("Open Device", onOpenDevice), + ScreenAction("Back to Dashboard", onBackToDashboard) + ) + ) +} diff --git a/app/src/main/kotlin/org/db3/airmq/features/navigation/AirMqNavGraph.kt b/app/src/main/kotlin/org/db3/airmq/features/navigation/AirMqNavGraph.kt new file mode 100644 index 0000000..80e9d1b --- /dev/null +++ b/app/src/main/kotlin/org/db3/airmq/features/navigation/AirMqNavGraph.kt @@ -0,0 +1,184 @@ +package org.db3.airmq.features.navigation + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument +import org.db3.airmq.features.city.CityScreen +import org.db3.airmq.features.constructor.ChartConstructorScreen +import org.db3.airmq.features.constructor.MapConstructorScreen +import org.db3.airmq.features.constructor.NewsConstructorScreen +import org.db3.airmq.features.constructor.SelectMapWidgetLocationScreen +import org.db3.airmq.features.constructor.WidgetConstructorScreen +import org.db3.airmq.features.dashboard.DashboardScreen +import org.db3.airmq.features.debug.DebugScreen +import org.db3.airmq.features.device.DeviceScreen +import org.db3.airmq.features.entry.SplashScreen +import org.db3.airmq.features.entry.WizardScreen +import org.db3.airmq.features.location.LocationScreen +import org.db3.airmq.features.login.LoginScreen +import org.db3.airmq.features.manage.ManageScreen +import org.db3.airmq.features.map.MapScreen +import org.db3.airmq.features.news.NewsDetailScreen +import org.db3.airmq.features.news.NewsScreen +import org.db3.airmq.features.settings.SettingsScreen +import org.db3.airmq.features.setup.SetupScreen + +@Composable +fun AirMqNavGraph(modifier: Modifier = Modifier) { + val navController = rememberNavController() + + NavHost( + navController = navController, + startDestination = AirMqRoutes.SPLASH, + modifier = modifier + ) { + composable(AirMqRoutes.SPLASH) { + SplashScreen( + onContinue = { navController.navigate(AirMqRoutes.WIZARD) } + ) + } + composable(AirMqRoutes.WIZARD) { + WizardScreen( + onFinish = { + navController.navigate(AirMqRoutes.DASHBOARD) { + popUpTo(AirMqRoutes.SPLASH) { inclusive = true } + } + } + ) + } + composable(AirMqRoutes.DASHBOARD) { + DashboardScreen( + onOpenMap = { navController.navigate(AirMqRoutes.MAP) }, + onOpenManage = { navController.navigate(AirMqRoutes.MANAGE) }, + onOpenCity = { navController.navigate(AirMqRoutes.CITY) }, + onOpenDevice = { navController.navigate(AirMqRoutes.device()) }, + onOpenNews = { navController.navigate(AirMqRoutes.NEWS) }, + onOpenWidgetConstructor = { navController.navigate(AirMqRoutes.WIDGET_CONSTRUCTOR) } + ) + } + composable(AirMqRoutes.MAP) { + MapScreen( + onOpenDevice = { navController.navigate(AirMqRoutes.device()) }, + onBackToDashboard = { navController.navigate(AirMqRoutes.DASHBOARD) } + ) + } + composable(AirMqRoutes.MANAGE) { + ManageScreen( + onOpenDevice = { navController.navigate(AirMqRoutes.device()) }, + onOpenSetup = { navController.navigate(AirMqRoutes.SETUP) }, + onOpenSettings = { navController.navigate(AirMqRoutes.SETTINGS) }, + onOpenLogin = { navController.navigate(AirMqRoutes.LOGIN) }, + onOpenLocation = { navController.navigate(AirMqRoutes.LOCATION) }, + onOpenWidgetConstructor = { navController.navigate(AirMqRoutes.WIDGET_CONSTRUCTOR) }, + onBackToDashboard = { navController.navigate(AirMqRoutes.DASHBOARD) } + ) + } + composable( + route = AirMqRoutes.DEVICE, + arguments = listOf( + navArgument("deviceId") { + type = NavType.StringType + defaultValue = "mock-device-id" + } + ) + ) { backStackEntry -> + DeviceScreen( + deviceId = backStackEntry.arguments?.getString("deviceId") ?: "mock-device-id", + onOpenLocation = { navController.navigate(AirMqRoutes.LOCATION) }, + onShowOnMap = { navController.navigate(AirMqRoutes.MAP) } + ) + } + composable(AirMqRoutes.CITY) { + CityScreen( + onBackToDashboard = { navController.navigate(AirMqRoutes.DASHBOARD) } + ) + } + composable(AirMqRoutes.SETTINGS) { + SettingsScreen( + onOpenDebug = { navController.navigate(AirMqRoutes.DEBUG) }, + onOpenCity = { navController.navigate(AirMqRoutes.CITY) }, + onLogOutToManage = { + navController.navigate(AirMqRoutes.MANAGE) { + popUpTo(AirMqRoutes.MANAGE) { inclusive = true } + } + } + ) + } + composable(AirMqRoutes.DEBUG) { + DebugScreen( + onBackToSettings = { navController.popBackStack() } + ) + } + composable(AirMqRoutes.LOCATION) { + LocationScreen( + onBackToManage = { navController.navigate(AirMqRoutes.MANAGE) } + ) + } + composable(AirMqRoutes.SETUP) { + SetupScreen( + onFinishSetup = { navController.navigate(AirMqRoutes.MANAGE) }, + onCancelSetup = { navController.navigate(AirMqRoutes.MANAGE) } + ) + } + composable(AirMqRoutes.LOGIN) { + LoginScreen( + onLogInToManage = { navController.navigate(AirMqRoutes.MANAGE) } + ) + } + composable(AirMqRoutes.NEWS) { + NewsScreen( + onOpenNewsDetail = { navController.navigate(AirMqRoutes.newsDetail()) }, + onBackToDashboard = { navController.navigate(AirMqRoutes.DASHBOARD) } + ) + } + composable( + route = AirMqRoutes.NEWS_DETAIL, + arguments = listOf( + navArgument("newsId") { + type = NavType.StringType + defaultValue = "mock-news-id" + } + ) + ) { backStackEntry -> + NewsDetailScreen( + newsId = backStackEntry.arguments?.getString("newsId") ?: "mock-news-id", + onBackToNews = { navController.popBackStack() } + ) + } + composable(AirMqRoutes.WIDGET_CONSTRUCTOR) { + WidgetConstructorScreen( + onOpenSelectMapWidgetLocation = { + navController.navigate(AirMqRoutes.SELECT_MAP_WIDGET_LOCATION) + }, + onOpenMapConstructor = { navController.navigate(AirMqRoutes.MAP_CONSTRUCTOR) }, + onOpenChartConstructor = { navController.navigate(AirMqRoutes.CHART_CONSTRUCTOR) }, + onOpenNewsConstructor = { navController.navigate(AirMqRoutes.NEWS_CONSTRUCTOR) }, + onBackToManage = { navController.navigate(AirMqRoutes.MANAGE) } + ) + } + composable(AirMqRoutes.SELECT_MAP_WIDGET_LOCATION) { + SelectMapWidgetLocationScreen( + onDone = { navController.popBackStack() } + ) + } + composable(AirMqRoutes.MAP_CONSTRUCTOR) { + MapConstructorScreen( + onBackToWidgetConstructor = { navController.popBackStack() } + ) + } + composable(AirMqRoutes.CHART_CONSTRUCTOR) { + ChartConstructorScreen( + onBackToWidgetConstructor = { navController.popBackStack() } + ) + } + composable(AirMqRoutes.NEWS_CONSTRUCTOR) { + NewsConstructorScreen( + onBackToWidgetConstructor = { navController.popBackStack() } + ) + } + } +} diff --git a/app/src/main/kotlin/org/db3/airmq/features/navigation/AirMqRoutes.kt b/app/src/main/kotlin/org/db3/airmq/features/navigation/AirMqRoutes.kt new file mode 100644 index 0000000..a844cf2 --- /dev/null +++ b/app/src/main/kotlin/org/db3/airmq/features/navigation/AirMqRoutes.kt @@ -0,0 +1,27 @@ +package org.db3.airmq.features.navigation + +object AirMqRoutes { + const val SPLASH = "entry/splash" + const val WIZARD = "entry/wizard" + const val DASHBOARD = "tab/dashboard" + const val MAP = "tab/map" + const val MANAGE = "tab/manage" + const val CITY = "detail/city" + const val SETTINGS = "detail/settings" + const val DEBUG = "detail/debug" + const val LOCATION = "detail/location" + const val SETUP = "detail/setup" + const val LOGIN = "detail/login" + const val NEWS = "detail/news" + const val NEWS_DETAIL = "detail/news/{newsId}" + const val DEVICE = "detail/device/{deviceId}" + const val WIDGET_CONSTRUCTOR = "constructor/widget" + const val SELECT_MAP_WIDGET_LOCATION = "constructor/select-map-widget-location" + const val MAP_CONSTRUCTOR = "constructor/map" + const val CHART_CONSTRUCTOR = "constructor/chart" + const val NEWS_CONSTRUCTOR = "constructor/news" + + fun device(deviceId: String = "mock-device-id"): String = "detail/device/$deviceId" + + fun newsDetail(newsId: String = "mock-news-id"): String = "detail/news/$newsId" +} diff --git a/app/src/main/kotlin/org/db3/airmq/features/news/NewsDetailScreen.kt b/app/src/main/kotlin/org/db3/airmq/features/news/NewsDetailScreen.kt new file mode 100644 index 0000000..8665c79 --- /dev/null +++ b/app/src/main/kotlin/org/db3/airmq/features/news/NewsDetailScreen.kt @@ -0,0 +1,14 @@ +package org.db3.airmq.features.news + +import androidx.compose.runtime.Composable +import org.db3.airmq.features.common.MockScreenScaffold +import org.db3.airmq.features.common.ScreenAction + +@Composable +fun NewsDetailScreen(newsId: String, onBackToNews: () -> Unit) { + MockScreenScaffold( + title = "News Detail", + subtitle = "Mock newsId: $newsId", + actions = listOf(ScreenAction("Back to News", onBackToNews)) + ) +} diff --git a/app/src/main/kotlin/org/db3/airmq/features/news/NewsScreen.kt b/app/src/main/kotlin/org/db3/airmq/features/news/NewsScreen.kt new file mode 100644 index 0000000..f221bcb --- /dev/null +++ b/app/src/main/kotlin/org/db3/airmq/features/news/NewsScreen.kt @@ -0,0 +1,20 @@ +package org.db3.airmq.features.news + +import androidx.compose.runtime.Composable +import org.db3.airmq.features.common.MockScreenScaffold +import org.db3.airmq.features.common.ScreenAction + +@Composable +fun NewsScreen( + onOpenNewsDetail: () -> Unit, + onBackToDashboard: () -> Unit +) { + MockScreenScaffold( + title = "News", + subtitle = "Mock news list screen.", + actions = listOf( + ScreenAction("Open News Detail", onOpenNewsDetail), + ScreenAction("Back to Dashboard", onBackToDashboard) + ) + ) +} diff --git a/app/src/main/kotlin/org/db3/airmq/features/settings/SettingsScreen.kt b/app/src/main/kotlin/org/db3/airmq/features/settings/SettingsScreen.kt new file mode 100644 index 0000000..ca7f438 --- /dev/null +++ b/app/src/main/kotlin/org/db3/airmq/features/settings/SettingsScreen.kt @@ -0,0 +1,22 @@ +package org.db3.airmq.features.settings + +import androidx.compose.runtime.Composable +import org.db3.airmq.features.common.MockScreenScaffold +import org.db3.airmq.features.common.ScreenAction + +@Composable +fun SettingsScreen( + onOpenDebug: () -> Unit, + onOpenCity: () -> Unit, + onLogOutToManage: () -> Unit +) { + MockScreenScaffold( + title = "Settings", + subtitle = "Settings and account actions.", + actions = listOf( + ScreenAction("Open Debug", onOpenDebug), + ScreenAction("Open City", onOpenCity), + ScreenAction("Log Out to Manage", onLogOutToManage) + ) + ) +} diff --git a/app/src/main/kotlin/org/db3/airmq/features/setup/SetupScreen.kt b/app/src/main/kotlin/org/db3/airmq/features/setup/SetupScreen.kt new file mode 100644 index 0000000..d165cf4 --- /dev/null +++ b/app/src/main/kotlin/org/db3/airmq/features/setup/SetupScreen.kt @@ -0,0 +1,20 @@ +package org.db3.airmq.features.setup + +import androidx.compose.runtime.Composable +import org.db3.airmq.features.common.MockScreenScaffold +import org.db3.airmq.features.common.ScreenAction + +@Composable +fun SetupScreen( + onFinishSetup: () -> Unit, + onCancelSetup: () -> Unit +) { + MockScreenScaffold( + title = "Setup", + subtitle = "Mock setup flow for device onboarding.", + actions = listOf( + ScreenAction("Finish Setup", onFinishSetup), + ScreenAction("Cancel Setup", onCancelSetup) + ) + ) +} diff --git a/app/src/main/kotlin/org/db3/airmq/ui/theme/Color.kt b/app/src/main/kotlin/org/db3/airmq/ui/theme/Color.kt new file mode 100644 index 0000000..89408e0 --- /dev/null +++ b/app/src/main/kotlin/org/db3/airmq/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package org.db3.airmq.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/app/src/main/kotlin/org/db3/airmq/ui/theme/Theme.kt b/app/src/main/kotlin/org/db3/airmq/ui/theme/Theme.kt new file mode 100644 index 0000000..eeb3969 --- /dev/null +++ b/app/src/main/kotlin/org/db3/airmq/ui/theme/Theme.kt @@ -0,0 +1,58 @@ +package org.db3.airmq.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun AirMQTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/org/db3/airmq/ui/theme/Type.kt b/app/src/main/kotlin/org/db3/airmq/ui/theme/Type.kt new file mode 100644 index 0000000..08b1022 --- /dev/null +++ b/app/src/main/kotlin/org/db3/airmq/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package org.db3.airmq.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..df06fa1 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + AirMQ + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..25b5420 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +