commit f38123a1ae0508895e755a6556bc5bb0e5fb9eb6 Author: admin Date: Tue Jun 30 19:13:44 2026 +0200 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6253853 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.gradle/ +build/ +local.properties +*/build/ +*.iml diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..a2e5a92 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,52 @@ +# Design System: TPP Kierowca + +## 1. Visual Theme & Atmosphere +Mobile-first industrial logistics interface for TPP drivers working in transport and waste management. The UI must feel durable, calm, and immediately readable in a cab: high contrast, large targets, structured cards, no decorative softness. Density is daily-app balanced, with enough information on route cards to scan the day quickly. + +## 2. Color Palette & Roles +- **TPP Forest** (#154212) - Primary actions, active state, top route accents. +- **TPP Container Green** (#2D5A27) - Branded panels, selected chips, success surfaces. +- **Logo Leaf** (#609830) - Small brand highlights and progress accents only. +- **Operational Navy** (#1B365D) - Secondary actions and structural emphasis. +- **Field Surface** (#F7FAF8) - Primary app background. +- **Manifest Surface** (#FFFFFF) - Cards, inputs, photo tiles. +- **Panel Surface** (#EBEFED) - Section bands and secondary containers. +- **Charcoal Ink** (#181C1B) - Primary text. +- **Muted Steel** (#42493E) - Secondary text and metadata. +- **Industrial Outline** (#72796E) - Important borders and dividers. +- **Soft Outline** (#C2C9BB) - Low-emphasis borders. +- **Critical Red** (#BA1A1A) - Errors and destructive messaging. + +## 3. Typography Rules +- **Display / Headline:** Hanken Grotesk, 32sp/24sp/20sp, weight 700/600, tight but not oversized. +- **Body:** Hanken Grotesk, 18sp for driver-facing copy, 16sp for supporting text, relaxed line height. +- **Technical Labels:** JetBrains Mono, 14sp/12sp, for route time, route ID, contract code, status metadata. +- Use Android system fallback if bundled fonts are not present, but keep the same scale and weight rhythm. + +## 4. Spacing, Shape & Components +- Base grid is 8dp. Screen margin 16dp, route-card padding 20dp, list gap 16dp. +- Minimum touch target is 48dp. Main action buttons are 56dp high; route-critical actions use 64dp. +- Buttons and inputs use 4dp radius. Cards and sheets use 8dp. Photo thumbnails use 12dp. +- **Route Card:** White surface, 1dp Soft Outline border, 4dp left status strip, mono contract chip, large origin/destination. +- **OTP Input:** Six visible digits, mono, centered, high contrast, no floating labels. +- **Photo Tiles:** First two tiles are camera and gallery actions; uploaded cargo photos appear in a 2-column grid. +- **Status Chips:** Rectangular, low radius, text plus tonal background; never rely on color alone. + +## 5. Compose Implementation Rules +- Use Material 3 with a fixed light TPP theme. +- Use icons for phone, lock, route, camera, gallery, refresh, back, and logout actions. +- No emoji, neon glow, gradient text, blurred blobs, oversized marketing hero, or generic stock imagery. +- Lists use skeleton-like loading blocks where possible and clear empty states. +- Text must not overlap and must fit on small Android screens with large system font. + +## 6. Screens +- **Phone Login:** TPP brand header, phone number input, primary "Wyślij kod" action. +- **OTP:** masked phone, six-digit code input, resend action after failure or timeout. +- **Today Routes:** greeting, sync time, route cards for today's dispatcher assignments. +- **Route Detail:** manifest layout with route, contract, vehicle and notes sections, then cargo photo grid. +- **Photo Preview:** full-screen dark viewer with top back action and image fit-to-screen. + +## 7. Motion & Interaction +- Use short Material spring/tween transitions through Navigation Compose. +- Active cards and buttons use subtle pressed opacity/scale, not glow. +- Refresh and upload states must remain visible; never leave the driver guessing after tapping camera/gallery. diff --git a/README.md b/README.md new file mode 100644 index 0000000..bab89c5 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# TPP Kierowca + +Natywna aplikacja Android Jetpack Compose dla kierowcow TPP. + +## Funkcje + +- Logowanie numerem telefonu i kodem OTP. +- Lista dzisiejszych kursow kierowcy. +- Szczegoly kursu: relacja, kontrakt, status, pojazd i notatki. +- Dodawanie zdjec ladunku z aparatu lub galerii. +- Podglad zdjec ladunku w aplikacji. + +## Build + +```bash +./gradlew :app:assembleDebug +``` + +## Testy + +```bash +./gradlew :app:testDebugUnitTest +``` diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..711299c --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,81 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "pl.firmatpp.kierowca" + compileSdk = 35 + + defaultConfig { + applicationId = "pl.firmatpp.kierowca" + minSdk = 26 + targetSdk = 35 + versionCode = 1 + versionName = "1.0.0" + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + buildConfigField("String", "API_BASE_URL", "\"https://api-intranet.firmatpp.pl/api/\"") + } + + buildTypes { + debug { + isMinifyEnabled = false + } + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } + + buildFeatures { + compose = true + buildConfig = true + } +} + +dependencies { + val composeBom = platform(libs.compose.bom) + implementation(composeBom) + + implementation(libs.activity.compose) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.datastore.preferences) + implementation(libs.androidx.lifecycle.runtime.compose) + implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.androidx.navigation.compose) + implementation(libs.androidx.work.runtime.ktx) + implementation(libs.camera.camera2) + implementation(libs.camera.core) + implementation(libs.camera.lifecycle) + implementation(libs.camera.view) + implementation(libs.coil.compose) + implementation(libs.compose.material.icons.extended) + implementation(libs.compose.material3) + implementation(libs.compose.ui) + implementation(libs.compose.ui.graphics) + implementation(libs.compose.ui.tooling.preview) + implementation(libs.coroutines.android) + implementation(libs.okhttp) + implementation(libs.okhttp.logging) + implementation(libs.retrofit) + implementation(libs.retrofit.gson) + + debugImplementation(libs.compose.ui.tooling) + + testImplementation(libs.junit) + testImplementation(libs.coroutines.test) +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1 @@ + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..87617fe --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/pl/firmatpp/kierowca/MainActivity.kt b/app/src/main/java/pl/firmatpp/kierowca/MainActivity.kt new file mode 100644 index 0000000..e234944 --- /dev/null +++ b/app/src/main/java/pl/firmatpp/kierowca/MainActivity.kt @@ -0,0 +1,21 @@ +package pl.firmatpp.kierowca + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.lifecycle.viewmodel.compose.viewModel +import pl.firmatpp.kierowca.ui.DriverApp +import pl.firmatpp.kierowca.ui.DriverViewModel +import pl.firmatpp.kierowca.ui.theme.TppKierowcaTheme + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + TppKierowcaTheme { + val viewModel: DriverViewModel = viewModel() + DriverApp(viewModel = viewModel) + } + } + } +} diff --git a/app/src/main/java/pl/firmatpp/kierowca/data/DriverRepository.kt b/app/src/main/java/pl/firmatpp/kierowca/data/DriverRepository.kt new file mode 100644 index 0000000..14a2514 --- /dev/null +++ b/app/src/main/java/pl/firmatpp/kierowca/data/DriverRepository.kt @@ -0,0 +1,65 @@ +package pl.firmatpp.kierowca.data + +import android.content.Context +import android.net.Uri +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.toRequestBody +import pl.firmatpp.kierowca.data.api.ApiFactory +import pl.firmatpp.kierowca.data.api.MobileDriverApi +import pl.firmatpp.kierowca.data.model.BootstrapResponse +import pl.firmatpp.kierowca.data.model.DriverDto +import pl.firmatpp.kierowca.data.model.OtpResponse +import pl.firmatpp.kierowca.data.model.PhotoUploadResponse +import pl.firmatpp.kierowca.data.model.RequestOtpBody +import pl.firmatpp.kierowca.data.model.RouteResponse +import pl.firmatpp.kierowca.data.model.VerifyOtpBody + +class DriverRepository( + private val context: Context, + private val api: MobileDriverApi = ApiFactory.mobileDriverApi(), + private val tokenStore: TokenStore = TokenStore(context), +) { + suspend fun requestOtp(phoneNumber: String): OtpResponse = + api.requestOtp(RequestOtpBody(phoneNumber)) + + suspend fun verifyOtp(phoneNumber: String, code: String, deviceName: String): DriverDto { + val response = api.verifyOtp(VerifyOtpBody(phoneNumber, code, deviceName)) + val token = response.accessToken ?: error("Brak tokenu sesji.") + tokenStore.save(token) + return response.driver ?: error("Brak danych kierowcy.") + } + + suspend fun bootstrap(): BootstrapResponse = + api.bootstrap(authHeader(requireToken())) + + suspend fun route(routeId: String): RouteResponse = + api.route(authHeader(requireToken()), routeId) + + suspend fun uploadPhoto(routeId: String, uri: Uri, source: String): PhotoUploadResponse { + val resolver = context.contentResolver + val mimeType = resolver.getType(uri) ?: "image/jpeg" + val bytes = resolver.openInputStream(uri)?.use { it.readBytes() } ?: error("Nie mozna odczytac zdjecia.") + val body = bytes.toRequestBody(mimeType.toMediaTypeOrNull()) + val photo = MultipartBody.Part.createFormData("photo", "ladunek.jpg", body) + val sourceBody = source.toRequestBody("text/plain".toMediaTypeOrNull()) + + return api.uploadPhoto(authHeader(requireToken()), routeId, photo, sourceBody) + } + + suspend fun logout() { + val token = tokenStore.read() + if (token != null) { + runCatching { api.logout(authHeader(token)) } + } + tokenStore.clear() + } + + suspend fun hasToken(): Boolean = tokenStore.read() != null + + suspend fun imageAuthHeader(): String? = tokenStore.read()?.let(::authHeader) + + private suspend fun requireToken(): String = tokenStore.read() ?: error("Brak aktywnej sesji.") + + private fun authHeader(token: String): String = "Bearer $token" +} diff --git a/app/src/main/java/pl/firmatpp/kierowca/data/TokenStore.kt b/app/src/main/java/pl/firmatpp/kierowca/data/TokenStore.kt new file mode 100644 index 0000000..3855356 --- /dev/null +++ b/app/src/main/java/pl/firmatpp/kierowca/data/TokenStore.kt @@ -0,0 +1,28 @@ +package pl.firmatpp.kierowca.data + +import android.content.Context +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map + +private val Context.driverDataStore by preferencesDataStore(name = "driver_session") + +class TokenStore(private val context: Context) { + private val tokenKey = stringPreferencesKey("access_token") + + suspend fun save(token: String) { + context.driverDataStore.edit { preferences -> + preferences[tokenKey] = token + } + } + + suspend fun read(): String? = context.driverDataStore.data + .map { it[tokenKey] } + .first() + + suspend fun clear() { + context.driverDataStore.edit { it.remove(tokenKey) } + } +} diff --git a/app/src/main/java/pl/firmatpp/kierowca/data/api/ApiFactory.kt b/app/src/main/java/pl/firmatpp/kierowca/data/api/ApiFactory.kt new file mode 100644 index 0000000..778cc19 --- /dev/null +++ b/app/src/main/java/pl/firmatpp/kierowca/data/api/ApiFactory.kt @@ -0,0 +1,26 @@ +package pl.firmatpp.kierowca.data.api + +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import pl.firmatpp.kierowca.BuildConfig +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +object ApiFactory { + fun mobileDriverApi(): MobileDriverApi { + val logging = HttpLoggingInterceptor().apply { + level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BASIC else HttpLoggingInterceptor.Level.NONE + } + + val client = OkHttpClient.Builder() + .addInterceptor(logging) + .build() + + return Retrofit.Builder() + .baseUrl(BuildConfig.API_BASE_URL) + .client(client) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(MobileDriverApi::class.java) + } +} diff --git a/app/src/main/java/pl/firmatpp/kierowca/data/api/MobileDriverApi.kt b/app/src/main/java/pl/firmatpp/kierowca/data/api/MobileDriverApi.kt new file mode 100644 index 0000000..b3b85a0 --- /dev/null +++ b/app/src/main/java/pl/firmatpp/kierowca/data/api/MobileDriverApi.kt @@ -0,0 +1,47 @@ +package pl.firmatpp.kierowca.data.api + +import okhttp3.MultipartBody +import okhttp3.RequestBody +import pl.firmatpp.kierowca.data.model.BootstrapResponse +import pl.firmatpp.kierowca.data.model.OtpResponse +import pl.firmatpp.kierowca.data.model.PhotoUploadResponse +import pl.firmatpp.kierowca.data.model.RequestOtpBody +import pl.firmatpp.kierowca.data.model.RouteResponse +import pl.firmatpp.kierowca.data.model.VerifyOtpBody +import pl.firmatpp.kierowca.data.model.VerifyOtpResponse +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.Multipart +import retrofit2.http.POST +import retrofit2.http.Part +import retrofit2.http.Path + +interface MobileDriverApi { + @POST("mobile/driver/auth/request-otp") + suspend fun requestOtp(@Body body: RequestOtpBody): OtpResponse + + @POST("mobile/driver/auth/verify-otp") + suspend fun verifyOtp(@Body body: VerifyOtpBody): VerifyOtpResponse + + @POST("mobile/driver/auth/logout") + suspend fun logout(@Header("Authorization") authorization: String): Map + + @GET("mobile/driver/bootstrap") + suspend fun bootstrap(@Header("Authorization") authorization: String): BootstrapResponse + + @GET("mobile/driver/routes/{routeId}") + suspend fun route( + @Header("Authorization") authorization: String, + @Path("routeId") routeId: String, + ): RouteResponse + + @Multipart + @POST("mobile/driver/routes/{routeId}/photos") + suspend fun uploadPhoto( + @Header("Authorization") authorization: String, + @Path("routeId") routeId: String, + @Part photo: MultipartBody.Part, + @Part("source") source: RequestBody, + ): PhotoUploadResponse +} diff --git a/app/src/main/java/pl/firmatpp/kierowca/data/model/DriverModels.kt b/app/src/main/java/pl/firmatpp/kierowca/data/model/DriverModels.kt new file mode 100644 index 0000000..167a202 --- /dev/null +++ b/app/src/main/java/pl/firmatpp/kierowca/data/model/DriverModels.kt @@ -0,0 +1,82 @@ +package pl.firmatpp.kierowca.data.model + +data class RequestOtpBody(val phoneNumber: String) + +data class VerifyOtpBody( + val phoneNumber: String, + val code: String, + val deviceName: String, +) + +data class OtpResponse( + val ok: Boolean, + val phoneMasked: String?, + val expiresInSeconds: Int?, +) + +data class VerifyOtpResponse( + val ok: Boolean, + val accessToken: String?, + val expiresAt: String?, + val driver: DriverDto?, +) + +data class DriverDto( + val id: String, + val displayName: String, + val anonymousLabel: String?, +) + +data class BootstrapResponse( + val session: DriverSessionDto, + val routes: RoutesBucketDto, +) + +data class DriverSessionDto( + val driver: DriverDto, + val lastSyncAt: String, +) + +data class RoutesBucketDto( + val today: List = emptyList(), +) + +data class RouteResponse( + val route: DriverRouteDto, +) + +data class DriverRouteDto( + val id: String, + val startsAt: String, + val originName: String, + val destinationName: String, + val contractorName: String, + val contractName: String?, + val contractCode: String?, + val relationLabel: String, + val status: String, + val distanceKm: Double?, + val notes: String?, + val truck: TruckDto?, + val photos: List = emptyList(), +) + +data class TruckDto( + val id: String, + val registration: String?, + val name: String?, +) + +data class RoutePhotoDto( + val id: String, + val routeId: String, + val source: String, + val mimeType: String?, + val size: Long, + val url: String, + val createdAt: String?, +) + +data class PhotoUploadResponse( + val photo: RoutePhotoDto, +) diff --git a/app/src/main/java/pl/firmatpp/kierowca/domain/PhoneNumberFormatter.kt b/app/src/main/java/pl/firmatpp/kierowca/domain/PhoneNumberFormatter.kt new file mode 100644 index 0000000..c09edcd --- /dev/null +++ b/app/src/main/java/pl/firmatpp/kierowca/domain/PhoneNumberFormatter.kt @@ -0,0 +1,12 @@ +package pl.firmatpp.kierowca.domain + +object PhoneNumberFormatter { + fun normalize(input: String): String { + var digits = input.filter(Char::isDigit) + if (digits.startsWith("00")) digits = digits.drop(2) + if (digits.length == 9) digits = "48$digits" + return digits + } + + fun mask(normalized: String): String = "+48 *** *** ${normalized.takeLast(3)}" +} diff --git a/app/src/main/java/pl/firmatpp/kierowca/domain/RouteDisplayMapper.kt b/app/src/main/java/pl/firmatpp/kierowca/domain/RouteDisplayMapper.kt new file mode 100644 index 0000000..16d02cd --- /dev/null +++ b/app/src/main/java/pl/firmatpp/kierowca/domain/RouteDisplayMapper.kt @@ -0,0 +1,39 @@ +package pl.firmatpp.kierowca.domain + +import kotlin.math.roundToInt +import pl.firmatpp.kierowca.data.model.DriverRouteDto + +data class RouteCardDisplay( + val id: String, + val origin: String, + val destination: String, + val contractLabel: String, + val time: String, + val status: String, + val distanceLabel: String, +) + +object RouteDisplayMapper { + fun toCard(route: DriverRouteDto): RouteCardDisplay { + val contract = route.contractCode + ?.takeIf { it.isNotBlank() } + ?: route.contractName?.takeIf { it.isNotBlank() } + ?: route.contractorName + + val distance = route.distanceKm + ?.takeIf { it > 0.0 } + ?.roundToInt() + ?.let { "$it km" } + ?: "" + + return RouteCardDisplay( + id = route.id, + origin = route.originName.ifBlank { "Start" }, + destination = route.destinationName.ifBlank { "Cel" }, + contractLabel = contract, + time = route.startsAt.ifBlank { "--:--" }, + status = route.status, + distanceLabel = distance, + ) + } +} diff --git a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt new file mode 100644 index 0000000..bfa869a --- /dev/null +++ b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt @@ -0,0 +1,374 @@ +package pl.firmatpp.kierowca.ui + +import android.content.Context +import android.net.Uri +import androidx.activity.compose.ManagedActivityResultLauncher +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.AddPhotoAlternate +import androidx.compose.material.icons.outlined.ArrowBack +import androidx.compose.material.icons.outlined.CameraAlt +import androidx.compose.material.icons.outlined.LocalShipping +import androidx.compose.material.icons.outlined.Logout +import androidx.compose.material.icons.outlined.Phone +import androidx.compose.material.icons.outlined.Refresh +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.unit.dp +import androidx.core.content.FileProvider +import coil.compose.AsyncImage +import coil.request.ImageRequest +import java.io.File +import pl.firmatpp.kierowca.data.model.DriverRouteDto +import pl.firmatpp.kierowca.data.model.RoutePhotoDto +import pl.firmatpp.kierowca.domain.RouteDisplayMapper +import pl.firmatpp.kierowca.ui.theme.TppColors + +@Composable +fun DriverApp(viewModel: DriverViewModel) { + val state by viewModel.state.collectAsState() + + Box(Modifier.fillMaxSize().background(TppColors.Surface)) { + when (state.screen) { + DriverScreen.Phone -> PhoneScreen(state, viewModel::requestOtp) + DriverScreen.Otp -> OtpScreen(state, viewModel::verifyOtp, viewModel::back) + DriverScreen.Routes -> RoutesScreen(state, viewModel::refreshRoutes, viewModel::openRoute, viewModel::logout) + DriverScreen.Detail -> DetailScreen(state, viewModel::back, viewModel::uploadPhoto, viewModel::openPhoto) + DriverScreen.Photo -> PhotoScreen(state, viewModel::back) + } + + if (state.loading) { + Box(Modifier.fillMaxSize().background(Color.White.copy(alpha = 0.42f)), contentAlignment = Alignment.Center) { + CircularProgressIndicator(color = TppColors.Forest) + } + } + } +} + +@Composable +private fun PhoneScreen(state: DriverUiState, onSubmit: (String) -> Unit) { + var phone by remember { mutableStateOf(state.phone) } + AuthShell(title = "TPP Kierowca", subtitle = "Zaloguj sie numerem telefonu kierowcy.") { + OutlinedTextField( + value = phone, + onValueChange = { phone = it }, + label = { Text("Numer telefonu") }, + leadingIcon = { Icon(Icons.Outlined.Phone, contentDescription = null) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone), + modifier = Modifier.fillMaxWidth(), + singleLine = true, + ) + ErrorText(state.error) + PrimaryButton("Wyslij kod") { onSubmit(phone) } + } +} + +@Composable +private fun OtpScreen(state: DriverUiState, onSubmit: (String) -> Unit, onBack: () -> Unit) { + var code by remember { mutableStateOf("") } + AuthShell(title = "Kod SMS", subtitle = "Wpisz kod wyslany na ${state.maskedPhone}.", onBack = onBack) { + OutlinedTextField( + value = code, + onValueChange = { code = it.filter(Char::isDigit).take(6) }, + label = { Text("Kod OTP") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword), + modifier = Modifier.fillMaxWidth(), + singleLine = true, + ) + ErrorText(state.error) + PrimaryButton("Zaloguj") { onSubmit(code) } + } +} + +@Composable +private fun AuthShell(title: String, subtitle: String, onBack: (() -> Unit)? = null, content: @Composable ColumnScope.() -> Unit) { + Column( + Modifier.fillMaxSize().padding(20.dp), + verticalArrangement = Arrangement.Center, + ) { + if (onBack != null) IconButton(onClick = onBack) { Icon(Icons.Outlined.ArrowBack, contentDescription = "Wstecz") } + Text(title, style = MaterialTheme.typography.headlineLarge, fontWeight = FontWeight.Bold, color = TppColors.Forest) + Text(subtitle, color = TppColors.Muted, modifier = Modifier.padding(top = 8.dp, bottom = 24.dp)) + content() + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun RoutesScreen( + state: DriverUiState, + onRefresh: () -> Unit, + onRoute: (String) -> Unit, + onLogout: () -> Unit, +) { + Scaffold( + topBar = { + TopAppBar( + title = { Text("Dzisiejsze kursy") }, + actions = { + IconButton(onClick = onRefresh) { Icon(Icons.Outlined.Refresh, contentDescription = "Odswiez") } + IconButton(onClick = onLogout) { Icon(Icons.Outlined.Logout, contentDescription = "Wyloguj") } + }, + colors = TopAppBarDefaults.topAppBarColors(containerColor = TppColors.Surface), + ) + }, + containerColor = TppColors.Surface, + ) { padding -> + LazyColumn( + Modifier.fillMaxSize().padding(padding), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + item { + Text("Kierowca: ${state.driver?.displayName.orEmpty()}", color = TppColors.Muted) + } + if (state.routes.isEmpty()) { + item { EmptyState("Brak kursow na dzisiaj") } + } else { + items(state.routes, key = { it.id }) { route -> RouteCard(route, onRoute) } + } + item { ErrorText(state.error) } + } + } +} + +@Composable +private fun RouteCard(route: DriverRouteDto, onRoute: (String) -> Unit) { + val display = RouteDisplayMapper.toCard(route) + Card( + modifier = Modifier.fillMaxWidth().clickable { onRoute(route.id) }, + colors = CardDefaults.cardColors(containerColor = Color.White), + shape = RoundedCornerShape(8.dp), + border = CardDefaults.outlinedCardBorder(), + ) { + Row(Modifier.fillMaxWidth()) { + Box(Modifier.size(width = 5.dp, height = 132.dp).background(TppColors.Forest)) + Column(Modifier.padding(18.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text(display.time, fontFamily = FontFamily.Monospace, color = TppColors.Navy, fontWeight = FontWeight.Bold) + Text("${display.origin} -> ${display.destination}", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) + Text(display.contractLabel, fontFamily = FontFamily.Monospace, color = TppColors.Muted) + Text(listOf(display.status, display.distanceLabel).filter { it.isNotBlank() }.joinToString(" / "), color = TppColors.Muted) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun DetailScreen( + state: DriverUiState, + onBack: () -> Unit, + onUpload: (Uri, String) -> Unit, + onPhoto: (RoutePhotoDto) -> Unit, +) { + val context = LocalContext.current + var cameraUri by remember { mutableStateOf(null) } + val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { ok -> + val capturedUri = cameraUri + if (ok && capturedUri != null) onUpload(capturedUri, "camera") + } + val pickerLauncher = rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri -> + if (uri != null) onUpload(uri, "gallery") + } + val route = state.selectedRoute + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Szczegoly kursu") }, + navigationIcon = { IconButton(onClick = onBack) { Icon(Icons.Outlined.ArrowBack, contentDescription = "Wstecz") } }, + colors = TopAppBarDefaults.topAppBarColors(containerColor = TppColors.Surface), + ) + }, + containerColor = TppColors.Surface, + ) { padding -> + if (route == null) return@Scaffold + LazyColumn( + Modifier.fillMaxSize().padding(padding), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + item { ManifestSection(route) } + item { + Text("Zdjecia ladunku", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) + Spacer(Modifier.height(12.dp)) + PhotoGrid(route.photos, state.imageAuthHeader, onPhoto, onCamera = { + val newUri = createCameraUri(context) + cameraUri = newUri + cameraLauncher.launch(newUri) + }, onGallery = { + pickerLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) + }) + } + item { ErrorText(state.error) } + } + } +} + +@Composable +private fun ManifestSection(route: DriverRouteDto) { + Card(colors = CardDefaults.cardColors(containerColor = Color.White), shape = RoundedCornerShape(8.dp)) { + Column(Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { + Icon(Icons.Outlined.LocalShipping, contentDescription = null, tint = TppColors.Forest) + Text(route.relationLabel, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold) + Text("Kontrakt: ${route.contractCode ?: route.contractName ?: route.contractorName}", fontFamily = FontFamily.Monospace) + Text("Status: ${route.status}", color = TppColors.Muted) + if (!route.notes.isNullOrBlank()) Text(route.notes, color = TppColors.Muted) + } + } +} + +@Composable +private fun PhotoGrid( + photos: List, + imageAuthHeader: String?, + onPhoto: (RoutePhotoDto) -> Unit, + onCamera: () -> Unit, + onGallery: () -> Unit, +) { + LazyVerticalGrid( + columns = GridCells.Fixed(2), + modifier = Modifier.height(360.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + item { ActionTile("Aparat", Icons.Outlined.CameraAlt, onCamera) } + item { ActionTile("Galeria", Icons.Outlined.AddPhotoAlternate, onGallery) } + items(photos, key = { it.id }) { photo -> + AsyncImage( + model = imageRequest(photo.url, imageAuthHeader), + contentDescription = "Zdjecie ladunku", + contentScale = ContentScale.Crop, + modifier = Modifier.aspectRatio(1f).clickable { onPhoto(photo) }, + ) + } + } +} + +@Composable +private fun ActionTile(label: String, icon: androidx.compose.ui.graphics.vector.ImageVector, onClick: () -> Unit) { + Card( + modifier = Modifier.aspectRatio(1f).clickable(onClick = onClick), + colors = CardDefaults.cardColors(containerColor = TppColors.Panel), + shape = RoundedCornerShape(12.dp), + ) { + Column(Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) { + Icon(icon, contentDescription = label, tint = TppColors.Forest) + Text(label, color = TppColors.Muted) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun PhotoScreen(state: DriverUiState, onBack: () -> Unit) { + val photo = state.selectedPhoto + Scaffold( + topBar = { + TopAppBar( + title = { Text("Podglad zdjecia") }, + navigationIcon = { IconButton(onClick = onBack) { Icon(Icons.Outlined.ArrowBack, contentDescription = "Wstecz") } }, + colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Black, titleContentColor = Color.White, navigationIconContentColor = Color.White), + ) + }, + containerColor = Color.Black, + ) { padding -> + AsyncImage( + model = imageRequest(photo?.url, state.imageAuthHeader), + contentDescription = "Zdjecie ladunku", + contentScale = ContentScale.Fit, + modifier = Modifier.fillMaxSize().padding(padding), + ) + } +} + +@Composable +private fun PrimaryButton(label: String, onClick: () -> Unit) { + Button( + onClick = onClick, + modifier = Modifier.fillMaxWidth().height(56.dp), + colors = ButtonDefaults.buttonColors(containerColor = TppColors.Forest), + shape = RoundedCornerShape(4.dp), + ) { Text(label, fontWeight = FontWeight.Bold) } +} + +@Composable +private fun EmptyState(text: String) { + Card(colors = CardDefaults.cardColors(containerColor = Color.White), shape = RoundedCornerShape(8.dp)) { + Text(text, modifier = Modifier.padding(24.dp), color = TppColors.Muted) + } +} + +@Composable +private fun ErrorText(error: String?) { + if (!error.isNullOrBlank()) Text(error, color = TppColors.Error, modifier = Modifier.padding(vertical = 8.dp)) +} + +private fun createCameraUri(context: Context): Uri { + val dir = File(context.cacheDir, "camera").apply { mkdirs() } + val file = File.createTempFile("ladunek-", ".jpg", dir) + return FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file) +} + +@Composable +private fun imageRequest(url: String?, authorization: String?): ImageRequest { + val context = LocalContext.current + return ImageRequest.Builder(context) + .data(url) + .apply { + if (!authorization.isNullOrBlank()) addHeader("Authorization", authorization) + } + .crossfade(true) + .build() +} diff --git a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverViewModel.kt b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverViewModel.kt new file mode 100644 index 0000000..507e302 --- /dev/null +++ b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverViewModel.kt @@ -0,0 +1,114 @@ +package pl.firmatpp.kierowca.ui + +import android.app.Application +import android.net.Uri +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import pl.firmatpp.kierowca.data.DriverRepository +import pl.firmatpp.kierowca.data.model.DriverDto +import pl.firmatpp.kierowca.data.model.DriverRouteDto +import pl.firmatpp.kierowca.data.model.RoutePhotoDto + +enum class DriverScreen { Phone, Otp, Routes, Detail, Photo } + +data class DriverUiState( + val screen: DriverScreen = DriverScreen.Phone, + val loading: Boolean = false, + val phone: String = "", + val maskedPhone: String = "", + val driver: DriverDto? = null, + val routes: List = emptyList(), + val selectedRoute: DriverRouteDto? = null, + val selectedPhoto: RoutePhotoDto? = null, + val imageAuthHeader: String? = null, + val error: String? = null, +) + +class DriverViewModel(application: Application) : AndroidViewModel(application) { + private val repository = DriverRepository(application) + private val _state = MutableStateFlow(DriverUiState(loading = true)) + val state: StateFlow = _state + + init { + viewModelScope.launch { + if (repository.hasToken()) refreshRoutes() else _state.update { it.copy(loading = false) } + } + } + + fun requestOtp(phone: String) = runLoading { + val response = repository.requestOtp(phone) + _state.update { + it.copy( + screen = DriverScreen.Otp, + phone = phone, + maskedPhone = response.phoneMasked.orEmpty(), + error = null, + ) + } + } + + fun verifyOtp(code: String) = runLoading { + val driver = repository.verifyOtp(_state.value.phone, code, android.os.Build.MODEL ?: "Android") + _state.update { it.copy(driver = driver, imageAuthHeader = repository.imageAuthHeader()) } + refreshRoutes() + } + + fun refreshRoutes() = runLoading { + val response = repository.bootstrap() + _state.update { + it.copy( + screen = DriverScreen.Routes, + driver = response.session.driver, + routes = response.routes.today, + imageAuthHeader = repository.imageAuthHeader(), + error = null, + ) + } + } + + fun openRoute(routeId: String) = runLoading { + val response = repository.route(routeId) + _state.update { it.copy(screen = DriverScreen.Detail, selectedRoute = response.route, error = null) } + } + + fun uploadPhoto(uri: Uri, source: String) = runLoading { + val route = _state.value.selectedRoute ?: return@runLoading + repository.uploadPhoto(route.id, uri, source) + openRoute(route.id) + } + + fun openPhoto(photo: RoutePhotoDto) { + _state.update { it.copy(screen = DriverScreen.Photo, selectedPhoto = photo) } + } + + fun back() { + _state.update { + when (it.screen) { + DriverScreen.Photo -> it.copy(screen = DriverScreen.Detail, selectedPhoto = null) + DriverScreen.Detail -> it.copy(screen = DriverScreen.Routes, selectedRoute = null) + DriverScreen.Otp -> it.copy(screen = DriverScreen.Phone) + else -> it + } + } + } + + fun logout() = runLoading { + repository.logout() + _state.update { DriverUiState(screen = DriverScreen.Phone) } + } + + private fun runLoading(block: suspend () -> Unit) { + viewModelScope.launch { + _state.update { it.copy(loading = true, error = null) } + runCatching { block() } + .onFailure { throwable -> + _state.update { it.copy(error = throwable.message ?: "Wystapil blad.") } + } + _state.update { it.copy(loading = false) } + } + } +} diff --git a/app/src/main/java/pl/firmatpp/kierowca/ui/theme/Theme.kt b/app/src/main/java/pl/firmatpp/kierowca/ui/theme/Theme.kt new file mode 100644 index 0000000..39a51e1 --- /dev/null +++ b/app/src/main/java/pl/firmatpp/kierowca/ui/theme/Theme.kt @@ -0,0 +1,42 @@ +package pl.firmatpp.kierowca.ui.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +object TppColors { + val Forest = Color(0xFF154212) + val ContainerGreen = Color(0xFF2D5A27) + val LogoLeaf = Color(0xFF609830) + val Navy = Color(0xFF1B365D) + val Surface = Color(0xFFF7FAF8) + val Panel = Color(0xFFEBEFED) + val Ink = Color(0xFF181C1B) + val Muted = Color(0xFF42493E) + val Outline = Color(0xFFC2C9BB) + val Error = Color(0xFFBA1A1A) +} + +private val TppScheme = lightColorScheme( + primary = TppColors.Forest, + onPrimary = Color.White, + primaryContainer = TppColors.ContainerGreen, + secondary = TppColors.Navy, + background = TppColors.Surface, + surface = Color.White, + surfaceVariant = TppColors.Panel, + onBackground = TppColors.Ink, + onSurface = TppColors.Ink, + onSurfaceVariant = TppColors.Muted, + outline = TppColors.Outline, + error = TppColors.Error, +) + +@Composable +fun TppKierowcaTheme(content: @Composable () -> Unit) { + MaterialTheme( + colorScheme = TppScheme, + content = content, + ) +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..af35d75 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + TPP Kierowca + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..6453be8 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..2b50af2 --- /dev/null +++ b/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/test/java/pl/firmatpp/kierowca/domain/PhoneNumberFormatterTest.kt b/app/src/test/java/pl/firmatpp/kierowca/domain/PhoneNumberFormatterTest.kt new file mode 100644 index 0000000..79a872f --- /dev/null +++ b/app/src/test/java/pl/firmatpp/kierowca/domain/PhoneNumberFormatterTest.kt @@ -0,0 +1,18 @@ +package pl.firmatpp.kierowca.domain + +import org.junit.Assert.assertEquals +import org.junit.Test + +class PhoneNumberFormatterTest { + @Test + fun normalizesPolishDriverPhoneForBackend() { + assertEquals("48501222333", PhoneNumberFormatter.normalize("501 222 333")) + assertEquals("48501222333", PhoneNumberFormatter.normalize("+48 501-222-333")) + assertEquals("48501222333", PhoneNumberFormatter.normalize("0048 501 222 333")) + } + + @Test + fun masksPhoneLikeBackendResponse() { + assertEquals("+48 *** *** 333", PhoneNumberFormatter.mask("48501222333")) + } +} diff --git a/app/src/test/java/pl/firmatpp/kierowca/domain/RouteDisplayMapperTest.kt b/app/src/test/java/pl/firmatpp/kierowca/domain/RouteDisplayMapperTest.kt new file mode 100644 index 0000000..3d8ea4e --- /dev/null +++ b/app/src/test/java/pl/firmatpp/kierowca/domain/RouteDisplayMapperTest.kt @@ -0,0 +1,34 @@ +package pl.firmatpp.kierowca.domain + +import org.junit.Assert.assertEquals +import org.junit.Test +import pl.firmatpp.kierowca.data.model.DriverRouteDto + +class RouteDisplayMapperTest { + @Test + fun mapsRouteToDriverCardText() { + val route = DriverRouteDto( + id = "42", + startsAt = "09:15", + originName = "TPP Baza Krakow", + destinationName = "Instalacja Odpadowa Slask", + contractorName = "TPP Baza Krakow", + contractName = "Odpady przemyslowe", + contractCode = "TPP-ODP-24", + relationLabel = "TPP Baza Krakow -> Instalacja Odpadowa Slask", + status = "ZAPLANOWANA", + distanceKm = 91.4, + notes = null, + truck = null, + photos = emptyList(), + ) + + val display = RouteDisplayMapper.toCard(route) + + assertEquals("TPP Baza Krakow", display.origin) + assertEquals("Instalacja Odpadowa Slask", display.destination) + assertEquals("TPP-ODP-24", display.contractLabel) + assertEquals("09:15", display.time) + assertEquals("91 km", display.distanceLabel) + } +} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..9deb573 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,5 @@ +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.compose) apply false +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..d365f06 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,4 @@ +android.useAndroidX=true +android.nonTransitiveRClass=true +kotlin.code.style=official +org.gradle.jvmargs=-Xmx3072m -Dfile.encoding=UTF-8 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..f04f678 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,49 @@ +[versions] +agp = "8.7.3" +kotlin = "2.1.10" +composeBom = "2026.03.00" +activityCompose = "1.9.3" +navigationCompose = "2.8.5" +lifecycle = "2.8.7" +coreKtx = "1.15.0" +retrofit = "2.11.0" +okhttp = "4.12.0" +coroutines = "1.10.1" +datastore = "1.1.1" +coil = "2.7.0" +camerax = "1.4.1" +work = "2.10.0" +junit = "4.13.2" + +[libraries] +activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" } +androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" } +androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" } +androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } +androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work" } +camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "camerax" } +camera-core = { group = "androidx.camera", name = "camera-core", version.ref = "camerax" } +camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "camerax" } +camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "camerax" } +coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" } +compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } +compose-material3 = { group = "androidx.compose.material3", name = "material3" } +compose-ui = { group = "androidx.compose.ui", name = "ui" } +compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } +compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } +compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } +coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" } +coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" } +junit = { group = "junit", name = "junit", version.ref = "junit" } +okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } +okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } +retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } +retrofit-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..a4b76b9 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e2847c8 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..497a028 --- /dev/null +++ b/gradlew @@ -0,0 +1,141 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced functionality shells and target +# temporary micro-containers:// +# +# Implementations may assume +# /usr/bin/env +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld -- "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NonStop* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 ; then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is:// + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + ;; + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is:// + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + ;; + esac +fi + +# Collect all arguments for the java command. +set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "-Dorg.gradle.appname=$APP_BASE_NAME" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..a26d8ff --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %OS%==Windows_NT endlocal + +:omega + +@exit /b %ERRORLEVEL% + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..cdc5f21 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "TPPKierowca" +include(":app")