()
+
+ val datePattern = "(\\d{4}-\\d{2}-\\d{2})
".toRegex()
+ val rowPattern =
+ "]*>(\\d{2}:\\d{2}:\\d{2}) | \\s*]*>([^<]*) | \\s*]*>([^<]*) | "
+ .toRegex()
+
+ val dateMatches = datePattern.findAll(html)
+
+ for (dateMatch in dateMatches) {
+ val dateText = dateMatch.groupValues[1]
+
+ val startIndex = dateMatch.range.last
+ val nextDateMatch = datePattern.find(html, startIndex + 1)
+ val endIndex = nextDateMatch?.range?.first ?: html.length
+
+ val tableSection = html.substring(startIndex, endIndex)
+ val rowMatches = rowPattern.findAll(tableSection)
+
+ for (rowMatch in rowMatches) {
+ val timeText = rowMatch.groupValues[1]
+ val codeText = rowMatch.groupValues[2].trim()
+ val descriptionText = rowMatch.groupValues[3].trim()
+
+ if (timeText.isNotEmpty() && descriptionText.isNotEmpty()) {
+ try {
+ val dateTimeString = "$dateText $timeText"
+ val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
+ val dateTime = LocalDateTime.parse(dateTimeString, formatter)
+
+ var cleanDescription = descriptionText.replace(" ", " ").trim()
+ val locationMatch = "\\(([^)]+)\\)".toRegex().find(cleanDescription)
+ val location = locationMatch?.groupValues?.get(1) ?: ""
+
+ if (location.isNotEmpty()) {
+ cleanDescription = cleanDescription.replace("($location)", "").trim()
+ }
+
+ history.add(ParcelHistoryItem(cleanDescription, dateTime, location))
+ } catch (e: Exception) {
+ continue
+ }
+ }
+ }
+ }
+
+ history.sortByDescending { it.time }
+
+ if (history.isEmpty()) {
+ history.add(
+ ParcelHistoryItem(
+ "Csomag nyomon követése elindítva",
+ LocalDateTime.now(),
+ "Express One Hungary",
+ )
+ )
+ }
+
+ return Parcel(trackingId, history, currentStatus)
+ }
+}
diff --git a/app/src/main/java/dev/itsvic/parceltracker/api/IMileDeliveryService.kt b/app/src/main/java/dev/itsvic/parceltracker/api/IMileDeliveryService.kt
new file mode 100644
index 0000000..4feb4c3
--- /dev/null
+++ b/app/src/main/java/dev/itsvic/parceltracker/api/IMileDeliveryService.kt
@@ -0,0 +1,101 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+package dev.itsvic.parceltracker.api
+
+import com.squareup.moshi.JsonClass
+import dev.itsvic.parceltracker.R
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
+import retrofit2.Retrofit
+import retrofit2.http.GET
+import retrofit2.http.Query
+
+object IMileDeliveryService : DeliveryService {
+ override val nameResource: Int = R.string.service_imile
+ override val acceptsPostCode: Boolean = false
+ override val requiresPostCode: Boolean = false
+
+ private const val BASE_URL = "https://www.imile.com/"
+
+ private val retrofit =
+ Retrofit.Builder()
+ .baseUrl(BASE_URL)
+ .client(api_client)
+ .addConverterFactory(api_factory)
+ .build()
+
+ private val service = retrofit.create(API::class.java)
+
+ override suspend fun getParcel(trackingId: String, postCode: String?): Parcel {
+ val response = service.getParcel(trackingId)
+
+ if (response.status != "success" || response.resultObject == null) {
+ throw ParcelNonExistentException()
+ }
+
+ val resultObject = response.resultObject
+ val trackInfos = resultObject.trackInfos
+
+ if (trackInfos.isEmpty()) {
+ throw ParcelNonExistentException()
+ }
+
+ val history = trackInfos.map { trackInfo ->
+ ParcelHistoryItem(
+ description = trackInfo.content,
+ time = LocalDateTime.parse(trackInfo.time, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")),
+ location = trackInfo.operateStationName ?: ""
+ )
+ }
+
+ val currentStatus = mapTrackStageToStatus(trackInfos.firstOrNull()?.trackStage ?: 0)
+
+ return Parcel(
+ id = trackingId,
+ history = history,
+ currentStatus = currentStatus
+ )
+ }
+
+ private fun mapTrackStageToStatus(trackStage: Int): Status {
+ return when (trackStage) {
+ 1001 -> Status.Preadvice
+ 2004 -> Status.InWarehouse
+ 1002 -> Status.InTransit
+ 1003 -> Status.OutForDelivery
+ 2060 -> Status.Delivered
+ else -> logUnknownStatus("iMile", trackStage.toString())
+ }
+ }
+
+ private interface API {
+ @GET("saastms/mobileWeb/track/query")
+ suspend fun getParcel(@Query("waybillNo") waybillNo: String): IMileResponse
+ }
+
+ @JsonClass(generateAdapter = true)
+ data class IMileResponse(
+ val status: String,
+ val resultCode: String,
+ val resultObject: IMileResultObject?,
+ val message: String
+ )
+
+ @JsonClass(generateAdapter = true)
+ data class IMileResultObject(
+ val waybillNo: String,
+ val sendSite: String?,
+ val dispatchStation: String?,
+ val country: String?,
+ val trackInfos: List
+ )
+
+ @JsonClass(generateAdapter = true)
+ data class IMileTrackInfo(
+ val content: String,
+ val trackStage: Int,
+ val trackStageTx: String,
+ val time: String,
+ val operateStationName: String?,
+ val proofs: Any?
+ )
+}
diff --git a/app/src/main/java/dev/itsvic/parceltracker/ui/components/AboutDialog.kt b/app/src/main/java/dev/itsvic/parceltracker/ui/components/AboutDialog.kt
index deb0d9b..5e65b7f 100644
--- a/app/src/main/java/dev/itsvic/parceltracker/ui/components/AboutDialog.kt
+++ b/app/src/main/java/dev/itsvic/parceltracker/ui/components/AboutDialog.kt
@@ -4,11 +4,14 @@ package dev.itsvic.parceltracker.ui.components
import android.content.Context
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
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.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
@@ -16,6 +19,9 @@ import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -37,19 +43,32 @@ fun AboutDialog(onDismissRequest: () -> Unit) {
Dialog(onDismissRequest = onDismissRequest) {
Card(modifier = Modifier.fillMaxWidth().padding(16.dp), shape = RoundedCornerShape(16.dp)) {
Column(modifier = Modifier.padding(24.dp)) {
- Text(
- text = "Parcel",
- style = MaterialTheme.typography.titleLarge,
- modifier = Modifier.fillMaxWidth(),
- textAlign = TextAlign.Center,
- )
- Text(
- text = BuildConfig.VERSION_NAME,
- style = MaterialTheme.typography.titleSmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
+ Row(
modifier = Modifier.fillMaxWidth(),
- textAlign = TextAlign.Center,
- )
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.icon_foreground),
+ contentDescription = null,
+ modifier = Modifier.size(80.dp),
+ tint = Color.Unspecified
+ )
+ Spacer(Modifier.width(16.dp))
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = "Parcel",
+ style = MaterialTheme.typography.titleLarge,
+ )
+ Text(
+ text = BuildConfig.VERSION_NAME,
+ style = MaterialTheme.typography.titleSmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+ }
Spacer(Modifier.height(24.dp))
diff --git a/app/src/main/java/dev/itsvic/parceltracker/ui/components/BottomNavBar.kt b/app/src/main/java/dev/itsvic/parceltracker/ui/components/BottomNavBar.kt
new file mode 100644
index 0000000..4babfac
--- /dev/null
+++ b/app/src/main/java/dev/itsvic/parceltracker/ui/components/BottomNavBar.kt
@@ -0,0 +1,56 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+package dev.itsvic.parceltracker.ui.components
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Home
+import androidx.compose.material.icons.filled.Settings
+import androidx.compose.material3.Icon
+import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import dev.itsvic.parceltracker.R
+
+@Composable
+fun BottomNavBar(
+ currentRoute: String,
+ onNavigateToHome: () -> Unit,
+ onNavigateToAddParcel: () -> Unit,
+ onNavigateToSettings: () -> Unit,
+ onBackPressed: (() -> Unit)? = null,
+ showBackButton: Boolean = false,
+) {
+ NavigationBar {
+ if (showBackButton && onBackPressed != null) {
+ NavigationBarItem(
+ icon = { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.go_back)) },
+ label = { Text(stringResource(R.string.go_back)) },
+ selected = false,
+ onClick = onBackPressed,
+ )
+ }
+ NavigationBarItem(
+ icon = { Icon(Icons.Filled.Home, contentDescription = stringResource(R.string.home)) },
+ label = { Text(stringResource(R.string.home)) },
+ selected = currentRoute.contains("HomePage"),
+ onClick = onNavigateToHome,
+ )
+ NavigationBarItem(
+ icon = { Icon(Icons.Filled.Add, contentDescription = stringResource(R.string.add)) },
+ label = { Text(stringResource(R.string.add)) },
+ selected = currentRoute.contains("AddParcelPage"),
+ onClick = onNavigateToAddParcel,
+ )
+ NavigationBarItem(
+ icon = {
+ Icon(Icons.Filled.Settings, contentDescription = stringResource(R.string.settings))
+ },
+ label = { Text(stringResource(R.string.settings)) },
+ selected = currentRoute.contains("SettingsPage"),
+ onClick = onNavigateToSettings,
+ )
+ }
+}
diff --git a/app/src/main/java/dev/itsvic/parceltracker/ui/components/EditParcelDialog.kt b/app/src/main/java/dev/itsvic/parceltracker/ui/components/EditParcelDialog.kt
new file mode 100644
index 0000000..050b68b
--- /dev/null
+++ b/app/src/main/java/dev/itsvic/parceltracker/ui/components/EditParcelDialog.kt
@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+package dev.itsvic.parceltracker.ui.components
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.sizeIn
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import dev.itsvic.parceltracker.R
+import dev.itsvic.parceltracker.db.Parcel
+import dev.itsvic.parceltracker.ui.views.AddEditParcelContent
+
+@Composable
+fun EditParcelDialog(
+ parcel: Parcel,
+ onDismissRequest: () -> Unit,
+ onCompleted: (Parcel) -> Unit
+) {
+ Dialog(onDismissRequest = onDismissRequest) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .sizeIn(maxHeight = 700.dp, maxWidth = 500.dp)
+ .verticalScroll(rememberScrollState())
+ ) {
+ Text(
+ text = stringResource(R.string.edit_parcel),
+ style = MaterialTheme.typography.headlineMedium,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.fillMaxWidth(),
+ textAlign = TextAlign.Center,
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ AddEditParcelContent(
+ parcel = parcel,
+ onCompleted = onCompleted,
+ isDialog = true
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/dev/itsvic/parceltracker/ui/components/FloatingCollapsibleActionBar.kt b/app/src/main/java/dev/itsvic/parceltracker/ui/components/FloatingCollapsibleActionBar.kt
new file mode 100644
index 0000000..41eccd4
--- /dev/null
+++ b/app/src/main/java/dev/itsvic/parceltracker/ui/components/FloatingCollapsibleActionBar.kt
@@ -0,0 +1,217 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+package dev.itsvic.parceltracker.ui.components
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.shrinkVertically
+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.Row
+import androidx.compose.foundation.layout.Spacer
+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.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.icons.filled.KeyboardArrowDown
+import androidx.compose.material.icons.filled.KeyboardArrowUp
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+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.draw.clip
+import androidx.compose.ui.draw.rotate
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import dev.itsvic.parceltracker.R
+import dev.itsvic.parceltracker.api.Status
+
+@Composable
+fun FloatingCollapsibleActionBar(
+ status: Status?,
+ onEdit: () -> Unit,
+ onArchive: () -> Unit,
+ onDelete: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ var isExpanded by remember { mutableStateOf(false) }
+ var showDeleteDialog by remember { mutableStateOf(false) }
+
+ val rotationAngle by animateFloatAsState(
+ targetValue = if (isExpanded) 180f else 0f,
+ animationSpec = tween(300),
+ label = "rotation"
+ )
+
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ contentAlignment = Alignment.BottomCenter
+ ) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(16.dp),
+ elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surface
+ )
+ ) {
+ Column(
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable { isExpanded = !isExpanded }
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = stringResource(R.string.actions),
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ Icon(
+ imageVector = Icons.Default.KeyboardArrowDown,
+ contentDescription = if (isExpanded) stringResource(R.string.collapse) else stringResource(R.string.expand),
+ modifier = Modifier.rotate(rotationAngle),
+ tint = MaterialTheme.colorScheme.onSurface
+ )
+ }
+ AnimatedVisibility(
+ visible = isExpanded,
+ enter = expandVertically(
+ animationSpec = tween(300)
+ ) + fadeIn(animationSpec = tween(300)),
+ exit = shrinkVertically(
+ animationSpec = tween(300)
+ ) + fadeOut(animationSpec = tween(300))
+ ) {
+ Column(
+ modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
+ ) {
+
+ ActionButton(
+ icon = { Icon(Icons.Filled.Edit, contentDescription = stringResource(R.string.edit)) },
+ text = stringResource(R.string.edit),
+ onClick = onEdit
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ if (status == Status.Delivered) {
+ ActionButton(
+ icon = {
+ Icon(
+ painterResource(R.drawable.archive),
+ contentDescription = stringResource(R.string.archive)
+ )
+ },
+ text = stringResource(R.string.archive),
+ onClick = onArchive
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ }
+ ActionButton(
+ icon = { Icon(Icons.Filled.Delete, contentDescription = stringResource(R.string.delete)) },
+ text = stringResource(R.string.delete),
+ onClick = { showDeleteDialog = true },
+ isDestructive = true
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+ }
+ }
+ }
+ }
+ }
+ if (showDeleteDialog) {
+ AlertDialog(
+ onDismissRequest = { showDeleteDialog = false },
+ title = { Text(stringResource(R.string.delete)) },
+ text = { Text(stringResource(R.string.delete_confirmation)) },
+ confirmButton = {
+ TextButton(
+ onClick = {
+ showDeleteDialog = false
+ onDelete()
+ }
+ ) {
+ Text(stringResource(R.string.delete))
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = { showDeleteDialog = false }) {
+ Text(stringResource(R.string.cancel))
+ }
+ },
+ )
+ }
+}
+
+@Composable
+fun ActionButton(
+ icon: @Composable () -> Unit,
+ text: String,
+ onClick: () -> Unit,
+ isDestructive: Boolean = false,
+ modifier: Modifier = Modifier
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(12.dp))
+ .clickable { onClick() }
+ .background(
+ if (isDestructive)
+ MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.1f)
+ else
+ MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.1f)
+ )
+ .padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Box(
+ modifier = Modifier.size(24.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ icon()
+ }
+ Spacer(modifier = Modifier.width(16.dp))
+ Text(
+ text = text,
+ style = MaterialTheme.typography.bodyLarge,
+ color = if (isDestructive)
+ MaterialTheme.colorScheme.error
+ else
+ MaterialTheme.colorScheme.onSurface
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/dev/itsvic/parceltracker/ui/components/ParcelActionBar.kt b/app/src/main/java/dev/itsvic/parceltracker/ui/components/ParcelActionBar.kt
new file mode 100644
index 0000000..90f7ce7
--- /dev/null
+++ b/app/src/main/java/dev/itsvic/parceltracker/ui/components/ParcelActionBar.kt
@@ -0,0 +1,93 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+package dev.itsvic.parceltracker.ui.components
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Icon
+import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import dev.itsvic.parceltracker.R
+import dev.itsvic.parceltracker.api.Status
+
+@Composable
+fun ParcelActionBar(
+ status: Status?,
+ onEdit: () -> Unit,
+ onArchive: () -> Unit,
+ onDelete: () -> Unit,
+ onBackPressed: (() -> Unit)? = null,
+) {
+ var showDeleteDialog by remember { mutableStateOf(false) }
+
+ NavigationBar {
+ if (onBackPressed != null) {
+ NavigationBarItem(
+ icon = { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.go_back)) },
+ label = { Text(stringResource(R.string.go_back)) },
+ selected = false,
+ onClick = onBackPressed,
+ )
+ }
+
+ NavigationBarItem(
+ icon = { Icon(Icons.Filled.Edit, contentDescription = stringResource(R.string.edit)) },
+ label = { Text(stringResource(R.string.edit)) },
+ selected = false,
+ onClick = onEdit,
+ )
+
+ if (status == Status.Delivered) {
+ NavigationBarItem(
+ icon = {
+ Icon(
+ painterResource(R.drawable.archive),
+ contentDescription = stringResource(R.string.archive),
+ )
+ },
+ label = { Text(stringResource(R.string.archive)) },
+ selected = false,
+ onClick = onArchive,
+ )
+ }
+
+ NavigationBarItem(
+ icon = { Icon(Icons.Filled.Delete, contentDescription = stringResource(R.string.delete)) },
+ label = { Text(stringResource(R.string.delete)) },
+ selected = false,
+ onClick = { showDeleteDialog = true },
+ )
+ }
+
+ if (showDeleteDialog) {
+ AlertDialog(
+ onDismissRequest = { showDeleteDialog = false },
+ title = { Text(stringResource(R.string.delete)) },
+ text = { Text(stringResource(R.string.delete_confirmation)) },
+ confirmButton = {
+ TextButton(
+ onClick = {
+ showDeleteDialog = false
+ onDelete()
+ }
+ ) {
+ Text(stringResource(R.string.delete))
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = { showDeleteDialog = false }) { Text(stringResource(R.string.cancel)) }
+ },
+ )
+ }
+}
diff --git a/app/src/main/java/dev/itsvic/parceltracker/ui/components/ParcelCard.kt b/app/src/main/java/dev/itsvic/parceltracker/ui/components/ParcelCard.kt
new file mode 100644
index 0000000..5278e91
--- /dev/null
+++ b/app/src/main/java/dev/itsvic/parceltracker/ui/components/ParcelCard.kt
@@ -0,0 +1,140 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+package dev.itsvic.parceltracker.ui.components
+
+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.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import dev.itsvic.parceltracker.R
+import dev.itsvic.parceltracker.api.Service
+import dev.itsvic.parceltracker.api.Status
+import dev.itsvic.parceltracker.api.getDeliveryServiceName
+import dev.itsvic.parceltracker.db.Parcel
+import dev.itsvic.parceltracker.ui.theme.ParcelTrackerTheme
+
+@Composable
+fun ParcelCard(parcel: Parcel, status: Status?, onClick: () -> Unit) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 8.dp)
+ .clickable(onClick = onClick),
+ elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
+ shape = RoundedCornerShape(12.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainer
+ )
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ if (status != null) {
+ Box(
+ modifier = Modifier
+ .size(48.dp)
+ .clip(CircleShape)
+ .background(MaterialTheme.colorScheme.primaryContainer),
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(
+ painter = painterResource(
+ when (status) {
+ Status.Preadvice -> R.drawable.outline_other_admission_24
+ Status.LockerboxAcceptedParcel -> R.drawable.outline_deployed_code_update_24
+ Status.PickedUpByCourier -> R.drawable.outline_deployed_code_account_24
+ Status.InTransit -> R.drawable.outline_local_shipping_24
+ Status.InWarehouse -> R.drawable.outline_warehouse_24
+ Status.Customs -> R.drawable.outline_search_24
+ Status.OutForDelivery -> R.drawable.outline_delivery_truck_speed_24
+ Status.DeliveryFailure -> R.drawable.outline_error_24
+ Status.PickupTimeEndingSoon -> R.drawable.outline_notifications_active_24
+ Status.AwaitingPickup -> R.drawable.outline_pin_drop_24
+ Status.Delivered,
+ Status.PickedUp -> R.drawable.outline_check_24
+ Status.DeliveredToNeighbor -> R.drawable.outline_holiday_village_24
+ Status.DeliveredToASafePlace -> R.drawable.outline_roofing_24
+ Status.DroppedAtCustomerService -> R.drawable.outline_support_agent_24
+ Status.ReturningToSender -> R.drawable.outline_arrow_top_left_24
+ Status.ReturnedToSender -> R.drawable.outline_arrow_top_left_24
+ Status.Delayed -> R.drawable.outline_deployed_code_history_24
+ Status.Damaged -> R.drawable.outline_deployed_code_alert_24
+ Status.Destroyed -> R.drawable.outline_destruction_24
+ else -> R.drawable.outline_question_mark_24
+ }
+ ),
+ contentDescription = stringResource(status.nameResource),
+ tint = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.size(24.dp)
+ )
+ }
+ }
+
+ Column(
+ modifier = Modifier.weight(1f),
+ verticalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ Text(
+ text = parcel.humanName,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Medium,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+
+ Text(
+ text = "${stringResource(getDeliveryServiceName(parcel.service)!!)}: ${parcel.parcelId}",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+
+ if (status != null) {
+ Text(
+ text = stringResource(status.nameResource),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.primary,
+ fontWeight = FontWeight.Medium
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+@PreviewLightDark
+fun ParcelCardPreview() {
+ ParcelTrackerTheme {
+ Box(modifier = Modifier.background(color = MaterialTheme.colorScheme.background)) {
+ ParcelCard(
+ Parcel(0, "My precious package", "EXMPL0001", null, Service.EXAMPLE),
+ status = Status.InTransit,
+ onClick = {},
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/dev/itsvic/parceltracker/ui/components/ParcelRow.kt b/app/src/main/java/dev/itsvic/parceltracker/ui/components/ParcelRow.kt
index 45c3289..16336c7 100644
--- a/app/src/main/java/dev/itsvic/parceltracker/ui/components/ParcelRow.kt
+++ b/app/src/main/java/dev/itsvic/parceltracker/ui/components/ParcelRow.kt
@@ -31,58 +31,68 @@ import dev.itsvic.parceltracker.db.Parcel
import dev.itsvic.parceltracker.ui.theme.ParcelTrackerTheme
@Composable
-fun ParcelRow(parcel: Parcel, status: Status?, onClick: () -> Unit) {
+fun ParcelRow(parcel: Parcel, status: Status?, isSelected: Boolean = false, onClick: () -> Unit) {
Row(
- modifier = Modifier.clickable(onClick = onClick).fillMaxWidth().padding(16.dp, 12.dp),
- horizontalArrangement = Arrangement.spacedBy(16.dp),
- verticalAlignment = Alignment.CenterVertically) {
- if (status != null)
- Box(
- modifier =
- Modifier.size(40.dp)
- .clip(CircleShape)
- .background(MaterialTheme.colorScheme.primaryContainer),
- contentAlignment = Alignment.Center) {
- Icon(
- painterResource(
- when (status) {
- Status.Preadvice -> R.drawable.outline_other_admission_24
- Status.LockerboxAcceptedParcel ->
- R.drawable.outline_deployed_code_update_24
- Status.PickedUpByCourier -> R.drawable.outline_deployed_code_account_24
- Status.InTransit -> R.drawable.outline_local_shipping_24
- Status.InWarehouse -> R.drawable.outline_warehouse_24
- Status.Customs -> R.drawable.outline_search_24
- Status.OutForDelivery -> R.drawable.outline_delivery_truck_speed_24
- Status.DeliveryFailure -> R.drawable.outline_error_24
- Status.PickupTimeEndingSoon ->
- R.drawable.outline_notifications_active_24
- Status.AwaitingPickup -> R.drawable.outline_pin_drop_24
- Status.Delivered,
- Status.PickedUp -> R.drawable.outline_check_24
- Status.DeliveredToNeighbor -> R.drawable.outline_holiday_village_24
- Status.DeliveredToASafePlace -> R.drawable.outline_roofing_24
- Status.DroppedAtCustomerService -> R.drawable.outline_support_agent_24
- Status.ReturningToSender -> R.drawable.outline_arrow_top_left_24
- Status.ReturnedToSender -> R.drawable.outline_arrow_top_left_24
- Status.Delayed -> R.drawable.outline_deployed_code_history_24
- Status.Damaged -> R.drawable.outline_deployed_code_alert_24
- Status.Destroyed -> R.drawable.outline_destruction_24
- else -> R.drawable.outline_question_mark_24
- }),
- stringResource(status.nameResource),
- tint = MaterialTheme.colorScheme.primary)
- }
+ modifier =
+ Modifier.clickable(onClick = onClick)
+ .fillMaxWidth()
+ .background(
+ if (isSelected) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
+ else MaterialTheme.colorScheme.surface
+ )
+ .padding(16.dp, 12.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ if (status != null)
+ Box(
+ modifier =
+ Modifier.size(40.dp)
+ .clip(CircleShape)
+ .background(MaterialTheme.colorScheme.primaryContainer),
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(
+ painterResource(
+ when (status) {
+ Status.Preadvice -> R.drawable.outline_other_admission_24
+ Status.LockerboxAcceptedParcel -> R.drawable.outline_deployed_code_update_24
+ Status.PickedUpByCourier -> R.drawable.outline_deployed_code_account_24
+ Status.InTransit -> R.drawable.outline_local_shipping_24
+ Status.InWarehouse -> R.drawable.outline_warehouse_24
+ Status.Customs -> R.drawable.outline_search_24
+ Status.OutForDelivery -> R.drawable.outline_delivery_truck_speed_24
+ Status.DeliveryFailure -> R.drawable.outline_error_24
+ Status.PickupTimeEndingSoon -> R.drawable.outline_notifications_active_24
+ Status.AwaitingPickup -> R.drawable.outline_pin_drop_24
+ Status.Delivered,
+ Status.PickedUp -> R.drawable.outline_check_24
+ Status.DeliveredToNeighbor -> R.drawable.outline_holiday_village_24
+ Status.DeliveredToASafePlace -> R.drawable.outline_roofing_24
+ Status.DroppedAtCustomerService -> R.drawable.outline_support_agent_24
+ Status.ReturningToSender -> R.drawable.outline_arrow_top_left_24
+ Status.ReturnedToSender -> R.drawable.outline_arrow_top_left_24
+ Status.Delayed -> R.drawable.outline_deployed_code_history_24
+ Status.Damaged -> R.drawable.outline_deployed_code_alert_24
+ Status.Destroyed -> R.drawable.outline_destruction_24
+ else -> R.drawable.outline_question_mark_24
+ }
+ ),
+ stringResource(status.nameResource),
+ tint = MaterialTheme.colorScheme.primary,
+ )
+ }
- Column {
- Text(parcel.humanName, color = MaterialTheme.colorScheme.onBackground)
+ Column {
+ Text(parcel.humanName, color = MaterialTheme.colorScheme.onBackground)
- Text(
- "${stringResource(getDeliveryServiceName(parcel.service)!!)}: ${parcel.parcelId}",
- fontSize = 12.sp,
- color = MaterialTheme.colorScheme.onSurfaceVariant)
- }
- }
+ Text(
+ "${stringResource(getDeliveryServiceName(parcel.service)!!)}: ${parcel.parcelId}",
+ fontSize = 12.sp,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+ }
}
@Composable
@@ -91,9 +101,9 @@ fun ParcelRowPreview() {
ParcelTrackerTheme {
Box(modifier = Modifier.background(color = MaterialTheme.colorScheme.background)) {
ParcelRow(
- Parcel(0, "My precious package", "EXMPL0001", null, Service.EXAMPLE),
- status = Status.InTransit,
- onClick = {},
+ Parcel(0, "My precious package", "EXMPL0001", null, Service.EXAMPLE),
+ status = Status.InTransit,
+ onClick = {},
)
}
}
diff --git a/app/src/main/java/dev/itsvic/parceltracker/ui/views/AdaptiveView.kt b/app/src/main/java/dev/itsvic/parceltracker/ui/views/AdaptiveView.kt
new file mode 100644
index 0000000..0ac6eee
--- /dev/null
+++ b/app/src/main/java/dev/itsvic/parceltracker/ui/views/AdaptiveView.kt
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+package dev.itsvic.parceltracker.ui.views
+
+import androidx.compose.material3.windowsizeclass.WindowSizeClass
+import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import dev.itsvic.parceltracker.api.Parcel as APIParcel
+import dev.itsvic.parceltracker.db.Parcel
+import dev.itsvic.parceltracker.db.ParcelWithStatus
+
+@Composable
+fun AdaptiveParcelApp(
+ windowSizeClass: WindowSizeClass,
+ parcels: List,
+ selectedParcel: Parcel?,
+ apiParcel: APIParcel?,
+ isLoading: Boolean,
+ onNavigateToParcel: (Parcel) -> Unit,
+ onNavigateToAddParcel: () -> Unit,
+ onNavigateToSettings: () -> Unit,
+ onEditParcel: (Parcel) -> Unit,
+ onDeleteParcel: (Parcel) -> Unit,
+ onArchiveParcel: (Parcel) -> Unit,
+ onArchivePromptDismissal: (Parcel) -> Unit,
+ settingsContent: @Composable () -> Unit,
+ addParcelContent: @Composable () -> Unit,
+ homeContent: @Composable () -> Unit,
+) {
+ val isTablet = windowSizeClass.widthSizeClass >= WindowWidthSizeClass.Medium
+
+ if (isTablet) {
+ var currentNavigationItem by remember { mutableStateOf(TabletNavigationItem.HOME) }
+
+ TabletView(
+ parcels = parcels,
+ selectedParcel = selectedParcel,
+ apiParcel = apiParcel,
+ isLoading = isLoading,
+ currentNavigationItem = currentNavigationItem,
+ onNavigateToItem = { currentNavigationItem = it },
+ onNavigateToParcel = onNavigateToParcel,
+ onNavigateToAddParcel = onNavigateToAddParcel,
+ onNavigateToSettings = onNavigateToSettings,
+ onEditParcel = onEditParcel,
+ onDeleteParcel = onDeleteParcel,
+ onArchiveParcel = onArchiveParcel,
+ onArchivePromptDismissal = onArchivePromptDismissal,
+ settingsContent = settingsContent,
+ addParcelContent = addParcelContent,
+ )
+ } else {
+ homeContent()
+ }
+}
diff --git a/app/src/main/java/dev/itsvic/parceltracker/ui/views/AddEditParcelView.kt b/app/src/main/java/dev/itsvic/parceltracker/ui/views/AddEditParcelView.kt
index 43ebadb..a6ba415 100644
--- a/app/src/main/java/dev/itsvic/parceltracker/ui/views/AddEditParcelView.kt
+++ b/app/src/main/java/dev/itsvic/parceltracker/ui/views/AddEditParcelView.kt
@@ -2,19 +2,29 @@
package dev.itsvic.parceltracker.ui.views
import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+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.layout.sizeIn
+import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Button
+import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuAnchorType
import androidx.compose.material3.ExposedDropdownMenuBox
@@ -24,10 +34,15 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Surface
import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontWeight
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -35,29 +50,73 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalClipboardManager
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import dev.itsvic.parceltracker.CLIPBOARD_PASTE_ENABLED
+import dev.itsvic.parceltracker.PREFERRED_REGION
import dev.itsvic.parceltracker.R
import dev.itsvic.parceltracker.api.Service
import dev.itsvic.parceltracker.api.getDeliveryService
import dev.itsvic.parceltracker.api.getDeliveryServiceName
import dev.itsvic.parceltracker.api.serviceOptions
+import dev.itsvic.parceltracker.dataStore
import dev.itsvic.parceltracker.db.Parcel
import dev.itsvic.parceltracker.ui.theme.ParcelTrackerTheme
+import kotlinx.coroutines.flow.map
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun AddEditParcelView(
- parcel: Parcel?,
- onBackPressed: () -> Unit,
- onCompleted: (Parcel) -> Unit,
+fun AddEditParcelView(parcel: Parcel?, onBackPressed: () -> Unit, onCompleted: (Parcel) -> Unit) {
+ val isEdit = parcel != null
+ val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
+
+ Scaffold(
+ topBar = {
+ LargeTopAppBar(
+ title = {
+ Text(stringResource(if (isEdit) R.string.edit_parcel else R.string.add_a_parcel))
+ },
+ scrollBehavior = scrollBehavior,
+ )
+ },
+ modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+ ) { innerPadding ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ .padding(horizontal = 16.dp)
+ .verticalScroll(rememberScrollState()),
+ ) {
+ AddEditParcelContent(
+ parcel = parcel,
+ onCompleted = onCompleted,
+ isDialog = false
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AddEditParcelContent(
+ parcel: Parcel?,
+ onCompleted: (Parcel) -> Unit,
+ isDialog: Boolean = false
) {
val isEdit = parcel != null
+ val context = LocalContext.current
+ val clipboardManager = LocalClipboardManager.current
+ val clipboardPasteEnabled by
+ context.dataStore.data.map { it[CLIPBOARD_PASTE_ENABLED] == true }.collectAsState(false)
+ val preferredRegion by
+ context.dataStore.data.map { it[PREFERRED_REGION] ?: "" }.collectAsState("")
var humanName by remember { mutableStateOf(parcel?.humanName ?: "") }
- var nameError by remember { mutableStateOf(false) }
var trackingId by remember { mutableStateOf(parcel?.parcelId ?: "") }
var idError by remember { mutableStateOf(false) }
var specifyPostalCode by remember { mutableStateOf(parcel?.postalCode != null) }
@@ -69,17 +128,11 @@ fun AddEditParcelView(
val backend = if (service != Service.UNDEFINED) getDeliveryService(service) else null
fun validateInputs(): Boolean {
- // reset error states first
- nameError = false
idError = false
serviceError = false
postalCodeError = false
var success = true
- if (humanName.isBlank()) {
- success = false
- nameError = true
- }
if (trackingId.isBlank()) {
success = false
idError = true
@@ -88,8 +141,10 @@ fun AddEditParcelView(
success = false
serviceError = true
}
- if (((backend?.acceptsPostCode == true && specifyPostalCode) ||
- (backend?.requiresPostCode == true)) && postalCode.isBlank()) {
+ if (
+ ((backend?.acceptsPostCode == true && specifyPostalCode) ||
+ (backend?.requiresPostCode == true)) && postalCode.isBlank()
+ ) {
success = false
postalCodeError = true
}
@@ -102,176 +157,436 @@ fun AddEditParcelView(
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val sortedServiceOptions =
- serviceOptions.sortedBy { getDeliveryService(it)?.acceptsFormat(trackingId)?.not() }
+ serviceOptions.sortedWith(
+ compareBy {
+ val isPreferredRegion =
+ when (preferredRegion) {
+ "international" ->
+ it in listOf(Service.CAINIAO, Service.DHL, Service.GLS, Service.UPS, Service.FPX)
+ "north_america" -> it == Service.UNIUNI
+ "europe" ->
+ it in
+ listOf(
+ Service.BELPOST,
+ Service.SAMEDAY_BG,
+ Service.DPD_UK,
+ Service.EVRI,
+ Service.AN_POST,
+ Service.ALLEGRO_ONEBOX,
+ Service.INPOST,
+ Service.ORLEN_PACZKA,
+ Service.POLISH_POST,
+ Service.GLS_HUNGARY,
+ Service.MAGYAR_POSTA,
+ Service.SAMEDAY_HU,
+ Service.IMILE,
+ Service.DPD_GER,
+ Service.HERMES,
+ Service.POSTE_ITALIANE,
+ Service.SAMEDAY_RO,
+ Service.POSTNORD,
+ Service.NOVA_POSHTA,
+ Service.UKRPOSHTA,
+ Service.PACKETA,
+ Service.EXPRESS_ONE,
+ )
+ "asia" -> it in listOf(Service.EKART, Service.SPX_TH)
+ "belarus" -> it == Service.BELPOST
+ "bulgaria" -> it == Service.SAMEDAY_BG
+ "uk" -> it in listOf(Service.DPD_UK, Service.EVRI)
+ "ireland" -> it == Service.AN_POST
+ "poland" ->
+ it in
+ listOf(
+ Service.ALLEGRO_ONEBOX,
+ Service.INPOST,
+ Service.ORLEN_PACZKA,
+ Service.POLISH_POST,
+ )
+ "hungary" ->
+ it in
+ listOf(
+ Service.GLS_HUNGARY,
+ Service.MAGYAR_POSTA,
+ Service.SAMEDAY_HU,
+ Service.EXPRESS_ONE,
+ Service.IMILE,
+ )
+ "germany" -> it in listOf(Service.DPD_GER, Service.HERMES)
+ "italy" -> it == Service.POSTE_ITALIANE
+ "romania" -> it == Service.SAMEDAY_RO
+ "scandinavia" -> it == Service.POSTNORD
+ "ukraine" -> it in listOf(Service.NOVA_POSHTA, Service.UKRPOSHTA)
+ "india" -> it == Service.EKART
+ "thailand" -> it == Service.SPX_TH
+ else -> false
+ }
+ if (isPreferredRegion) 0 else 1
+ }
+ .thenBy {
+ when (it) {
+ Service.CAINIAO,
+ Service.DHL,
+ Service.GLS,
+ Service.UPS,
+ Service.FPX -> 0
+ Service.UNIUNI -> 1
+ Service.BELPOST,
+ Service.SAMEDAY_BG,
+ Service.PACKETA,
+ Service.DPD_UK,
+ Service.EVRI,
+ Service.AN_POST,
+ Service.ALLEGRO_ONEBOX,
+ Service.INPOST,
+ Service.ORLEN_PACZKA,
+ Service.POLISH_POST,
+ Service.GLS_HUNGARY,
+ Service.MAGYAR_POSTA,
+ Service.SAMEDAY_HU,
+ Service.EXPRESS_ONE,
+ Service.DPD_GER,
+ Service.HERMES,
+ Service.POSTE_ITALIANE,
+ Service.SAMEDAY_RO,
+ Service.POSTNORD,
+ Service.NOVA_POSHTA,
+ Service.EKART,
+ Service.SPX_TH,
+ Service.IMILE,
+ Service.UKRPOSHTA -> 2
+ else -> 4
+ } as Comparable<*>?
+ }
+ .thenBy {
+ when (it) {
+ Service.BELPOST -> "A_Belarus"
+ Service.SAMEDAY_BG -> "B_Bulgaria"
+ Service.PACKETA -> "C_Europe"
+ Service.DPD_UK,
+ Service.EVRI -> "D_UK"
+ Service.AN_POST -> "E_Ireland"
+ Service.ALLEGRO_ONEBOX,
+ Service.INPOST,
+ Service.ORLEN_PACZKA,
+ Service.POLISH_POST -> "F_Poland"
+ Service.GLS_HUNGARY,
+ Service.MAGYAR_POSTA,
+ Service.SAMEDAY_HU,
+ Service.EXPRESS_ONE,
+ Service.IMILE -> "G_Hungary"
+ Service.DPD_GER,
+ Service.HERMES -> "H_Germany"
+ Service.POSTE_ITALIANE -> "I_Italy"
+ Service.SAMEDAY_RO -> "J_Romania"
+ Service.POSTNORD -> "K_Scandinavia"
+ Service.NOVA_POSHTA,
+ Service.UKRPOSHTA -> "L_Ukraine"
+ else -> it.name
+ }
+ }
+ .thenBy {
+ if (trackingId.isNotBlank()) {
+ val backend = getDeliveryService(it)
+ if (backend?.acceptsFormat(trackingId) == true) 0 else 1
+ } else {
+ 0
+ }
+ }
+ )
- Scaffold(
- topBar = {
- TopAppBar(
- title = {
- Text(stringResource(if (isEdit) R.string.edit_parcel else R.string.add_a_parcel))
- },
- navigationIcon = {
- IconButton(onClick = onBackPressed) {
- Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.go_back))
- }
- },
- scrollBehavior = scrollBehavior,
- )
- },
- modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) { innerPadding ->
- Column(
- modifier =
- Modifier.padding(innerPadding).fillMaxWidth().verticalScroll(rememberScrollState()),
- horizontalAlignment = Alignment.CenterHorizontally) {
- Column(
- modifier =
- Modifier.padding(horizontal = 16.dp).sizeIn(maxWidth = 488.dp).fillMaxWidth(),
- verticalArrangement = Arrangement.spacedBy(8.dp),
- ) {
- OutlinedTextField(
- value = humanName,
- onValueChange = {
- humanName = it
- nameError = false
- },
- singleLine = true,
- label = { Text(stringResource(R.string.parcel_name)) },
- modifier = Modifier.fillMaxWidth(),
- isError = nameError,
- supportingText = {
- if (nameError) Text(stringResource(R.string.human_name_error_text))
- })
+ Column(
+ modifier = Modifier
+ .padding(horizontal = if (isDialog) 0.dp else 16.dp)
+ .sizeIn(maxWidth = 600.dp)
+ .fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ if (!isDialog) {
+ Spacer(modifier = Modifier.height(8.dp))
+ }
+ ElevatedCard(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.elevatedCardElevation(defaultElevation = 6.dp)
+ ) {
+ Column(
+ modifier = Modifier.padding(20.dp),
+ verticalArrangement = Arrangement.spacedBy(20.dp)
+ ) {
+ OutlinedTextField(
+ value = humanName,
+ onValueChange = { humanName = it },
+ singleLine = true,
+ label = { Text(stringResource(R.string.parcel_name)) },
+ leadingIcon = {
+ Icon(
+ painter = painterResource(R.drawable.ic_label),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ },
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(12.dp)
+ )
+ OutlinedTextField(
+ value = trackingId,
+ onValueChange = {
+ trackingId = it
+ idError = false
+ },
+ singleLine = true,
+ label = { Text(stringResource(R.string.tracking_id)) },
+ leadingIcon = {
+ Icon(
+ painter = painterResource(R.drawable.package_2),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ },
+ modifier = Modifier.fillMaxWidth(),
+ isError = idError,
+ trailingIcon = {
+ if (clipboardPasteEnabled) {
+ IconButton(
+ onClick = {
+ clipboardManager.getText()?.text?.let { clipboardText ->
+ trackingId = clipboardText
+ idError = false
+ }
+ }
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_contentpaste),
+ contentDescription = stringResource(R.string.clipboard_paste),
+ modifier = Modifier.size(20.dp),
+ )
+ }
+ }
+ },
+ supportingText = { if (idError) Text(stringResource(R.string.tracking_id_error_text)) },
+ shape = RoundedCornerShape(12.dp)
+ )
+ ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = it }) {
OutlinedTextField(
- value = trackingId,
- onValueChange = {
- trackingId = it
- idError = false
- },
- singleLine = true,
- label = { Text(stringResource(R.string.tracking_id)) },
- modifier = Modifier.fillMaxWidth(),
- isError = idError,
- supportingText = {
- if (idError) Text(stringResource(R.string.tracking_id_error_text))
- })
+ value =
+ if (service == Service.UNDEFINED) ""
+ else stringResource(getDeliveryServiceName(service)!!),
+ onValueChange = {},
+ modifier =
+ Modifier.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable).fillMaxWidth(),
+ readOnly = true,
+ label = { Text(stringResource(R.string.delivery_service)) },
+ leadingIcon = {
+ Icon(
+ painter = painterResource(R.drawable.outline_local_shipping_24),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ },
+ trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) },
+ colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
+ isError = serviceError,
+ supportingText = { if (serviceError) Text(stringResource(R.string.service_error_text)) },
+ shape = RoundedCornerShape(12.dp)
+ )
- // Service dropdown
- ExposedDropdownMenuBox(
- expanded = expanded,
- onExpandedChange = { expanded = it },
+ ExposedDropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false },
+ modifier = Modifier
+ .background(
+ MaterialTheme.colorScheme.surface,
+ RoundedCornerShape(12.dp)
+ )
+ .border(
+ 1.dp,
+ MaterialTheme.colorScheme.outline.copy(alpha = 0.12f),
+ RoundedCornerShape(12.dp)
+ )
) {
- OutlinedTextField(
- value =
- if (service == Service.UNDEFINED) ""
- else stringResource(getDeliveryServiceName(service)!!),
- onValueChange = {},
- modifier =
- Modifier.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
- .fillMaxWidth(),
- readOnly = true,
- label = { Text(stringResource(R.string.delivery_service)) },
- trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) },
- colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
- isError = serviceError,
- supportingText = {
- if (serviceError) Text(stringResource(R.string.service_error_text))
- })
-
- ExposedDropdownMenu(
- expanded = expanded, onDismissRequest = { expanded = false }) {
- sortedServiceOptions.forEach { option ->
- DropdownMenuItem(
- text = { Text(stringResource(getDeliveryServiceName(option)!!)) },
- onClick = {
- service = option
- expanded = false
- serviceError = false
- },
- contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
- )
- }
+ var currentCategory = ""
+ sortedServiceOptions.forEach { option ->
+ val category =
+ when (option) {
+ Service.CAINIAO,
+ Service.DHL,
+ Service.GLS,
+ Service.UPS,
+ Service.FPX -> stringResource(R.string.category_international)
+ Service.UNIUNI -> stringResource(R.string.category_north_america)
+ Service.BELPOST -> stringResource(R.string.category_europe_belarus)
+ Service.SAMEDAY_BG -> stringResource(R.string.category_europe_bulgaria)
+ Service.PACKETA -> stringResource(R.string.category_europe_czech)
+ Service.DPD_UK,
+ Service.EVRI -> stringResource(R.string.category_europe_uk)
+ Service.AN_POST -> stringResource(R.string.category_europe_ireland)
+ Service.ALLEGRO_ONEBOX,
+ Service.INPOST,
+ Service.ORLEN_PACZKA,
+ Service.POLISH_POST -> stringResource(R.string.category_europe_poland)
+ Service.GLS_HUNGARY -> stringResource(R.string.category_europe_hungary)
+ Service.MAGYAR_POSTA -> stringResource(R.string.category_europe_hungary)
+ Service.SAMEDAY_HU -> stringResource(R.string.category_europe_hungary)
+ Service.DPD_GER,
+ Service.HERMES -> stringResource(R.string.category_europe_germany)
+ Service.POSTE_ITALIANE -> stringResource(R.string.category_europe_italy)
+ Service.SAMEDAY_RO -> stringResource(R.string.category_europe_romania)
+ Service.POSTNORD -> stringResource(R.string.category_europe_scandinavia)
+ Service.NOVA_POSHTA,
+ Service.UKRPOSHTA -> stringResource(R.string.category_europe_ukraine)
+ Service.EKART -> stringResource(R.string.category_asia_india)
+ Service.SPX_TH -> stringResource(R.string.category_asia_thailand)
+ Service.IMILE -> stringResource(R.string.category_europe_hungary)
+ Service.EXPRESS_ONE -> stringResource(R.string.category_europe_hungary)
+ else -> stringResource(R.string.category_other)
}
- }
- AnimatedVisibility(backend?.acceptsPostCode == true && !backend.requiresPostCode) {
- Row(
+ if (category != currentCategory) {
+ currentCategory = category
+ DropdownMenuItem(
+ text = {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(vertical = 4.dp)
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.outline_local_shipping_24),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.size(16.dp)
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(
+ text = category,
+ style = MaterialTheme.typography.labelMedium,
+ color = MaterialTheme.colorScheme.primary,
+ fontWeight = FontWeight.SemiBold
+ )
+ }
+ },
+ onClick = {},
+ enabled = false,
+ modifier = Modifier.background(
+ MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.1f)
+ )
+ )
+ }
+
+ DropdownMenuItem(
+ text = {
+ Text(
+ text = " " + stringResource(getDeliveryServiceName(option)!!),
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.padding(start = 8.dp)
+ )
+ },
+ onClick = {
+ service = option
+ expanded = false
+ serviceError = false
+ },
+ modifier = Modifier
+ .background(
+ if (service == option) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.2f)
+ else Color.Transparent
+ )
+ )
+ }
+ }
+ }
+
+ AnimatedVisibility(
+ backend?.acceptsPostCode == true || backend?.requiresPostCode == true
+ ) {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ AnimatedVisibility(backend?.acceptsPostCode == true && !backend.requiresPostCode) {
+ Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
- modifier = Modifier.fillMaxWidth()) {
- Column(modifier = Modifier.fillMaxWidth(0.8f)) {
- Text(stringResource(R.string.specify_a_postal_code))
- Text(
- stringResource(R.string.specify_postal_code_flavor_text),
- fontSize = 14.sp,
- lineHeight = 21.sp,
- color = MaterialTheme.colorScheme.onSurfaceVariant)
- }
- Checkbox(
- checked = specifyPostalCode,
- onCheckedChange = { specifyPostalCode = it },
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ Column(modifier = Modifier.fillMaxWidth(0.8f)) {
+ Text(stringResource(R.string.specify_a_postal_code))
+ Text(
+ stringResource(R.string.specify_postal_code_flavor_text),
+ fontSize = 14.sp,
+ lineHeight = 21.sp,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
- }
-
- AnimatedVisibility(
- backend?.requiresPostCode == true ||
- (backend?.requiresPostCode == false &&
- backend.acceptsPostCode &&
- specifyPostalCode)) {
- OutlinedTextField(
- value = postalCode,
- onValueChange = {
- postalCode = it
- postalCodeError = false
- },
- singleLine = true,
- label = { Text(stringResource(R.string.postal_code)) },
- modifier = Modifier.fillMaxWidth(),
- isError = postalCodeError,
- supportingText = {
- if (postalCodeError)
- Text(stringResource(R.string.postal_code_error_text))
- })
+ Checkbox(checked = specifyPostalCode, onCheckedChange = { specifyPostalCode = it })
}
+ }
- Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()) {
- Button(
- onClick = {
- val isOk = validateInputs()
- if (isOk) {
- // data valid, pass it along
- onCompleted(
- Parcel(
- id = parcel?.id ?: 0,
- humanName = humanName,
- parcelId = trackingId,
- service = service,
- postalCode =
- if (backend?.requiresPostCode == true ||
- (backend?.acceptsPostCode == true && specifyPostalCode))
- postalCode
- else null))
- }
- }) {
- Text(stringResource(if (isEdit) R.string.save else R.string.add_parcel))
- }
+ AnimatedVisibility(
+ backend?.requiresPostCode == true ||
+ (backend?.requiresPostCode == false && backend.acceptsPostCode && specifyPostalCode)
+ ) {
+ OutlinedTextField(
+ value = postalCode,
+ onValueChange = {
+ postalCode = it
+ postalCodeError = false
+ },
+ singleLine = true,
+ label = { Text(stringResource(R.string.postal_code)) },
+ leadingIcon = {
+ Icon(
+ painter = painterResource(R.drawable.outline_pin_drop_24),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ },
+ modifier = Modifier.fillMaxWidth(),
+ isError = postalCodeError,
+ supportingText = {
+ if (postalCodeError) Text(stringResource(R.string.postal_code_error_text))
+ },
+ shape = RoundedCornerShape(12.dp)
+ )
+ }
}
}
}
- }
-}
+ }
+ Button(
+ onClick = {
+ val isOk = validateInputs()
+ if (isOk) {
+ onCompleted(
+ Parcel(
+ id = parcel?.id ?: 0,
+ humanName = humanName.ifBlank { context.getString(R.string.undefinied_packagename) },
+ parcelId = trackingId,
+ service = service,
+ postalCode =
+ if (
+ backend?.requiresPostCode == true ||
+ (backend?.acceptsPostCode == true && specifyPostalCode)
+ )
+ postalCode
+ else null,
+ )
+ )
+ }
+ },
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(12.dp)
+ ) {
+ Text(stringResource(if (isEdit) R.string.save else R.string.add_parcel))
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+ }
+ }
@Composable
@PreviewLightDark
fun AddParcelPreview() {
- ParcelTrackerTheme {
- AddEditParcelView(
- null,
- onBackPressed = {},
- onCompleted = {},
- )
- }
+ ParcelTrackerTheme { AddEditParcelView(null, onBackPressed = {}, onCompleted = {}) }
}
@Composable
@@ -279,9 +594,9 @@ fun AddParcelPreview() {
fun EditParcelPreview() {
ParcelTrackerTheme {
AddEditParcelView(
- Parcel(0, "Test", "Test", null, Service.EXAMPLE),
- onBackPressed = {},
- onCompleted = {},
+ Parcel(0, "Test", "Test", null, Service.EXAMPLE),
+ onBackPressed = {},
+ onCompleted = {},
)
}
}
diff --git a/app/src/main/java/dev/itsvic/parceltracker/ui/views/HomeView.kt b/app/src/main/java/dev/itsvic/parceltracker/ui/views/HomeView.kt
index cb015ee..5cbeff5 100644
--- a/app/src/main/java/dev/itsvic/parceltracker/ui/views/HomeView.kt
+++ b/app/src/main/java/dev/itsvic/parceltracker/ui/views/HomeView.kt
@@ -4,26 +4,12 @@ package dev.itsvic.parceltracker.ui.views
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Add
-import androidx.compose.material.icons.filled.Info
-import androidx.compose.material.icons.filled.MoreVert
-import androidx.compose.material.icons.filled.Settings
-import androidx.compose.material3.DropdownMenu
-import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.FloatingActionButton
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
@@ -35,84 +21,43 @@ import dev.itsvic.parceltracker.api.Status
import dev.itsvic.parceltracker.db.Parcel
import dev.itsvic.parceltracker.db.ParcelStatus
import dev.itsvic.parceltracker.db.ParcelWithStatus
-import dev.itsvic.parceltracker.ui.components.AboutDialog
-import dev.itsvic.parceltracker.ui.components.ParcelRow
-import dev.itsvic.parceltracker.ui.theme.MenuItemContentPadding
+import dev.itsvic.parceltracker.ui.components.ParcelCard
import dev.itsvic.parceltracker.ui.theme.ParcelTrackerTheme
import java.time.Instant
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeView(
- parcels: List,
- onNavigateToAddParcel: () -> Unit,
- onNavigateToParcel: (Parcel) -> Unit,
- onNavigateToSettings: () -> Unit,
+ parcels: List,
+ onNavigateToAddParcel: () -> Unit,
+ onNavigateToParcel: (Parcel) -> Unit,
+ onNavigateToSettings: () -> Unit,
) {
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
- var expanded by remember { mutableStateOf(false) }
- var aboutDialogOpen by remember { mutableStateOf(false) }
Scaffold(
- topBar = {
- LargeTopAppBar(
- title = { Text(stringResource(R.string.app_name)) },
- scrollBehavior = scrollBehavior,
- actions = {
- IconButton(onClick = { expanded = !expanded }) {
- Icon(Icons.Filled.MoreVert, stringResource(R.string.more_options))
- }
- DropdownMenu(
- expanded = expanded,
- onDismissRequest = { expanded = false },
- ) {
- DropdownMenuItem(
- leadingIcon = {
- Icon(Icons.Filled.Settings, stringResource(R.string.settings))
- },
- text = { Text(stringResource(R.string.settings)) },
- onClick = {
- expanded = false
- onNavigateToSettings()
- },
- contentPadding = MenuItemContentPadding,
- )
- DropdownMenuItem(
- leadingIcon = { Icon(Icons.Filled.Info, stringResource(R.string.about_app)) },
- text = { Text(stringResource(R.string.about_app)) },
- onClick = {
- expanded = false
- aboutDialogOpen = true
- },
- contentPadding = MenuItemContentPadding,
- )
- }
- },
- )
- },
- floatingActionButton = {
- FloatingActionButton(onClick = onNavigateToAddParcel) {
- Icon(Icons.Default.Add, contentDescription = stringResource(R.string.add_a_parcel))
+ topBar = {
+ LargeTopAppBar(
+ title = { Text(stringResource(R.string.app_name)) },
+ scrollBehavior = scrollBehavior,
+ )
+ },
+ modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+ ) { innerPadding ->
+ LazyColumn(modifier = Modifier.padding(innerPadding)) {
+ if (parcels.isEmpty())
+ item {
+ Text(
+ stringResource(R.string.no_parcels_flavor),
+ modifier = Modifier.padding(horizontal = 16.dp, vertical = 24.dp),
+ )
}
- },
- modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) { innerPadding ->
- LazyColumn(modifier = Modifier.padding(innerPadding)) {
- if (parcels.isEmpty())
- item {
- Text(
- stringResource(R.string.no_parcels_flavor),
- modifier = Modifier.padding(horizontal = 16.dp))
- }
- items(parcels.reversed()) { parcel ->
- ParcelRow(parcel.parcel, parcel.status?.status) { onNavigateToParcel(parcel.parcel) }
- }
- }
-
- if (aboutDialogOpen) {
- AboutDialog { aboutDialogOpen = false }
- }
+ items(parcels.reversed()) { parcel ->
+ ParcelCard(parcel.parcel, parcel.status?.status) { onNavigateToParcel(parcel.parcel) }
}
+ }
+ }
}
@Composable
@@ -120,14 +65,16 @@ fun HomeView(
fun HomeViewPreview() {
ParcelTrackerTheme {
HomeView(
- parcels =
- listOf(
- ParcelWithStatus(
- Parcel(0, "My precious package", "EXMPL0001", null, Service.EXAMPLE),
- ParcelStatus(0, Status.InTransit, Instant.now()))),
- onNavigateToAddParcel = {},
- onNavigateToParcel = {},
- onNavigateToSettings = {},
+ parcels =
+ listOf(
+ ParcelWithStatus(
+ Parcel(0, "My precious package", "EXMPL0001", null, Service.EXAMPLE),
+ ParcelStatus(0, Status.InTransit, Instant.now()),
+ )
+ ),
+ onNavigateToAddParcel = {},
+ onNavigateToParcel = {},
+ onNavigateToSettings = {},
)
}
}
diff --git a/app/src/main/java/dev/itsvic/parceltracker/ui/views/ParcelView.kt b/app/src/main/java/dev/itsvic/parceltracker/ui/views/ParcelView.kt
index 807adfe..2209e6b 100644
--- a/app/src/main/java/dev/itsvic/parceltracker/ui/views/ParcelView.kt
+++ b/app/src/main/java/dev/itsvic/parceltracker/ui/views/ParcelView.kt
@@ -1,43 +1,40 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.itsvic.parceltracker.ui.views
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
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.items
+import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.ui.res.painterResource
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
-import androidx.compose.material.icons.filled.Delete
-import androidx.compose.material.icons.filled.Edit
-import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.Button
import androidx.compose.material3.Card
-import androidx.compose.material3.DropdownMenu
-import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.MediumTopAppBar
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
-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.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewLightDark
@@ -48,196 +45,308 @@ import dev.itsvic.parceltracker.api.ParcelHistoryItem
import dev.itsvic.parceltracker.api.Service
import dev.itsvic.parceltracker.api.Status
import dev.itsvic.parceltracker.api.getDeliveryServiceName
+import dev.itsvic.parceltracker.ui.components.FloatingCollapsibleActionBar
import dev.itsvic.parceltracker.ui.components.ParcelHistoryItemRow
-import dev.itsvic.parceltracker.ui.theme.MenuItemContentPadding
import dev.itsvic.parceltracker.ui.theme.ParcelTrackerTheme
import java.time.LocalDateTime
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ParcelView(
- parcel: Parcel,
- humanName: String,
- service: Service,
- isArchived: Boolean,
- archivePromptDismissed: Boolean,
- onBackPressed: () -> Unit,
- onEdit: () -> Unit,
- onDelete: () -> Unit,
- onArchive: () -> Unit,
- onArchivePromptDismissal: () -> Unit,
+ parcel: Parcel,
+ humanName: String,
+ service: Service,
+ isArchived: Boolean,
+ archivePromptDismissed: Boolean,
+ onBackPressed: () -> Unit,
+ onEdit: () -> Unit,
+ onDelete: () -> Unit,
+ onArchive: () -> Unit,
+ onArchivePromptDismissal: () -> Unit,
+ showBackButton: Boolean = true,
) {
- val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
- var expanded by remember { mutableStateOf(false) }
-
- Scaffold(
+ Box {
+ Scaffold(
topBar = {
- MediumTopAppBar(
- title = { Text(humanName) },
- navigationIcon = {
+ TopAppBar(
+ title = {
+ Text(
+ text = humanName,
+ style = MaterialTheme.typography.titleLarge
+ )
+ },
+ navigationIcon = {
+ if (showBackButton) {
IconButton(onClick = onBackPressed) {
- Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.go_back))
- }
- },
- actions = {
- IconButton(onClick = { expanded = !expanded }) {
- Icon(Icons.Filled.MoreVert, stringResource(R.string.more_options))
- }
- DropdownMenu(
- expanded = expanded,
- onDismissRequest = { expanded = false },
- ) {
- DropdownMenuItem(
- leadingIcon = { Icon(Icons.Filled.Edit, stringResource(R.string.edit)) },
- text = { Text(stringResource(R.string.edit)) },
- onClick = {
- expanded = false
- onEdit()
- },
- contentPadding = MenuItemContentPadding,
- )
- if (!isArchived)
- DropdownMenuItem(
- leadingIcon = {
- Icon(
- painterResource(R.drawable.archive), stringResource(R.string.archive))
- },
- text = { Text(stringResource(R.string.archive)) },
- onClick = onArchive,
- contentPadding = MenuItemContentPadding,
- )
- DropdownMenuItem(
- leadingIcon = { Icon(Icons.Filled.Delete, stringResource(R.string.delete)) },
- text = { Text(stringResource(R.string.delete)) },
- onClick = onDelete,
- contentPadding = MenuItemContentPadding,
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = stringResource(R.string.go_back)
)
}
- },
- scrollBehavior = scrollBehavior,
+ }
+ }
)
},
- modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) { innerPadding ->
- LazyColumn(
- modifier = Modifier.padding(innerPadding).padding(16.dp, 0.dp),
- verticalArrangement = Arrangement.spacedBy(8.dp),
- ) {
- item {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween) {
- getDeliveryServiceName(service)?.let {
- Text(
- stringResource(it),
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant)
- }
+ bottomBar = {},
+ ) { innerPadding ->
+ LazyColumn(
+ modifier = Modifier
+ .padding(innerPadding)
+ .padding(16.dp)
+ .padding(bottom = 45.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
- SelectionContainer {
- Text(
- parcel.id,
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant)
- }
+ if (parcel.properties.isNotEmpty()) {
+ item {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ Column(modifier = Modifier.padding(20.dp)) {
+ Row(
+ verticalAlignment = androidx.compose.ui.Alignment.CenterVertically,
+ modifier = Modifier.padding(bottom = 16.dp)
+ ) {
+ Box(
+ modifier = Modifier
+ .size(40.dp)
+ .background(
+ MaterialTheme.colorScheme.secondaryContainer,
+ CircleShape
+ ),
+ contentAlignment = androidx.compose.ui.Alignment.Center
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.package_2),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSecondaryContainer,
+ modifier = Modifier.size(20.dp)
+ )
}
- }
-
- items(parcel.properties.entries.toList()) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween) {
+ Spacer(modifier = Modifier.size(12.dp))
+ Text(
+ text = stringResource(R.string.additional_info),
+ style = MaterialTheme.typography.titleLarge,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ }
+
+ parcel.properties.entries.forEachIndexed { index, entry ->
+ if (index > 0) {
+ HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
+ }
+ Row(
+ modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
+ ) {
Text(
- stringResource(it.key),
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant)
+ text = stringResource(entry.key),
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
Text(
- it.value,
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- textAlign = TextAlign.End)
+ text = entry.value,
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurface
+ )
}
+ }
+ }
}
-
- item {
+ }
+ }
+ item {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
+ shape = RoundedCornerShape(16.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = when (parcel.currentStatus) {
+ Status.Delivered, Status.PickedUp -> MaterialTheme.colorScheme.primaryContainer
+ Status.InTransit, Status.OutForDelivery -> MaterialTheme.colorScheme.tertiaryContainer
+ else -> MaterialTheme.colorScheme.surfaceVariant
+ }
+ )
+ ) {
+ Column(modifier = Modifier.padding(20.dp)) {
+ Row(
+ verticalAlignment = androidx.compose.ui.Alignment.CenterVertically,
+ modifier = Modifier.padding(bottom = 12.dp)
+ ) {
+ Box(
+ modifier = Modifier
+ .size(40.dp)
+ .background(
+ when (parcel.currentStatus) {
+ Status.Delivered, Status.PickedUp -> MaterialTheme.colorScheme.primary
+ Status.InTransit, Status.OutForDelivery -> MaterialTheme.colorScheme.tertiary
+ else -> MaterialTheme.colorScheme.outline
+ },
+ CircleShape
+ ),
+ contentAlignment = androidx.compose.ui.Alignment.Center
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.outline_deployed_code_history_24),
+ contentDescription = null,
+ tint = when (parcel.currentStatus) {
+ Status.Delivered, Status.PickedUp -> MaterialTheme.colorScheme.onPrimary
+ Status.InTransit, Status.OutForDelivery -> MaterialTheme.colorScheme.onTertiary
+ else -> MaterialTheme.colorScheme.surface
+ },
+ modifier = Modifier.size(20.dp)
+ )
+ }
+ Spacer(modifier = Modifier.size(12.dp))
+ Text(
+ text = stringResource(R.string.current_status),
+ style = MaterialTheme.typography.titleMedium,
+ color = when (parcel.currentStatus) {
+ Status.Delivered, Status.PickedUp -> MaterialTheme.colorScheme.onPrimaryContainer
+ Status.InTransit, Status.OutForDelivery -> MaterialTheme.colorScheme.onTertiaryContainer
+ else -> MaterialTheme.colorScheme.onSurfaceVariant
+ }
+ )
+ }
Text(
- LocalContext.current.getString(parcel.currentStatus.nameResource),
- style = MaterialTheme.typography.headlineLarge,
- modifier = Modifier.padding(vertical = 16.dp),
+ text = LocalContext.current.getString(parcel.currentStatus.nameResource),
+ style = MaterialTheme.typography.headlineMedium,
+ color = when (parcel.currentStatus) {
+ Status.Delivered, Status.PickedUp -> MaterialTheme.colorScheme.onPrimaryContainer
+ Status.InTransit, Status.OutForDelivery -> MaterialTheme.colorScheme.onTertiaryContainer
+ else -> MaterialTheme.colorScheme.onSurfaceVariant
+ }
)
}
+ }
+ }
- if (!isArchived &&
- !archivePromptDismissed &&
- (parcel.currentStatus == Status.Delivered || parcel.currentStatus == Status.PickedUp))
- item {
- Card(
- shape = RoundedCornerShape(16.dp),
- modifier = Modifier.padding(bottom = 16.dp)) {
- Column(
- Modifier.padding(24.dp),
- verticalArrangement = Arrangement.spacedBy(8.dp)) {
- Text(
- stringResource(R.string.archive_prompt_question),
- style = MaterialTheme.typography.titleMedium)
- Text(stringResource(R.string.archive_prompt_text))
- Row(
- horizontalArrangement = Arrangement.spacedBy(16.dp),
- modifier = Modifier.fillMaxWidth()) {
- FilledTonalButton(
- onArchivePromptDismissal, modifier = Modifier.weight(1f)) {
- Text(stringResource(R.string.ignore))
- }
- Button(onArchive, modifier = Modifier.weight(1f)) {
- Text(stringResource(R.string.archive))
- }
- }
- }
- }
+ if (
+ !isArchived &&
+ !archivePromptDismissed &&
+ (parcel.currentStatus == Status.Delivered || parcel.currentStatus == Status.PickedUp)
+ )
+ item {
+ Card(shape = RoundedCornerShape(16.dp), modifier = Modifier.padding(bottom = 16.dp)) {
+ Column(Modifier.padding(24.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ Text(
+ stringResource(R.string.archive_prompt_question),
+ style = MaterialTheme.typography.titleMedium,
+ )
+ Text(stringResource(R.string.archive_prompt_text))
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ FilledTonalButton(onArchivePromptDismissal, modifier = Modifier.weight(1f)) {
+ Text(stringResource(R.string.ignore))
+ }
+ Button(onArchive, modifier = Modifier.weight(1f)) {
+ Text(stringResource(R.string.archive))
+ }
}
-
- items(parcel.history.size) { index ->
- if (index > 0) HorizontalDivider(Modifier.padding(top = 8.dp, bottom = 16.dp))
- ParcelHistoryItemRow(parcel.history[index])
+ }
+ }
+ }
+ if (parcel.history.isNotEmpty()) {
+ item {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ Column(modifier = Modifier.padding(20.dp)) {
+ Row(
+ verticalAlignment = androidx.compose.ui.Alignment.CenterVertically,
+ modifier = Modifier.padding(bottom = 16.dp)
+ ) {
+ Box(
+ modifier = Modifier
+ .size(40.dp)
+ .background(
+ MaterialTheme.colorScheme.tertiaryContainer,
+ CircleShape
+ ),
+ contentAlignment = androidx.compose.ui.Alignment.Center
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.outline_deployed_code_history_24),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onTertiaryContainer,
+ modifier = Modifier.size(20.dp)
+ )
+ }
+ Spacer(modifier = Modifier.size(12.dp))
+ Text(
+ text = stringResource(R.string.tracking_history),
+ style = MaterialTheme.typography.titleLarge,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ }
+
+ parcel.history.forEachIndexed { index, historyItem ->
+ if (index > 0) {
+ HorizontalDivider(modifier = Modifier.padding(vertical = 12.dp))
+ }
+ ParcelHistoryItemRow(historyItem)
+ }
+ }
}
}
}
+ }
+ FloatingCollapsibleActionBar(
+ status = parcel.currentStatus,
+ onEdit = onEdit,
+ onArchive = onArchive,
+ onDelete = onDelete,
+ modifier = Modifier.align(Alignment.BottomCenter)
+ )
+ }
}
@Composable
@PreviewLightDark
-private fun ParcelViewPreview() {
+fun ParcelViewPreview() {
val parcel =
- Parcel(
- "EXMPL0001",
- listOf(
- ParcelHistoryItem(
- "The package got lost. Whoops!",
- LocalDateTime.of(2025, 1, 1, 12, 0, 0),
- "Warsaw, Poland"),
- ParcelHistoryItem(
- "Arrived at local warehouse",
- LocalDateTime.of(2025, 1, 1, 10, 0, 0),
- "Warsaw, Poland"),
- ParcelHistoryItem(
- "En route to local warehouse",
- LocalDateTime.of(2024, 12, 1, 12, 0, 0),
- "Netherlands"),
- ParcelHistoryItem(
- "Label created", LocalDateTime.of(2024, 12, 1, 12, 0, 0), "Netherlands"),
- ),
- Status.DeliveryFailure)
+ Parcel(
+ "EXMPL0001",
+ listOf(
+ ParcelHistoryItem(
+ "The package got lost. Whoops!",
+ LocalDateTime.of(2025, 1, 1, 12, 0, 0),
+ "Warsaw, Poland",
+ ),
+ ParcelHistoryItem(
+ "Arrived at local warehouse",
+ LocalDateTime.of(2025, 1, 1, 10, 0, 0),
+ "Warsaw, Poland",
+ ),
+ ParcelHistoryItem(
+ "En route to local warehouse",
+ LocalDateTime.of(2024, 12, 1, 12, 0, 0),
+ "Netherlands",
+ ),
+ ParcelHistoryItem("Label created", LocalDateTime.of(2024, 12, 1, 12, 0, 0), "Netherlands"),
+ ),
+ Status.DeliveryFailure,
+ )
ParcelTrackerTheme {
ParcelView(
- parcel,
- "My precious package",
- Service.EXAMPLE,
- isArchived = false,
- archivePromptDismissed = false,
- onBackPressed = {},
- onEdit = {},
- onDelete = {},
- onArchive = {},
- onArchivePromptDismissal = {},
+ parcel,
+ "My precious package",
+ Service.EXAMPLE,
+ isArchived = false,
+ archivePromptDismissed = false,
+ onBackPressed = {},
+ onEdit = {},
+ onDelete = {},
+ onArchive = {},
+ onArchivePromptDismissal = {},
)
}
}
+}
diff --git a/app/src/main/java/dev/itsvic/parceltracker/ui/views/SettingsView.kt b/app/src/main/java/dev/itsvic/parceltracker/ui/views/SettingsView.kt
index 087ca56..347f90f 100644
--- a/app/src/main/java/dev/itsvic/parceltracker/ui/views/SettingsView.kt
+++ b/app/src/main/java/dev/itsvic/parceltracker/ui/views/SettingsView.kt
@@ -5,19 +5,26 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
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.layout.width
import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.FilledTonalButton
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
@@ -25,15 +32,21 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+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.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLinkStyles
+import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.fromHtml
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.style.TextDecoration
@@ -42,36 +55,39 @@ import androidx.compose.ui.unit.dp
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import dev.itsvic.parceltracker.BuildConfig
+import dev.itsvic.parceltracker.CLIPBOARD_PASTE_ENABLED
import dev.itsvic.parceltracker.DEMO_MODE
import dev.itsvic.parceltracker.DHL_API_KEY
+import dev.itsvic.parceltracker.PREFERRED_REGION
import dev.itsvic.parceltracker.R
import dev.itsvic.parceltracker.UNMETERED_ONLY
-import dev.itsvic.parceltracker.api.ParcelHistoryItem
-import dev.itsvic.parceltracker.api.Service
-import dev.itsvic.parceltracker.api.Status
import dev.itsvic.parceltracker.dataStore
-import dev.itsvic.parceltracker.db.Parcel
import dev.itsvic.parceltracker.enqueueNotificationWorker
-import dev.itsvic.parceltracker.sendNotification
-import dev.itsvic.parceltracker.ui.components.LogcatButton
+import dev.itsvic.parceltracker.ui.components.AboutDialog
import dev.itsvic.parceltracker.ui.theme.ParcelTrackerTheme
-import java.time.LocalDateTime
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun SettingsView(
- onBackPressed: () -> Unit,
-) {
+fun SettingsView() {
val context = LocalContext.current
val demoMode by context.dataStore.data.map { it[DEMO_MODE] == true }.collectAsState(false)
val unmeteredOnly by
- context.dataStore.data.map { it[UNMETERED_ONLY] == true }.collectAsState(false)
+ context.dataStore.data.map { it[UNMETERED_ONLY] == true }.collectAsState(false)
+ val clipboardPasteEnabled by
+ context.dataStore.data.map { it[CLIPBOARD_PASTE_ENABLED] == true }.collectAsState(false)
+ val preferredRegion by
+ context.dataStore.data.map { it[PREFERRED_REGION] ?: "" }.collectAsState("")
+ val dhlApiKey by context.dataStore.data.map { it[DHL_API_KEY] ?: "" }.collectAsState("")
+
val coroutineScope = rememberCoroutineScope()
+ var regionDropdownExpanded by remember { mutableStateOf(false) }
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
+ var aboutDialogOpen by remember { mutableStateOf(false) }
- val dhlApiKey by context.dataStore.data.map { it[DHL_API_KEY] ?: "" }.collectAsState("")
+ val testPackageName = stringResource(R.string.settings_test_package_name)
+ val testPackageStatus = stringResource(R.string.settings_test_package_status)
fun setValue(key: Preferences.Key, value: T) {
coroutineScope.launch { context.dataStore.edit { it[key] = value } }
@@ -80,116 +96,352 @@ fun SettingsView(
val setUnmeteredOnly: (Boolean) -> Unit = { value ->
coroutineScope.launch {
context.dataStore.edit { it[UNMETERED_ONLY] = value }
- // reschedule notification worker to update constraints
context.enqueueNotificationWorker()
}
}
Scaffold(
- topBar = {
- LargeTopAppBar(
- title = { Text(stringResource(R.string.settings)) },
- navigationIcon = {
- IconButton(onClick = onBackPressed) {
- Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.go_back))
- }
- },
- scrollBehavior = scrollBehavior,
- )
- },
- modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+ topBar = {
+ LargeTopAppBar(
+ title = { Text(stringResource(R.string.settings)) },
+ scrollBehavior = scrollBehavior,
+ )
+ },
+ modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { innerPadding ->
- Column(Modifier.padding(innerPadding).verticalScroll(rememberScrollState())) {
- Row(
- modifier =
- Modifier.clickable { setUnmeteredOnly(unmeteredOnly.not()) }
- .padding(16.dp, 12.dp)
- .fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically,
+ Column(
+ modifier = Modifier
+ .padding(innerPadding)
+ .padding(horizontal = 16.dp)
+ .verticalScroll(rememberScrollState())
+ ) {
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable { aboutDialogOpen = true },
+ elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
- Column(modifier = Modifier.fillMaxWidth(0.8f)) {
- Text(stringResource(R.string.unmetered_only_setting))
- Text(
- stringResource(R.string.unmetered_only_setting_detail),
- style = MaterialTheme.typography.bodyMedium)
+ Column(
+ modifier = Modifier.padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Spacer(modifier = Modifier.height(8.dp))
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.icon_foreground),
+ contentDescription = null,
+ modifier = Modifier.size(100.dp),
+ tint = Color.Unspecified
+ )
+ Spacer(modifier = Modifier.width(16.dp))
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = stringResource(R.string.app_name),
+ style = MaterialTheme.typography.headlineSmall,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ text = stringResource(R.string.settings_version_label, BuildConfig.VERSION_NAME),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ Spacer(modifier = Modifier.height(8.dp))
}
- Switch(checked = unmeteredOnly, onCheckedChange = { setUnmeteredOnly(it) })
}
+
+ Spacer(modifier = Modifier.height(16.dp))
- Text(
- stringResource(R.string.settings_api_keys),
- modifier = Modifier.padding(16.dp, 16.dp, 16.dp, 2.dp),
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- )
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
+ ) {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(bottom = 12.dp)
+ ) {
+ Icon(
+ painterResource(R.drawable.ic_networkwifi),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary
+ )
+ Text(
+ text = stringResource(R.string.settings_network),
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.SemiBold,
+ modifier = Modifier.padding(start = 8.dp)
+ )
+ }
+
+ Row(
+ modifier = Modifier
+ .clickable { setUnmeteredOnly(unmeteredOnly.not()) }
+ .padding(vertical = 8.dp)
+ .fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Column(modifier = Modifier.fillMaxWidth(0.8f)) {
+ Text(stringResource(R.string.unmetered_only_setting))
+ Text(
+ stringResource(R.string.unmetered_only_setting_detail),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ Switch(checked = unmeteredOnly, onCheckedChange = { setUnmeteredOnly(it) })
+ }
+
+ Row(
+ modifier = Modifier
+ .clickable { setValue(CLIPBOARD_PASTE_ENABLED, clipboardPasteEnabled.not()) }
+ .padding(vertical = 8.dp)
+ .fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Column(modifier = Modifier.fillMaxWidth(0.8f)) {
+ Text(stringResource(R.string.clipboard_paste_enabled))
+ Text(
+ stringResource(R.string.clipboard_paste_description),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ Switch(
+ checked = clipboardPasteEnabled,
+ onCheckedChange = { setValue(CLIPBOARD_PASTE_ENABLED, it) },
+ )
+ }
+ }
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
- OutlinedTextField(
- dhlApiKey,
- { setValue(DHL_API_KEY, it) },
- modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp).fillMaxWidth(),
- label = { Text(stringResource(R.string.service_dhl)) },
- singleLine = true,
- visualTransformation = PasswordVisualTransformation(),
- )
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
+ ) {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(bottom = 12.dp)
+ ) {
+ Icon(
+ painterResource(R.drawable.ic_language),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary
+ )
+ Text(
+ text = stringResource(R.string.settings_region),
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.SemiBold,
+ modifier = Modifier.padding(start = 8.dp)
+ )
+ }
+
+ Text(
+ stringResource(R.string.preferred_region_description),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.padding(bottom = 8.dp),
+ )
- Text(
- AnnotatedString.fromHtml(
- stringResource(R.string.dhl_api_key_flavor_text),
- linkStyles =
- TextLinkStyles(
- style =
- SpanStyle(
- textDecoration = TextDecoration.Underline,
- color = MaterialTheme.colorScheme.primary))),
- style = MaterialTheme.typography.bodyMedium,
- modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp))
+ ExposedDropdownMenuBox(
+ expanded = regionDropdownExpanded,
+ onExpandedChange = { regionDropdownExpanded = !regionDropdownExpanded },
+ ) {
+ OutlinedTextField(
+ value = when (preferredRegion) {
+ "international" -> stringResource(R.string.region_international)
+ "north_america" -> stringResource(R.string.region_north_america)
+ "europe" -> stringResource(R.string.region_europe)
+ "asia" -> stringResource(R.string.region_asia)
+ "belarus" -> stringResource(R.string.country_belarus)
+ "bulgaria" -> stringResource(R.string.country_bulgaria)
+ "czech" -> stringResource(R.string.country_czech)
+ "uk" -> stringResource(R.string.country_uk)
+ "ireland" -> stringResource(R.string.country_ireland)
+ "poland" -> stringResource(R.string.country_poland)
+ "hungary" -> stringResource(R.string.country_hungary)
+ "germany" -> stringResource(R.string.country_germany)
+ "italy" -> stringResource(R.string.country_italy)
+ "romania" -> stringResource(R.string.country_romania)
+ "scandinavia" -> stringResource(R.string.country_scandinavia)
+ "ukraine" -> stringResource(R.string.country_ukraine)
+ "india" -> stringResource(R.string.country_india)
+ "thailand" -> stringResource(R.string.country_thailand)
+ else -> stringResource(R.string.region_international)
+ },
+ onValueChange = {},
+ readOnly = true,
+ label = { Text(stringResource(R.string.settings_region)) },
+ leadingIcon = {
+ Icon(
+ painterResource(R.drawable.ic_language),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary
+ )
+ },
+ trailingIcon = {
+ ExposedDropdownMenuDefaults.TrailingIcon(expanded = regionDropdownExpanded)
+ },
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedBorderColor = MaterialTheme.colorScheme.primary,
+ unfocusedBorderColor = MaterialTheme.colorScheme.outline,
+ focusedLabelColor = MaterialTheme.colorScheme.primary,
+ unfocusedLabelColor = MaterialTheme.colorScheme.onSurfaceVariant
+ ),
+ shape = RoundedCornerShape(12.dp),
+ modifier = Modifier.menuAnchor().fillMaxWidth(),
+ )
- Text(
- stringResource(R.string.settings_experimental),
- modifier = Modifier.padding(16.dp, 16.dp, 16.dp, 2.dp),
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- )
+ ExposedDropdownMenu(
+ expanded = regionDropdownExpanded,
+ onDismissRequest = { regionDropdownExpanded = false },
+ ) {
+ listOf(
+ "international" to R.string.region_international,
+ "north_america" to R.string.region_north_america,
+ "europe" to R.string.region_europe,
+ "asia" to R.string.region_asia,
+ "belarus" to R.string.country_belarus,
+ "bulgaria" to R.string.country_bulgaria,
+ "czech" to R.string.country_czech,
+ "uk" to R.string.country_uk,
+ "ireland" to R.string.country_ireland,
+ "poland" to R.string.country_poland,
+ "hungary" to R.string.country_hungary,
+ "germany" to R.string.country_germany,
+ "italy" to R.string.country_italy,
+ "romania" to R.string.country_romania,
+ "scandinavia" to R.string.country_scandinavia,
+ "ukraine" to R.string.country_ukraine,
+ "india" to R.string.country_india,
+ "thailand" to R.string.country_thailand
+ ).forEach { (key, stringRes) ->
+ DropdownMenuItem(
+ text = { Text(stringResource(stringRes)) },
+ onClick = {
+ setValue(PREFERRED_REGION, key)
+ regionDropdownExpanded = false
+ },
+ )
+ }
+ }
+ }
+ }
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
- Row(
- modifier =
- Modifier.clickable { setValue(DEMO_MODE, demoMode.not()) }
- .padding(16.dp, 12.dp)
- .fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically,
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
- Column(modifier = Modifier.fillMaxWidth(0.8f)) {
- Text(stringResource(R.string.demo_mode))
+ Column(modifier = Modifier.padding(16.dp)) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(bottom = 12.dp)
+ ) {
+ Icon(
+ painterResource(R.drawable.ic_vpnkey),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary
+ )
+ Text(
+ text = stringResource(R.string.settings_api_keys),
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.SemiBold,
+ modifier = Modifier.padding(start = 8.dp)
+ )
+ }
+
+ OutlinedTextField(
+ value = dhlApiKey,
+ onValueChange = { setValue(DHL_API_KEY, it) },
+ label = { Text(stringResource(R.string.settings_dhl_api_key_label)) },
+ visualTransformation = PasswordVisualTransformation(),
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(12.dp)
+ )
+
Text(
- stringResource(R.string.demo_mode_detail),
- style = MaterialTheme.typography.bodyMedium)
+ AnnotatedString.fromHtml(
+ stringResource(R.string.dhl_api_key_flavor_text),
+ linkStyles = TextLinkStyles(
+ style = SpanStyle(
+ textDecoration = TextDecoration.Underline,
+ color = MaterialTheme.colorScheme.primary,
+ )
+ ),
+ ),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.padding(top = 8.dp),
+ )
}
- Switch(checked = demoMode, onCheckedChange = { setValue(DEMO_MODE, it) })
}
+
+ Spacer(modifier = Modifier.height(16.dp))
- if (BuildConfig.DEBUG)
- FilledTonalButton(
- onClick = {
- context.sendNotification(
- Parcel(0xf100f, "Cool stuff", "", null, Service.EXAMPLE),
- Status.OutForDelivery,
- ParcelHistoryItem(
- "The courier has picked up the package", LocalDateTime.now(), ""))
- },
- modifier = Modifier.padding(16.dp, 12.dp).fillMaxWidth()) {
- Text("Send test notification")
- }
-
- LogcatButton(modifier = Modifier.padding(16.dp, 12.dp).fillMaxWidth())
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
+ ) {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(bottom = 12.dp)
+ ) {
+ Icon(
+ painterResource(R.drawable.ic_science),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary
+ )
+ Text(
+ text = stringResource(R.string.settings_experimental),
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.SemiBold,
+ modifier = Modifier.padding(start = 8.dp)
+ )
+ }
+
+ Row(
+ modifier = Modifier
+ .clickable { setValue(DEMO_MODE, demoMode.not()) }
+ .padding(vertical = 8.dp)
+ .fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Column(modifier = Modifier.fillMaxWidth(0.8f)) {
+ Text(stringResource(R.string.demo_mode))
+ Text(
+ stringResource(R.string.demo_mode_detail),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ Switch(checked = demoMode, onCheckedChange = { setValue(DEMO_MODE, it) })
+ }
+ }
+ }
+ Spacer(modifier = Modifier.height(24.dp))
+ }
- Text(
- "Parcel ${BuildConfig.VERSION_NAME}",
- modifier = Modifier.padding(16.dp, 8.dp),
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- )
+ if (aboutDialogOpen) {
+ AboutDialog { aboutDialogOpen = false }
}
}
}
@@ -197,9 +449,5 @@ fun SettingsView(
@Composable
@PreviewLightDark
private fun SettingsViewPreview() {
- ParcelTrackerTheme {
- SettingsView(
- onBackPressed = {},
- )
- }
+ ParcelTrackerTheme { SettingsView() }
}
diff --git a/app/src/main/java/dev/itsvic/parceltracker/ui/views/TabletView.kt b/app/src/main/java/dev/itsvic/parceltracker/ui/views/TabletView.kt
new file mode 100644
index 0000000..42ef185
--- /dev/null
+++ b/app/src/main/java/dev/itsvic/parceltracker/ui/views/TabletView.kt
@@ -0,0 +1,184 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+package dev.itsvic.parceltracker.ui.views
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Home
+import androidx.compose.material.icons.filled.Settings
+import androidx.compose.material3.Card
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import dev.itsvic.parceltracker.R
+import dev.itsvic.parceltracker.db.Parcel
+import dev.itsvic.parceltracker.db.ParcelWithStatus
+import dev.itsvic.parceltracker.ui.components.ParcelRow
+import dev.itsvic.parceltracker.api.Parcel as APIParcel
+
+enum class TabletNavigationItem {
+ HOME,
+ ADD_PARCEL,
+ EDIT_PARCEL,
+ SETTINGS,
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun TabletView(
+ parcels: List,
+ selectedParcel: Parcel?,
+ apiParcel: APIParcel?,
+ isLoading: Boolean,
+ currentNavigationItem: TabletNavigationItem,
+ onNavigateToItem: (TabletNavigationItem) -> Unit,
+ onNavigateToParcel: (Parcel) -> Unit,
+ onNavigateToAddParcel: () -> Unit,
+ onNavigateToSettings: () -> Unit,
+ onEditParcel: (Parcel) -> Unit,
+ onDeleteParcel: (Parcel) -> Unit,
+ onArchiveParcel: (Parcel) -> Unit,
+ onArchivePromptDismissal: (Parcel) -> Unit,
+ settingsContent: @Composable () -> Unit = {},
+ addParcelContent: @Composable () -> Unit = {},
+ editParcelContent: @Composable () -> Unit = {},
+) {
+ val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
+
+ Row(modifier = Modifier.fillMaxSize()) {
+ Card(modifier = Modifier.width(400.dp).fillMaxHeight().padding(8.dp)) {
+ Column {
+ Text(
+ text = stringResource(R.string.app_name),
+ style = MaterialTheme.typography.headlineSmall,
+ modifier = Modifier.padding(16.dp, 16.dp, 16.dp, 8.dp),
+ )
+
+ LazyColumn(modifier = Modifier.fillMaxWidth().weight(1f).padding(horizontal = 8.dp)) {
+ if (parcels.isEmpty()) {
+ item {
+ Card(modifier = Modifier.fillMaxWidth().padding(8.dp)) {
+ Text(
+ stringResource(R.string.no_parcels_flavor),
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.padding(16.dp),
+ )
+ }
+ }
+ } else {
+ items(parcels.reversed()) { parcel ->
+ Card(modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp)) {
+ ParcelRow(
+ parcel.parcel,
+ parcel.status?.status,
+ isSelected = selectedParcel?.id == parcel.parcel.id,
+ ) {
+ onNavigateToParcel(parcel.parcel)
+ }
+ }
+ }
+ }
+ }
+
+ HorizontalDivider()
+
+ NavigationBar(modifier = Modifier.fillMaxWidth()) {
+ NavigationBarItem(
+ icon = { Icon(Icons.Filled.Home, contentDescription = null) },
+ label = { Text(stringResource(R.string.home)) },
+ selected = currentNavigationItem == TabletNavigationItem.HOME,
+ onClick = { onNavigateToItem(TabletNavigationItem.HOME) },
+ )
+ NavigationBarItem(
+ icon = { Icon(Icons.Filled.Add, contentDescription = null) },
+ label = { Text(stringResource(R.string.add_parcel)) },
+ selected = currentNavigationItem == TabletNavigationItem.ADD_PARCEL,
+ onClick = { onNavigateToItem(TabletNavigationItem.ADD_PARCEL) },
+ )
+ NavigationBarItem(
+ icon = { Icon(Icons.Filled.Settings, contentDescription = null) },
+ label = { Text(stringResource(R.string.settings)) },
+ selected = currentNavigationItem == TabletNavigationItem.SETTINGS,
+ onClick = { onNavigateToItem(TabletNavigationItem.SETTINGS) },
+ )
+ }
+ }
+ }
+
+ // Right panel: Content area
+ Card(modifier = Modifier.weight(1f).fillMaxHeight().padding(8.dp)) {
+ when (currentNavigationItem) {
+ TabletNavigationItem.HOME -> {
+ if (selectedParcel != null) {
+ if (isLoading) {
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ CircularProgressIndicator()
+ }
+ } else if (apiParcel != null) {
+ ParcelView(
+ parcel = apiParcel,
+ humanName = selectedParcel.humanName,
+ service = selectedParcel.service,
+ isArchived = selectedParcel.isArchived,
+ archivePromptDismissed = selectedParcel.archivePromptDismissed,
+ onBackPressed = { /* No back button in tablet mode */ },
+ onEdit = { onEditParcel(selectedParcel) },
+ onDelete = { onDeleteParcel(selectedParcel) },
+ onArchive = { onArchiveParcel(selectedParcel) },
+ onArchivePromptDismissal = { onArchivePromptDismissal(selectedParcel) },
+ showBackButton = false,
+ )
+ }
+ } else {
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Text(
+ text = stringResource(R.string.app_name),
+ style = MaterialTheme.typography.headlineMedium,
+ modifier = Modifier.padding(bottom = 16.dp),
+ )
+ Text(
+ text = stringResource(R.string.select_parcel_to_view),
+ style = MaterialTheme.typography.bodyLarge,
+ )
+ }
+ }
+ }
+ }
+ TabletNavigationItem.ADD_PARCEL -> {
+ addParcelContent()
+ }
+ TabletNavigationItem.EDIT_PARCEL -> {
+ editParcelContent()
+ }
+ TabletNavigationItem.SETTINGS -> {
+ settingsContent()
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/res/drawable-anydpi/ic_contentpaste.xml b/app/src/main/res/drawable-anydpi/ic_contentpaste.xml
new file mode 100644
index 0000000..3c13649
--- /dev/null
+++ b/app/src/main/res/drawable-anydpi/ic_contentpaste.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/app/src/main/res/drawable-anydpi/ic_filterlist.xml b/app/src/main/res/drawable-anydpi/ic_filterlist.xml
new file mode 100644
index 0000000..9820675
--- /dev/null
+++ b/app/src/main/res/drawable-anydpi/ic_filterlist.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/app/src/main/res/drawable-anydpi/ic_label.xml b/app/src/main/res/drawable-anydpi/ic_label.xml
new file mode 100644
index 0000000..e255848
--- /dev/null
+++ b/app/src/main/res/drawable-anydpi/ic_label.xml
@@ -0,0 +1,12 @@
+
+
+
diff --git a/app/src/main/res/drawable-anydpi/ic_language.xml b/app/src/main/res/drawable-anydpi/ic_language.xml
new file mode 100644
index 0000000..8beebb7
--- /dev/null
+++ b/app/src/main/res/drawable-anydpi/ic_language.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/app/src/main/res/drawable-anydpi/ic_localshipping.xml b/app/src/main/res/drawable-anydpi/ic_localshipping.xml
new file mode 100644
index 0000000..76832c9
--- /dev/null
+++ b/app/src/main/res/drawable-anydpi/ic_localshipping.xml
@@ -0,0 +1,12 @@
+
+
+
diff --git a/app/src/main/res/drawable-anydpi/ic_locationon.xml b/app/src/main/res/drawable-anydpi/ic_locationon.xml
new file mode 100644
index 0000000..4d88730
--- /dev/null
+++ b/app/src/main/res/drawable-anydpi/ic_locationon.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/app/src/main/res/drawable-anydpi/ic_networkwifi.xml b/app/src/main/res/drawable-anydpi/ic_networkwifi.xml
new file mode 100644
index 0000000..d508b16
--- /dev/null
+++ b/app/src/main/res/drawable-anydpi/ic_networkwifi.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/app/src/main/res/drawable-anydpi/ic_qrcode.xml b/app/src/main/res/drawable-anydpi/ic_qrcode.xml
new file mode 100644
index 0000000..ca6249b
--- /dev/null
+++ b/app/src/main/res/drawable-anydpi/ic_qrcode.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable-anydpi/ic_science.xml b/app/src/main/res/drawable-anydpi/ic_science.xml
new file mode 100644
index 0000000..a00f40b
--- /dev/null
+++ b/app/src/main/res/drawable-anydpi/ic_science.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/app/src/main/res/drawable-anydpi/ic_timeline.xml b/app/src/main/res/drawable-anydpi/ic_timeline.xml
new file mode 100644
index 0000000..4566deb
--- /dev/null
+++ b/app/src/main/res/drawable-anydpi/ic_timeline.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/app/src/main/res/drawable-anydpi/ic_vpnkey.xml b/app/src/main/res/drawable-anydpi/ic_vpnkey.xml
new file mode 100644
index 0000000..ead0ae1
--- /dev/null
+++ b/app/src/main/res/drawable-anydpi/ic_vpnkey.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index 666ec55..57b0790 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -17,7 +17,6 @@
Upravit
Upravit zásilku
Přejít zpět
- Název nesmí být prázdný.
Ignorovat
Licence
Další možnosti
@@ -66,4 +65,5 @@
Sledovací ID nesmí být prázdné.
Aktualizovat pouze na neměřených sítích
Pokud je povoleno, bude aplikace Parcel zjišťovat aktualizace pouze na neměřených připojeních, jako je vaše domácí Wi-Fi.
+ Balíček
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 56d766a..e179b26 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -20,7 +20,6 @@
Paket bearbeiten
Dieser Zustelldienst erfordert einen API-Schlüssel, aber es wurde keiner angegeben. Weitere Informationen finden Sie in den App-Einstellungen.
Zurück
- Name darf nicht leer sein.
Ignorieren
Lizenz
Weitere Optionen
@@ -73,4 +72,5 @@
Sendungsnummer darf nicht leer sein.
Nur bei unbegrenzten Netzwerken aktualisieren
Wenn aktiviert, sucht die App nur über unbegrenzte Verbindungen wie WLAN nach Paketupdates.
+ Paket
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index 206a034..de12071 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -1,23 +1,72 @@
App névjegye
+ Hozzáadás
Csomag hozzáadása
Csomag hozzáadása
Archivál
Biztosan archiválod ezt a csomagot?
Ha archiválod, akkor a csomag előzmény az eszközön kerül eltárolásra és frissítés sem történik.
+ Mégse
+ Beillesztés vágólapról
+ Beillesztés gomb engedélyezése
+ Beillesztés gomb megjelenítése a csomagszám mező mellett
+ Előnyben részesített régió
+ A régiód futárszolgálatainak előnyben részesítése
+ Nemzetközi
+ Észak-Amerika
+ Európa
+ Ázsia
+ Fehéroroszország
+ Bulgária
+ Csehország
+ Egyesült Királyság
+ Írország
+ Lengyelország
+ Magyarország
+ Németország
+ Olaszország
+ Románia
+ Skandinávia
+ Ukrajna
+ India
+ Thaiföld
+ Nemzetközi
+ Észak-Amerika
+ Európa - Fehéroroszország
+ Európa - Bulgária
+ Európa - Csehország
+ Európa - Egyesült Királyság
+ Európa - Írország
+ Európa - Lengyelország
+ Európa - Magyarország
+ Európa - Németország
+ Európa - Olaszország
+ Európa - Románia
+ Európa - Skandinávia
+ Európa - Ukrajna
+ Ázsia - India
+ Ázsia - Thaiföld
+ Egyéb
+ Válassz egy csomagot a részletek megtekintéséhez
+ Főoldal
+ Biztosan törölni szeretnéd ezt a csomagot?
Értesítés(ek) a csomag jelenlegi állapotáról
Csomag események
Jelenlegi állapot
Törlés
Futárszolgálat
Teszt mód
+ Csomag részletei
+ %s részletei
+ Követési szám
+ További információk
+ Követési előzmények
A művelet nem engedélyezett teszt módban
Minta csomagokat jelenít meg az applikációban. Nem érinti a már meglévő adatokat.
Támogatás
Szerkesztés
Csomag szerkesztése
Vissza
- A név nem lehet üres.
Figyelmen kívül hagy
Licenc
További lehetőségek
@@ -38,6 +87,8 @@
Ki kell választani egy futárszolgálatot.
Minta Posta
GLS Magyarország
+ Express One Magyarország
+ iMile
Magyar Posta
Lengyel Posta
Olasz Posta
@@ -67,4 +118,15 @@
A csomagszám nem lehet üres.
Csak korlátlan hálózaton való frissítés
A Parcel csak akkor fog frissítéseket keresni automatikusan, ha nem forgalmidíjas hálózatra van csatlakozva a készülék, pl. otthoni Wi-Fi.
+ Csomag
+ Hálózat
+ Régió
+ Fejlesztői eszközök
+ Verzió %s
+ Teszt értesítés küldése
+ DHL API kulcs
+ A DHL megköveteli, hogy a felhasználóktól API-kulcsot kérjünk. Ezt ingyenesen megszerezheted a <a href=\"https://developer.dhl.com\">DHL\'s API Developer Portal</a> és regisztrálhatsz a \"Shipment Tracking - Unified\" API-ra.
+ Teszt csomag
+ A futár felvette a csomagot
+ Műveletek
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index c4146a4..058adda 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -20,7 +20,6 @@
荷物を編集
この配送サービスには API キーが必要ですが、提供されていません。詳細はアプリの設定をご確認ください。
戻る
- 名前は空白にできません。
無視
ライセンス
その他のオプション
@@ -73,4 +72,5 @@
追跡 ID は空白にできません。
定額制ネットワークでのみ更新する
有効化すると Parcel は自宅の Wi-Fi などの定額制接続時でのみ更新を検索します。
+ 荷物
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index da00d98..f79fb9d 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -20,7 +20,6 @@
Edytuj paczkę
Ten serwis wymaga klucza API, ale żaden został podany. Sprawdż ustawienia aplikacji po więcej informacji.
Wróć
- Nazwa nie może być pusta.
Ignoruj
Licencja
Więcej opcji
@@ -89,4 +88,5 @@
Identyfikator śledzenia nie może być pusty.
Aktualizuj tylko w sieciach bez limitu
Jeśli ta opcja jest włączona, Parcel będzie szukać aktualizacji przesyłek tylko w przypadku połączeń nielimitowanych, takich jak domowe Wi-Fi.
+ Paczka
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index 7d4d4a0..6966e91 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -19,7 +19,6 @@
Redigera paket
Denna leveransservice kräver en API-nyckel men ingen angavs. Kontrollera appens inställningar för mer information.
Tillbaka
- Namn får inte lämnas tom.
Tysta
Licens
Fler alternativ
@@ -72,4 +71,5 @@
Spårningsnummer får inte lämnas tom.
Uppdatera endast på obegränsade nätverk
Om aktiverat söker Parcel efter uppdateringar endast via obegränsade anslutningar, t.ex. ditt hem-Wi-Fi.
+ Paket
diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml
index 8888227..f27f110 100644
--- a/app/src/main/res/values-th/strings.xml
+++ b/app/src/main/res/values-th/strings.xml
@@ -14,7 +14,6 @@
แก้ไข
แก้ไขพัสดุ
กลับ
- ชื่อพัสดุไม่สามารถเว้นว่างได้
ลิขสิทธิ์
ตัวเลือกเพิ่มเติม
อาจจะเป็นไปได้ว่า อุปกรณ์ของคุณไม่มีสัญญาณในขณะนี้ หรือเกิดปัญหาที่เซิฟเวอร์ฝั่งผู้ให้บริการขนส่ง หรือคุณใส่รายละเอียดของพัสดุไม่ถูกต้อง
@@ -62,4 +61,5 @@
รหัสพัสดุไม่สามารถเว้นว่างได้
อัปเดตเมื่อเชื่อมต่อเครือข่ายที่ไม่จำกัดการใช้งานเท่านั้น
เมื่อเปิด Parcel จะตรวจสอบสถานะของพัสดุเมื่อเชื่อมต่อเครือข่ายที่ไม่จำกัดการใช้งานเท่านั้น เช่น Wi-Fi บ้าน
+ พัสดุ
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index ab96fa1..c964512 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -19,7 +19,6 @@
Koli düzenle
Bu teslimat servisi bir API anahtarı gerektiriyor, ancak hiçbiri sağlanmadı. Daha fazla bilgi için uygulamanın ayarlarını kontrol edin.
Geri dön
- İsim boş bırakılamaz.
Yoksay
Lisans
Daha fazla seçenek
@@ -71,4 +70,5 @@
Takip numarası boş bırakılamaz.
Yalnızca sınırsız ağlarda güncelle
Etkinleştirildiğinde, Parcel koli güncellemelerini yalnızca ev Wi-Fi gibi sınırsız bağlantılarda kontrol eder.
+ Koli
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index e54ca18..a6ad9fe 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -19,7 +19,6 @@
Редагувати посилку
Ця доставка потребує ключ API, але він не наданий. Перевірте налаштування застосунку для більшої інформації.
Повернутись
- Ім\'я не може бути пустим.
Ігнорувати
Ліцензія
More options
@@ -71,4 +70,5 @@
Трекінг-ID не може бути пустим.
Оновлюватись тільки на необмежених мережах
Якщо ввімкнено, то Parcel буде сповіщувати про дані посилки тільки на необмежених з\'єднаннях, такі як ваш домашній Wi-Fi.
+ Посилка
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index b2fdd3f..54cf68c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,17 +1,70 @@
About app
+ Add
Add a parcel
+ Actions
Add parcel
Parcel
Archive
+ Collapse
Do you want to archive this parcel?
If you archive this parcel, its history will be preserved on device. It will not be periodically checked for updates.
+ Cancel
+ Paste from clipboard
+ Enable clipboard paste button
+ Show a paste button next to the tracking number field
+ Preferred region
+ Prioritize delivery services from your region
+ International
+ North America
+ Europe
+ Asia
+ Belarus
+ Bulgaria
+ Czech Republic
+ United Kingdom
+ Ireland
+ Poland
+ Hungary
+ Germany
+ Italy
+ Romania
+ Scandinavia
+ Ukraine
+ India
+ Thailand
+ International
+ North America
+ Europe - Belarus
+ Europe - Bulgaria
+ Europe - Czech Republic
+ Europe - United Kingdom
+ Europe - Ireland
+ Europe - Poland
+ Europe - Hungary
+ Europe - Germany
+ Europe - Italy
+ Europe - Romania
+ Europe - Scandinavia
+ Europe - Ukraine
+ Asia - India
+ Asia - Thailand
+ Other
+ Select a parcel to view its details
+ Home
+ Are you sure you want to delete this parcel?
Notifications about the current status of the parcel
+ Expand
Parcel events
Current status
Delete
Delivery service
Demo mode
+ Parcel Details
+ %s details
+ Tracking Number
+ Additional Information
+ Tracking History
Action not allowed in demo mode
Shows example parcels for demo purposes. Does not affect user data.
DHL requires us to ask users to provide an API key. You can get one for free from <a href=\"https://developer.dhl.com\">DHL\'s API Developer Portal</a> and signing up for the \"Shipment Tracking - Unified\" API.
@@ -21,7 +74,6 @@
Edit parcel
This delivery service requires an API key, but none was provided. Check the app\'s settings for more information.
Go back
- Name must not be blank.
Ignore
License
More options
@@ -49,10 +101,12 @@
You must select a service.
Evri
Example Post
+ Express One Hungary
GLS
GLS Hungary
Hermes
Hungarian Post
+ iMile
InPost
Nova Post
Orlen Paczka (Poland)
@@ -103,4 +157,13 @@
Tracking ID must not be blank.
Update only on unmetered networks
If enabled, Parcel will look for parcel updates only on unmetered connections, such as your home Wi-Fi.
+ Package
+ Network
+ Region
+ Developer tools
+ Version %s
+ Send test notification
+ DHL API key
+ Test package
+ Courier picked up the package
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 1a187b2..5f4679e 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -21,6 +21,7 @@ work = "2.10.0"
kotlinxCoroutinesGuava = "1.10.1"
material3 = "1.4.0-alpha11"
browser = "1.8.0"
+jsoup = "1.17.2"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -37,6 +38,7 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
+androidx-material3-window-size = { group = "androidx.compose.material3", name = "material3-window-size-class" }
androidx-material-icons = { group = "androidx.compose.material", name = "material-icons-core" }
moshi-kotlin-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshiKotlinCodegen" }
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
@@ -57,6 +59,7 @@ work-runtime = { group = "androidx.work", name = "work-runtime", version.ref = "
work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work" }
kotlinx-coroutines-guava = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-guava", version.ref = "kotlinxCoroutinesGuava" }
androidx-browser = { group = "androidx.browser", name = "browser", version.ref = "browser" }
+jsoup = { group = "org.jsoup", name = "jsoup", version.ref = "jsoup" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }