Initial commit

This commit is contained in:
2026-02-28 14:26:16 +01:00
commit 43c21a0cd5
71 changed files with 1750 additions and 0 deletions

1
app/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

66
app/build.gradle.kts Normal file
View File

@@ -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)
}

21
app/proguard-rules.pro vendored Normal file
View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AirMQ">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.AirMQ">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -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())
}
}
}
}

View File

@@ -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))
)
}

View File

@@ -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<ScreenAction> = 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<ScreenAction>,
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)
}
}
}
}

View File

@@ -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))
)
}

View File

@@ -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))
)
}

View File

@@ -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))
)
}

View File

@@ -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))
)
}

View File

@@ -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)
)
)
}

View File

@@ -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)
)
)
}

View File

@@ -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))
)
}

View File

@@ -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)
)
)
}

View File

@@ -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))
)
}

View File

@@ -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))
)
}

View File

@@ -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))
)
}

View File

@@ -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))
)
}

View File

@@ -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)
)
)
}

View File

@@ -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)
)
)
}

View File

@@ -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() }
)
}
}
}

View File

@@ -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"
}

View File

@@ -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))
)
}

View File

@@ -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)
)
)
}

View File

@@ -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)
)
)
}

View File

@@ -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)
)
)
}

View File

@@ -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)

View File

@@ -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
)
}

View File

@@ -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
)
*/
)

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@@ -0,0 +1,6 @@
<?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" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?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" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">AirMQ</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.AirMQ" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

@@ -0,0 +1,17 @@
package org.db3.airmq
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}