first commit

This commit is contained in:
admin
2026-06-30 19:13:44 +02:00
commit f38123a1ae
30 changed files with 1427 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
.gradle/
build/
local.properties
*/build/
*.iml
+52
View File
@@ -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.
+23
View File
@@ -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
```
+81
View File
@@ -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)
}
+1
View File
@@ -0,0 +1 @@
+30
View File
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<application
android:allowBackup="false"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.TppKierowca">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>
@@ -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)
}
}
}
}
@@ -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"
}
@@ -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) }
}
}
@@ -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)
}
}
@@ -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<String, Boolean>
@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
}
@@ -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<DriverRouteDto> = 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<RoutePhotoDto> = 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,
)
@@ -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)}"
}
@@ -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,
)
}
}
@@ -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<Uri?>(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<RoutePhotoDto>,
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()
}
@@ -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<DriverRouteDto> = 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<DriverUiState> = _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) }
}
}
}
@@ -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,
)
}
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">TPP Kierowca</string>
</resources>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.TppKierowca" parent="android:style/Theme.Material.Light.NoActionBar">
<item name="android:windowLightStatusBar">true</item>
<item name="android:statusBarColor">#F7FAF8</item>
<item name="android:navigationBarColor">#F7FAF8</item>
</style>
</resources>
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="camera" path="camera/" />
</paths>
@@ -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"))
}
}
@@ -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)
}
}
+5
View File
@@ -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
}
+4
View File
@@ -0,0 +1,4 @@
android.useAndroidX=true
android.nonTransitiveRClass=true
kotlin.code.style=official
org.gradle.jvmargs=-Xmx3072m -Dfile.encoding=UTF-8
+49
View File
@@ -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" }
Binary file not shown.
+7
View File
@@ -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
Vendored Executable
+141
View File
@@ -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" "$@"
Vendored
+93
View File
@@ -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%
+18
View File
@@ -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")