diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f79651041..1517abe6f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -21,8 +21,8 @@ android { applicationId = "com.sameerasw.essentials" minSdk = 26 targetSdk = 36 - versionCode = 35 - versionName = "12.4" + versionCode = 36 + versionName = "12.5" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -79,8 +79,8 @@ dependencies { // Android 12+ SplashScreen API with backward compatibility attributes implementation("androidx.core:core-splashscreen:1.0.1") - // Force latest Material3 1.5.0-alpha12 for ToggleButton & ButtonGroup support - implementation("androidx.compose.material3:material3:1.5.0-alpha12") + // Force latest Material3 1.5.0-alpha16 for new ListItem expressive overloads + implementation("androidx.compose.material3:material3:1.5.0-alpha16") implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui.graphics) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 18ac13ce5..cb35dfa8b 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -41,4 +41,13 @@ # Emoji data classes for Gson -keep class com.sameerasw.essentials.ui.ime.EmojiObject { *; } -keep class com.sameerasw.essentials.ui.ime.EmojiCategory { *; } --keep class com.sameerasw.essentials.ui.ime.EmojiDataResponse { *; } \ No newline at end of file +-keep class com.sameerasw.essentials.ui.ime.EmojiDataResponse { *; } +# Keep ViewModel constructors for reflection-based instantiation +-keepclassmembers class * extends androidx.lifecycle.ViewModel { + public (...); +} + +# Ensure anonymous TypeToken subclasses (used for GSON generic lists) are kept +-keepclassmembers class * extends com.google.gson.reflect.TypeToken { + protected (...); +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b713facb1..f6e40f793 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -573,6 +573,16 @@ + + + + + { LocationReachedSettingsUI( mainViewModel = viewModel, - modifier = Modifier.padding(top = 16.dp), + modifier = Modifier.fillMaxSize(), highlightSetting = highlightSetting ) } @@ -645,7 +637,7 @@ class FeatureSettingsActivity : AppCompatActivity() { } // Bottom padding for toolbar - if (featureId != "Quick settings tiles") { + if (featureId != "Quick settings tiles" && featureId != "Location reached") { androidx.compose.foundation.layout.Spacer(modifier = Modifier.height(150.dp)) } } diff --git a/app/src/main/java/com/sameerasw/essentials/LinkPickerActivity.kt b/app/src/main/java/com/sameerasw/essentials/LinkPickerActivity.kt index c890fd3f1..199d26b7e 100644 --- a/app/src/main/java/com/sameerasw/essentials/LinkPickerActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/LinkPickerActivity.kt @@ -16,6 +16,16 @@ class LinkPickerActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val locationViewModel = com.sameerasw.essentials.viewmodels.LocationReachedViewModel(application) + if (locationViewModel.handleIntent(intent)) { + val settingsIntent = Intent(this, FeatureSettingsActivity::class.java).apply { + putExtra("feature", "Location reached") + } + startActivity(settingsIntent) + finish() + return + } + val uri = when (intent.action) { Intent.ACTION_SEND -> { val text = intent.getStringExtra(Intent.EXTRA_TEXT) ?: "" diff --git a/app/src/main/java/com/sameerasw/essentials/MainActivity.kt b/app/src/main/java/com/sameerasw/essentials/MainActivity.kt index d82dc982e..c85a7ca7d 100644 --- a/app/src/main/java/com/sameerasw/essentials/MainActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/MainActivity.kt @@ -450,14 +450,10 @@ class MainActivity : AppCompatActivity() { Box( modifier = Modifier .fillMaxSize() - .then( - if (isBlurEnabled) { - Modifier.progressiveBlur( - blurRadius = 40f, - height = statusBarHeightPx * 1.15f, - direction = BlurDirection.TOP - ) - } else Modifier + .progressiveBlur( + blurRadius = if (isBlurEnabled) 40f else 0f, + height = statusBarHeightPx * 1.15f, + direction = BlurDirection.TOP ) ) { val currentTab = remember(tabs, currentPage) { @@ -611,14 +607,10 @@ class MainActivity : AppCompatActivity() { modifier = Modifier .scale(1f - (backProgress.value * 0.05f)) .alpha(1f - (backProgress.value * 0.3f)) - .then( - if (isBlurEnabled) { - Modifier.progressiveBlur( - blurRadius = 40f, - height = with(androidx.compose.ui.platform.LocalDensity.current) { 130.dp.toPx() }, - direction = BlurDirection.BOTTOM - ) - } else Modifier + .progressiveBlur( + blurRadius = if (isBlurEnabled) 40f else 0f, + height = with(androidx.compose.ui.platform.LocalDensity.current) { 130.dp.toPx() }, + direction = BlurDirection.BOTTOM ), label = "Tab Transition" ) { targetPage -> @@ -651,6 +643,11 @@ class MainActivity : AppCompatActivity() { startActivity(Intent(context, FeatureSettingsActivity::class.java).apply { putExtra("feature", "Freeze") }) + }, + onSettingsClick = { + startActivity(Intent(context, FeatureSettingsActivity::class.java).apply { + putExtra("feature", "Freeze") + }) } ) } diff --git a/app/src/main/java/com/sameerasw/essentials/SettingsActivity.kt b/app/src/main/java/com/sameerasw/essentials/SettingsActivity.kt index a3ba33a14..f015a3b0d 100644 --- a/app/src/main/java/com/sameerasw/essentials/SettingsActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/SettingsActivity.kt @@ -147,14 +147,10 @@ class SettingsActivity : AppCompatActivity() { modifier = Modifier .fillMaxSize() .background(MaterialTheme.colorScheme.surfaceContainer) - .then( - if (isBlurEnabled) { - Modifier.progressiveBlur( - blurRadius = 40f, - height = statusBarHeightPx * 1.15f, - direction = BlurDirection.TOP - ) - } else Modifier + .progressiveBlur( + blurRadius = if (isBlurEnabled) 40f else 0f, + height = statusBarHeightPx * 1.15f, + direction = BlurDirection.TOP ) ) { val contentPadding = androidx.compose.foundation.layout.PaddingValues( @@ -168,14 +164,10 @@ class SettingsActivity : AppCompatActivity() { viewModel = viewModel, contentPadding = contentPadding, modifier = Modifier - .then( - if (isBlurEnabled) { - Modifier.progressiveBlur( - blurRadius = 40f, - height = with(LocalDensity.current) { 150.dp.toPx() }, - direction = BlurDirection.BOTTOM - ) - } else Modifier + .progressiveBlur( + blurRadius = if (isBlurEnabled) 40f else 0f, + height = with(LocalDensity.current) { 150.dp.toPx() }, + direction = BlurDirection.BOTTOM ) ) @@ -390,13 +382,6 @@ fun SettingsContent( } - Text( - text = "Default tab", - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(start = 16.dp, top = 8.dp, bottom = 8.dp), - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - val defaultTab by viewModel.defaultTab RoundedCardContainer { val availableTabs = remember { DIYTabs.entries } @@ -768,8 +753,8 @@ fun SettingsContent( .background( color = MaterialTheme.colorScheme.surfaceBright ) - .padding(start = 12.dp, end = 12.dp, top = 12.dp, bottom = 4.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) + .padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) ) { Button( onClick = { @@ -799,19 +784,21 @@ fun SettingsContent( .background( color = MaterialTheme.colorScheme.surfaceBright ) - .padding(start = 12.dp, end = 12.dp, top = 4.dp, bottom = 12.dp), + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Button( onClick = { HapticUtil.performVirtualKeyHaptic(view) viewModel.resetOnboarding(context) - // Navigate back to main screen - (context as? ComponentActivity)?.finish() + Toast.makeText(context, "Onboarding reset", Toast.LENGTH_SHORT).show() }, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error + ) ) { - Text("Reset onboarding") + Text("Reset App Data", color = MaterialTheme.colorScheme.onError) } } diff --git a/app/src/main/java/com/sameerasw/essentials/data/repository/LocationReachedRepository.kt b/app/src/main/java/com/sameerasw/essentials/data/repository/LocationReachedRepository.kt index 8ce23e930..69368108f 100644 --- a/app/src/main/java/com/sameerasw/essentials/data/repository/LocationReachedRepository.kt +++ b/app/src/main/java/com/sameerasw/essentials/data/repository/LocationReachedRepository.kt @@ -2,26 +2,80 @@ package com.sameerasw.essentials.data.repository import android.content.Context import android.content.SharedPreferences +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken import com.sameerasw.essentials.domain.model.LocationAlarm - import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import java.util.UUID +@androidx.annotation.Keep class LocationReachedRepository(context: Context) { private val prefs: SharedPreferences = context.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE) + private val gson = Gson() companion object { private val _isProcessing = MutableStateFlow(false) val isProcessing = _isProcessing.asStateFlow() - private val _alarmFlow = MutableStateFlow(null) - val alarmFlow = _alarmFlow.asStateFlow() + private val _alarmsFlow = MutableStateFlow>(emptyList()) + val alarmsFlow = _alarmsFlow.asStateFlow() + + private val _activeAlarmId = MutableStateFlow(null) + val activeAlarmId = _activeAlarmId.asStateFlow() + + private val _tempAlarm = MutableStateFlow(null) + val tempAlarm = _tempAlarm.asStateFlow() + + private val _showBottomSheet = MutableStateFlow(false) + val showBottomSheet = _showBottomSheet.asStateFlow() + } + + fun setTempAlarm(alarm: LocationAlarm?) { + _tempAlarm.value = alarm + } + + fun setShowBottomSheet(show: Boolean) { + _showBottomSheet.value = show } init { - if (_alarmFlow.value == null) { - _alarmFlow.value = getAlarm() + migrateIfNeeded() + _alarmsFlow.value = getAlarms() + _activeAlarmId.value = getActiveAlarmId() + } + + private fun migrateIfNeeded() { + if (prefs.contains("location_reached_lat") && !prefs.contains("location_reached_alarms_json")) { + val lat = java.lang.Double.longBitsToDouble(prefs.getLong("location_reached_lat", 0L)) + val lng = java.lang.Double.longBitsToDouble(prefs.getLong("location_reached_lng", 0L)) + val radius = prefs.getInt("location_reached_radius", 1000) + val enabled = prefs.getBoolean("location_reached_enabled", false) + + if (lat != 0.0 || lng != 0.0) { + val migratedAlarm = LocationAlarm( + id = UUID.randomUUID().toString(), + name = "Migrated Destination", + latitude = lat, + longitude = lng, + radius = radius, + isEnabled = enabled + ) + saveAlarms(listOf(migratedAlarm)) + if (enabled) { + saveActiveAlarmId(migratedAlarm.id) + } + } + + // Clear old prefs + prefs.edit().apply { + remove("location_reached_lat") + remove("location_reached_lng") + remove("location_reached_radius") + remove("location_reached_enabled") + apply() + } } } @@ -29,33 +83,47 @@ class LocationReachedRepository(context: Context) { _isProcessing.value = processing } - fun saveAlarm(alarm: LocationAlarm) { - prefs.edit().apply { - putLong("location_reached_lat", java.lang.Double.doubleToRawLongBits(alarm.latitude)) - putLong("location_reached_lng", java.lang.Double.doubleToRawLongBits(alarm.longitude)) - putInt("location_reached_radius", alarm.radius) - putBoolean("location_reached_enabled", alarm.isEnabled) - apply() + fun saveAlarms(alarms: List) { + val json = gson.toJson(alarms) + prefs.edit().putString("location_reached_alarms_json", json).apply() + _alarmsFlow.value = alarms + } + + fun getAlarms(): List { + val json = prefs.getString("location_reached_alarms_json", null) ?: return emptyList() + val type = object : TypeToken>() {}.type + return try { + gson.fromJson(json, type) ?: emptyList() + } catch (e: Exception) { + emptyList() + } + } + + fun saveActiveAlarmId(id: String?) { + prefs.edit().putString("location_reached_active_id", id).apply() + _activeAlarmId.value = id + } + + fun getActiveAlarmId(): String? { + return prefs.getString("location_reached_active_id", null) + } + + fun saveLastTrip(alarm: LocationAlarm?) { + if (alarm == null) { + prefs.edit().remove("location_reached_last_trip_json").apply() + } else { + val json = gson.toJson(alarm) + prefs.edit().putString("location_reached_last_trip_json", json).apply() + } + } + + fun getLastTrip(): LocationAlarm? { + val json = prefs.getString("location_reached_last_trip_json", null) ?: return null + return try { + gson.fromJson(json, LocationAlarm::class.java) + } catch (e: Exception) { + null } - _alarmFlow.value = alarm - } - - fun getAlarm(): LocationAlarm { - val lat = java.lang.Double.longBitsToDouble( - prefs.getLong( - "location_reached_lat", - java.lang.Double.doubleToRawLongBits(0.0) - ) - ) - val lng = java.lang.Double.longBitsToDouble( - prefs.getLong( - "location_reached_lng", - java.lang.Double.doubleToRawLongBits(0.0) - ) - ) - val radius = prefs.getInt("location_reached_radius", 1000) - val enabled = prefs.getBoolean("location_reached_enabled", false) - return LocationAlarm(lat, lng, radius, enabled) } fun saveStartDistance(distance: Float) { @@ -65,4 +133,21 @@ class LocationReachedRepository(context: Context) { fun getStartDistance(): Float { return prefs.getFloat("location_reached_start_dist", 0f) } + + fun saveStartTime(time: Long) { + prefs.edit().putLong("location_reached_start_time", time).apply() + } + + fun getStartTime(): Long { + return prefs.getLong("location_reached_start_time", 0L) + } + + fun updateLastTravelled(alarmId: String, timestamp: Long) { + val alarms = getAlarms().toMutableList() + val index = alarms.indexOfFirst { it.id == alarmId } + if (index != -1) { + alarms[index] = alarms[index].copy(lastTravelled = timestamp) + saveAlarms(alarms) + } + } } diff --git a/app/src/main/java/com/sameerasw/essentials/domain/model/LocationAlarm.kt b/app/src/main/java/com/sameerasw/essentials/domain/model/LocationAlarm.kt index f94969473..bfe149fc1 100644 --- a/app/src/main/java/com/sameerasw/essentials/domain/model/LocationAlarm.kt +++ b/app/src/main/java/com/sameerasw/essentials/domain/model/LocationAlarm.kt @@ -5,8 +5,13 @@ import com.google.gson.annotations.SerializedName @Keep data class LocationAlarm( + @SerializedName("id") val id: String = java.util.UUID.randomUUID().toString(), + @SerializedName("name") val name: String = "", @SerializedName("latitude") val latitude: Double = 0.0, @SerializedName("longitude") val longitude: Double = 0.0, @SerializedName("radius") val radius: Int = 1000, // in meters - @SerializedName("isEnabled") val isEnabled: Boolean = false + @SerializedName("isEnabled") val isEnabled: Boolean = false, + @SerializedName("lastTravelled") val lastTravelled: Long? = null, + @SerializedName("iconResName") val iconResName: String = "round_navigation_24", + @SerializedName("createdAt") val createdAt: Long = System.currentTimeMillis() ) diff --git a/app/src/main/java/com/sameerasw/essentials/services/LocationReachedService.kt b/app/src/main/java/com/sameerasw/essentials/services/LocationReachedService.kt index 259597a4a..e0df7a315 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/LocationReachedService.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/LocationReachedService.kt @@ -16,6 +16,7 @@ import com.google.android.gms.location.Priority import com.sameerasw.essentials.MainActivity import com.sameerasw.essentials.R import com.sameerasw.essentials.data.repository.LocationReachedRepository +import com.sameerasw.essentials.domain.model.LocationAlarm import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -79,26 +80,36 @@ class LocationReachedService : Service() { trackingJob?.cancel() trackingJob = serviceScope.launch { while (isActive) { - val alarm = repository.getAlarm() - if (alarm.isEnabled && alarm.latitude != 0.0 && alarm.longitude != 0.0) { + val activeId = repository.getActiveAlarmId() + val alarms = repository.getAlarms() + val alarm = alarms.find { it.id == activeId } + + if (alarm != null) { updateProgress(alarm) } else { stopSelf() break } - delay(10000) // Update every 10 seconds for better responsiveness + delay(10000) } } } private fun stopTracking() { - val alarm = repository.getAlarm() - repository.saveAlarm(alarm.copy(isEnabled = false)) + val activeId = repository.getActiveAlarmId() + val alarms = repository.getAlarms() + val alarm = alarms.find { it.id == activeId } + + if (alarm != null) { + repository.saveLastTrip(alarm) + } + + repository.saveActiveAlarmId(null) stopSelf() } @android.annotation.SuppressLint("MissingPermission") - private fun updateProgress(alarm: com.sameerasw.essentials.domain.model.LocationAlarm) { + private fun updateProgress(alarm: LocationAlarm) { fusedLocationClient.getCurrentLocation(Priority.PRIORITY_BALANCED_POWER_ACCURACY, null) .addOnSuccessListener { location -> location?.let { @@ -163,19 +174,39 @@ class LocationReachedService : Service() { private fun updateNotification(distanceKm: Float) { val startDist = repository.getStartDistance() + val startTime = repository.getStartTime() val progressPercent = if (startDist > 0) { ((1.0f - (distanceKm * 1000f / startDist)) * 100).toInt().coerceIn(0, 100) } else 0 - val notification = buildOngoingNotification(distanceKm, progressPercent) + var etaText: String? = null + if (startDist > 0 && startTime > 0) { + val elapsed = System.currentTimeMillis() - startTime + val currentDistMeters = distanceKm * 1000f + val distanceTravelled = startDist - currentDistMeters + if (distanceTravelled > 0 && elapsed > 0) { + val remainingMillis = (currentDistMeters * elapsed / distanceTravelled).toLong() + val remainingMinutes = (remainingMillis / 60000).toInt().coerceAtLeast(1) + + etaText = if (remainingMinutes >= 60) { + val hrs = remainingMinutes / 60 + val mins = remainingMinutes % 60 + getString(R.string.location_reached_eta_hr_min, hrs, mins) + } else { + getString(R.string.location_reached_eta_min, remainingMinutes) + } + } + } + + val notification = buildOngoingNotification(distanceKm, progressPercent, etaText) notificationManager.notify(NOTIFICATION_ID, notification) } private fun buildInitialNotification(): Notification { - return buildOngoingNotification(null, 0) + return buildOngoingNotification(null, 0, null) } - private fun buildOngoingNotification(distanceKm: Float?, progress: Int): Notification { + private fun buildOngoingNotification(distanceKm: Float?, progress: Int, etaText: String?): Notification { val stopIntent = Intent(this, LocationReachedService::class.java).apply { action = ACTION_STOP } @@ -201,12 +232,21 @@ class LocationReachedService : Service() { else getString(R.string.location_reached_dist_km, it) } ?: getString(R.string.location_reached_calculating) - val contentText = + val contentText = if (etaText != null) { + getString(R.string.location_reached_service_remaining_with_eta, distanceText, progress, etaText) + } else { getString(R.string.location_reached_service_remaining, distanceText, progress) + } if (Build.VERSION.SDK_INT >= 35) { + val activeId = repository.getActiveAlarmId() + val alarm = repository.getAlarms().find { it.id == activeId } + val iconResName = alarm?.iconResName ?: "round_navigation_24" + val iconResId = resources.getIdentifier(iconResName, "drawable", packageName) + val finalIconId = if (iconResId != 0) iconResId else R.drawable.round_navigation_24 + val builder = Notification.Builder(this, CHANNEL_ID) - .setSmallIcon(R.drawable.rounded_navigation_24) + .setSmallIcon(finalIconId) .setContentTitle(getString(R.string.location_reached_service_title)) .setContentText(contentText) .setOngoing(true) @@ -228,8 +268,8 @@ class LocationReachedService : Service() { .setProgressTrackerIcon( Icon.createWithResource( this, - R.drawable.rounded_navigation_24 - ) + R.drawable.round_play_arrow_24 + ).setTint(getColor(android.R.color.system_accent1_300)) ) builder.style = progressStyle } catch (_: Throwable) { @@ -262,8 +302,14 @@ class LocationReachedService : Service() { return builder.build() } + val activeId = repository.getActiveAlarmId() + val alarm = repository.getAlarms().find { it.id == activeId } + val iconResName = alarm?.iconResName ?: "round_navigation_24" + val iconResId = resources.getIdentifier(iconResName, "drawable", packageName) + val finalIconId = if (iconResId != 0) iconResId else R.drawable.round_navigation_24 + val builder = NotificationCompat.Builder(this, CHANNEL_ID) - .setSmallIcon(R.drawable.rounded_navigation_24) + .setSmallIcon(finalIconId) .setContentTitle(getString(R.string.location_reached_service_title)) .setContentText(contentText) .setOngoing(true) @@ -292,11 +338,11 @@ class LocationReachedService : Service() { val channel = NotificationChannel( CHANNEL_ID, getString(R.string.location_reached_channel_name), - NotificationManager.IMPORTANCE_HIGH // Increased importance + NotificationManager.IMPORTANCE_HIGH ).apply { description = getString(R.string.location_reached_channel_desc) setShowBadge(false) - lockscreenVisibility = Notification.VISIBILITY_PUBLIC // Ensure it's visible on lockscreen + lockscreenVisibility = Notification.VISIBILITY_PUBLIC setSound(null, null) enableVibration(false) } diff --git a/app/src/main/java/com/sameerasw/essentials/services/tiles/LocationReachedTileService.kt b/app/src/main/java/com/sameerasw/essentials/services/tiles/LocationReachedTileService.kt new file mode 100644 index 000000000..591b0828b --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/services/tiles/LocationReachedTileService.kt @@ -0,0 +1,69 @@ +package com.sameerasw.essentials.services.tiles + +import android.content.Intent +import android.os.Build +import android.service.quicksettings.Tile +import androidx.annotation.RequiresApi +import com.sameerasw.essentials.FeatureSettingsActivity +import com.sameerasw.essentials.R +import com.sameerasw.essentials.data.repository.LocationReachedRepository +import com.sameerasw.essentials.services.LocationReachedService +import com.sameerasw.essentials.utils.PermissionUtils + +@RequiresApi(Build.VERSION_CODES.N) +class LocationReachedTileService : BaseTileService() { + private lateinit var repository: LocationReachedRepository + + override fun onCreate() { + super.onCreate() + repository = LocationReachedRepository(this) + } + + override fun onTileClick() { + val activeId = LocationReachedRepository.activeAlarmId.value + val alarms = repository.getAlarms() + + if (activeId != null) { + // Stop tracking + repository.saveActiveAlarmId(null) + LocationReachedService.stop(this) + } else { + // Start tracking for the last trip or the first alarm + val lastTrip = repository.getLastTrip() + val targetAlarm = lastTrip ?: alarms.firstOrNull() + + if (targetAlarm != null) { + repository.saveActiveAlarmId(targetAlarm.id) + LocationReachedService.start(this) + } else { + // No alarms to start, open settings + val intent = Intent(this, FeatureSettingsActivity::class.java).apply { + putExtra("feature", "Location reached") + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + @Suppress("DEPRECATION") + startActivityAndCollapse(intent) + } + } + } + + override fun getTileLabel(): String = getString(R.string.tile_location_reached) + + override fun getTileSubtitle(): String { + val activeId = LocationReachedRepository.activeAlarmId.value + return if (activeId != null) { + repository.getAlarms().find { it.id == activeId }?.name ?: "Tracking" + } else { + "Idle" + } + } + + override fun hasFeaturePermission(): Boolean { + return PermissionUtils.hasLocationPermission(this) && + PermissionUtils.hasBackgroundLocationPermission(this) + } + + override fun getTileState(): Int { + return if (LocationReachedRepository.activeAlarmId.value != null) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/AppFreezingActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/AppFreezingActivity.kt index 3c62f6d56..028407f26 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/activities/AppFreezingActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/AppFreezingActivity.kt @@ -4,91 +4,37 @@ import android.content.Intent import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity -import androidx.activity.compose.BackHandler import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items -import androidx.compose.foundation.lazy.grid.rememberLazyGridState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.statusBars import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.FloatingActionButtonMenu -import androidx.compose.material3.FloatingActionButtonMenuItem -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.ToggleFloatingActionButton -import androidx.compose.material3.ToggleFloatingActionButtonDefaults.animateIcon -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.rememberTopAppBarState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateMapOf -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -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.graphics.ColorFilter -import androidx.compose.ui.graphics.ColorMatrix -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.stateDescription -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.core.view.WindowCompat -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver -import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.viewmodel.compose.viewModel import com.sameerasw.essentials.FeatureSettingsActivity -import com.sameerasw.essentials.R -import com.sameerasw.essentials.domain.model.NotificationApp -import com.sameerasw.essentials.ui.components.ReusableTopAppBar -import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer +import com.sameerasw.essentials.ui.composables.FreezeGridUI +import com.sameerasw.essentials.ui.modifiers.BlurDirection +import com.sameerasw.essentials.ui.modifiers.progressiveBlur +import com.sameerasw.essentials.ui.state.LocalMenuStateManager +import com.sameerasw.essentials.ui.state.MenuStateManager import com.sameerasw.essentials.ui.theme.EssentialsTheme -import com.sameerasw.essentials.utils.FreezeManager -import com.sameerasw.essentials.utils.HapticUtil -import com.sameerasw.essentials.utils.ShortcutUtil -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext +import com.sameerasw.essentials.viewmodels.MainViewModel -@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) class AppFreezingActivity : ComponentActivity() { @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { @@ -100,336 +46,68 @@ class AppFreezingActivity : ComponentActivity() { window.isNavigationBarContrastEnforced = false } - setContent { - val viewModel: com.sameerasw.essentials.viewmodels.MainViewModel = - androidx.lifecycle.viewmodel.compose.viewModel() + val viewModel: MainViewModel = viewModel() val context = LocalContext.current + LaunchedEffect(Unit) { viewModel.check(context) + viewModel.refreshFreezePickedApps(context) } - val isPitchBlackThemeEnabled by viewModel.isPitchBlackThemeEnabled - EssentialsTheme(pitchBlackTheme = isPitchBlackThemeEnabled) { - val context = LocalContext.current - val view = LocalView.current - val pickedApps by viewModel.freezePickedApps - val isPickedAppsLoading by viewModel.isFreezePickedAppsLoading - - val gridState = rememberLazyGridState() - val scrollBehavior = - TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()) - val frozenStates = remember { mutableStateMapOf() } - val lifecycleOwner = LocalLifecycleOwner.current - - // Refresh frozen states when activity gains focus - DisposableEffect(lifecycleOwner) { - val observer = LifecycleEventObserver { _, event -> - if (event == Lifecycle.Event.ON_RESUME) { - viewModel.check(context) - viewModel.refreshFreezePickedApps(context) - } - } - lifecycleOwner.lifecycle.addObserver(observer) - onDispose { - lifecycleOwner.lifecycle.removeObserver(observer) - } - } - - LaunchedEffect(pickedApps) { - withContext(Dispatchers.IO) { - pickedApps.forEach { app -> - frozenStates[app.packageName] = - FreezeManager.isAppFrozen(context, app.packageName) - } - } - } - Scaffold( - contentWindowInsets = WindowInsets(0, 0, 0, 0), - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - containerColor = MaterialTheme.colorScheme.surfaceContainer, - topBar = { - ReusableTopAppBar( - title = getString(R.string.freeze_activity_title), - subtitle = getString(R.string.freeze_activity_subtitle), - hasBack = false, - scrollBehavior = scrollBehavior, - actions = { - IconButton(onClick = { - HapticUtil.performVirtualKeyHaptic(view) - val intent = - Intent(context, FeatureSettingsActivity::class.java).apply { - putExtra("feature", "Freeze") - } - context.startActivity(intent) - }) { - Icon( - painter = painterResource(id = R.drawable.rounded_settings_24), - contentDescription = "Settings", - tint = MaterialTheme.colorScheme.primary - ) - } - } - ) - }, - floatingActionButton = { - ExpandableFreezeFab( - modifier = Modifier.padding(bottom = 16.dp, end = 16.dp), - onUnfreezeAll = { viewModel.unfreezeAllApps(context) }, - onFreezeAll = { viewModel.freezeAllApps(context) }, - onFreezeAutomatic = { viewModel.freezeAutomaticApps(context) } - ) - } - ) { innerPadding -> - Box( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - ) { - if (isPickedAppsLoading && pickedApps.isEmpty()) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - LoadingIndicator() - } - } else if (pickedApps.isEmpty()) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Icon( - painter = painterResource(id = R.drawable.rounded_mode_cool_24), - contentDescription = null, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f) - ) - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = stringResource(R.string.msg_no_apps_frozen), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(modifier = Modifier.height(16.dp)) - androidx.compose.material3.Button( - onClick = { - HapticUtil.performVirtualKeyHaptic(view) - val intent = Intent(context, FeatureSettingsActivity::class.java).apply { - putExtra("feature", "Freeze") - } - context.startActivity(intent) - } - ) { - Text(stringResource(R.string.action_get_started)) - } - } - } else { - RoundedCardContainer( - modifier = Modifier - .padding(16.dp) - ) { - LazyVerticalGrid( - columns = GridCells.Adaptive(minSize = 88.dp), - state = gridState, - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(bottom = 88.dp), - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - items(pickedApps, key = { it.packageName }) { app -> - AppGridItem( - app = app, - isFrozen = frozenStates[app.packageName] ?: false, - isAutoFreezeEnabled = app.isEnabled, - onClick = { - HapticUtil.performVirtualKeyHaptic(view) - viewModel.launchAndUnfreezeApp( - context, - app.packageName - ) - // Finish after launch - finish() - }, - onLongClick = { - ShortcutUtil.pinAppShortcut(context, app) - } - ) - } - } - } - } - } - } - } - } - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun AppGridItem( - app: NotificationApp, - isFrozen: Boolean, - isAutoFreezeEnabled: Boolean, - onClick: () -> Unit, - onLongClick: () -> Unit -) { - val view = LocalView.current - val grayscaleMatrix = remember { ColorMatrix().apply { setToSaturation(0.4f) } } - - Surface( - shape = RoundedCornerShape(4.dp), - color = MaterialTheme.colorScheme.surfaceContainerHigh, - modifier = Modifier - .fillMaxWidth() - .combinedClickable( - onClick = { - HapticUtil.performVirtualKeyHaptic(view) - onClick() - }, - onLongClick = { - HapticUtil.performVirtualKeyHaptic(view) - onLongClick() - } - ) - ) { - Column( - modifier = Modifier.padding(12.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Box( - modifier = Modifier - .size(56.dp) - ) { - // App Icon - Image( - bitmap = app.icon, - contentDescription = app.appName, - modifier = Modifier - .fillMaxSize() - .clip(RoundedCornerShape(14.dp)), - contentScale = ContentScale.Fit, - colorFilter = if (isFrozen) ColorFilter.colorMatrix(grayscaleMatrix) else null, - alpha = if (isFrozen) 0.6f else 1f - ) + val isPitchBlackThemeEnabled by viewModel.isPitchBlackThemeEnabled + val isBlurEnabled by viewModel.isBlurEnabled - // Status Badges (Top Right) - Row( - modifier = Modifier - .align(Alignment.TopEnd) - .offset(x = 4.dp, y = (-4).dp), - horizontalArrangement = Arrangement.spacedBy((-4).dp) + EssentialsTheme(pitchBlackTheme = isPitchBlackThemeEnabled) { + CompositionLocalProvider( + LocalMenuStateManager provides remember { MenuStateManager() } ) { - // Auto-freeze Exclusion Badge (Lock) - if (!isAutoFreezeEnabled) { + Scaffold( + contentWindowInsets = WindowInsets(0, 0, 0, 0), + containerColor = MaterialTheme.colorScheme.surfaceContainer, + ) { innerPadding -> + val density = LocalDensity.current + val statusBarHeightPx = with(density) { + WindowInsets.statusBars.asPaddingValues().calculateTopPadding().toPx() + } + Box( modifier = Modifier - .size(20.dp) - .background(MaterialTheme.colorScheme.error, CircleShape) - .padding(4.dp) + .fillMaxSize() + .progressiveBlur( + blurRadius = if (isBlurEnabled) 40f else 0f, + height = statusBarHeightPx * 1.15f, + direction = BlurDirection.TOP + ) + .progressiveBlur( + blurRadius = if (isBlurEnabled) 40f else 0f, + height = with(density) { 80.dp.toPx() }, + direction = BlurDirection.BOTTOM + ) ) { - Icon( - painter = painterResource(id = R.drawable.rounded_lock_clock_24), - contentDescription = "Auto-freeze excluded", + FreezeGridUI( + viewModel = viewModel, modifier = Modifier.fillMaxSize(), - tint = MaterialTheme.colorScheme.onError + contentPadding = PaddingValues( + top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding(), + bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 130.dp, + start = 0.dp, + end = 0.dp + ), + onAppLaunched = { + finish() + }, + onSettingsClick = { + val intent = Intent(context, FeatureSettingsActivity::class.java).apply { + putExtra("feature", "Freeze") + } + context.startActivity(intent) + } ) } } } } - - Spacer(modifier = Modifier.height(10.dp)) - - Text( - text = app.appName, - style = MaterialTheme.typography.labelSmall, - textAlign = TextAlign.Center, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = if (isFrozen) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurface - ) } } } - -@OptIn(ExperimentalMaterial3ExpressiveApi::class) -@Composable -fun ExpandableFreezeFab( - modifier: Modifier = Modifier, - onUnfreezeAll: () -> Unit, - onFreezeAll: () -> Unit, - onFreezeAutomatic: () -> Unit -) { - var fabMenuExpanded by rememberSaveable { mutableStateOf(false) } - - BackHandler(fabMenuExpanded) { fabMenuExpanded = false } - - FloatingActionButtonMenu( - modifier = modifier, - expanded = fabMenuExpanded, - button = { - ToggleFloatingActionButton( - modifier = Modifier - .semantics { - stateDescription = if (fabMenuExpanded) "Expanded" else "Collapsed" - contentDescription = "Toggle menu" - }, - checked = fabMenuExpanded, - onCheckedChange = { fabMenuExpanded = !fabMenuExpanded }, - ) { - Icon( - painter = painterResource( - id = if (checkedProgress > 0.5f) R.drawable.rounded_close_24 else R.drawable.rounded_mode_cool_24 - ), - contentDescription = null, - modifier = Modifier.animateIcon({ checkedProgress }), - ) - } - }, - ) { - FloatingActionButtonMenuItem( - onClick = { - fabMenuExpanded = false - onFreezeAll() - }, - icon = { - Icon( - painterResource(id = R.drawable.rounded_mode_cool_24), - contentDescription = null - ) - }, - text = { Text(text = "Freeze All") }, - ) - FloatingActionButtonMenuItem( - onClick = { - fabMenuExpanded = false - onUnfreezeAll() - }, - icon = { - Icon( - painterResource(id = R.drawable.rounded_mode_cool_off_24), - contentDescription = null - ) - }, - text = { Text(text = "Unfreeze All") }, - ) - FloatingActionButtonMenuItem( - onClick = { - fabMenuExpanded = false - onFreezeAutomatic() - }, - icon = { - Icon( - painterResource(id = R.drawable.rounded_nest_farsight_cool_24), - contentDescription = null - ) - }, - text = { Text(text = "Freeze Automatic") }, - ) - } -} diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/LocationAlarmActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/LocationAlarmActivity.kt index e96eaecae..8fd66769f 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/activities/LocationAlarmActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/LocationAlarmActivity.kt @@ -145,8 +145,14 @@ class LocationAlarmActivity : ComponentActivity() { // Disable alarm in repo val repo = LocationReachedRepository(this) - val alarm = repo.getAlarm() - repo.saveAlarm(alarm.copy(isEnabled = false)) + val activeId = repo.getActiveAlarmId() + val alarms = repo.getAlarms() + val alarm = alarms.find { it.id == activeId } + + if (alarm != null) { + repo.saveLastTrip(alarm) + } + repo.saveActiveAlarmId(null) // Stop the progress service LocationReachedService.stop(this) diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/QSPreferencesActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/QSPreferencesActivity.kt index e4b958021..5747fb51d 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/activities/QSPreferencesActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/QSPreferencesActivity.kt @@ -85,6 +85,7 @@ class QSPreferencesActivity : ComponentActivity() { "com.sameerasw.essentials.services.tiles.BatteryNotificationTileService" -> "Battery notification" "com.sameerasw.essentials.services.tiles.ChargeQuickTileService" -> "Quick settings tiles" "com.sameerasw.essentials.services.tiles.AlwaysOnDisplayTileService" -> "Always on Display" + "com.sameerasw.essentials.services.tiles.LocationReachedTileService" -> "Location reached" else -> null } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/YourAndroidActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/YourAndroidActivity.kt index 6f5bff2f4..443f1e1b9 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/activities/YourAndroidActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/YourAndroidActivity.kt @@ -224,14 +224,10 @@ class YourAndroidActivity : ComponentActivity() { modifier = Modifier .fillMaxSize() .background(MaterialTheme.colorScheme.surfaceContainer) - .then( - if (isBlurEnabled) { - Modifier.progressiveBlur( - blurRadius = 40f, - height = statusBarHeightPx * 1.15f, - direction = BlurDirection.TOP - ) - } else Modifier + .progressiveBlur( + blurRadius = if (isBlurEnabled) 40f else 0f, + height = statusBarHeightPx * 1.15f, + direction = BlurDirection.TOP ) ) { androidx.compose.material3.pulltorefresh.PullToRefreshBox( @@ -323,14 +319,10 @@ fun YourAndroidContent( Column( modifier = modifier .fillMaxSize() - .then( - if (isBlurEnabled) { - Modifier.progressiveBlur( - blurRadius = 40f, - height = with(LocalDensity.current) { 150.dp.toPx() }, - direction = BlurDirection.BOTTOM - ) - } else Modifier + .progressiveBlur( + blurRadius = if (isBlurEnabled) 40f else 0f, + height = with(LocalDensity.current) { 150.dp.toPx() }, + direction = BlurDirection.BOTTOM ) .verticalScroll(rememberScrollState()) .padding( diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/DeviceHeroCard.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/DeviceHeroCard.kt index 32f08e696..6ad7e155d 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/DeviceHeroCard.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/DeviceHeroCard.kt @@ -34,11 +34,34 @@ import coil.compose.AsyncImage import com.sameerasw.essentials.R import com.sameerasw.essentials.data.model.DeviceSpecs import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer +import android.content.ComponentName +import android.content.Intent +import android.os.Build +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.width +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.Surface +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.window.Dialog +import coil.ImageLoader +import coil.decode.GifDecoder +import coil.decode.ImageDecoderDecoder +import coil.request.ImageRequest import com.sameerasw.essentials.ui.theme.Shapes import com.sameerasw.essentials.ui.components.modifiers.shimmer import com.sameerasw.essentials.utils.DeviceInfo import com.sameerasw.essentials.utils.DeviceUtils import com.sameerasw.essentials.utils.DeviceImageMapper +import com.sameerasw.essentials.utils.HapticUtil @Composable fun DeviceHeroCard( @@ -49,9 +72,35 @@ fun DeviceHeroCard( contentOffset: () -> Dp = { 0.dp }, modifier: Modifier = Modifier ) { + val context = LocalContext.current + val view = LocalView.current val imageUrls = deviceSpecs?.imageUrls ?: emptyList() val isPixel = deviceInfo.manufacturer.contains("Google", ignoreCase = true) + var showFlashbangDialog by remember { mutableStateOf(false) } + + val launchIntent = { packageName: String, className: String -> + try { + val intent = Intent(Intent.ACTION_MAIN).apply { + component = ComponentName(packageName, className) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity(intent) + } catch (e: Exception) { + + } + } + + if (showFlashbangDialog) { + FlashbangDialog( + onDismiss = { showFlashbangDialog = false }, + onContinue = { + showFlashbangDialog = false + launchIntent("com.google.android.apps.diagnosticstool", "com.google.android.apps.diagnosticstool.login.EndUserLoginActivity") + } + ) + } + // Only show the illustration page if it's a Pixel AND we have a mapping val illustrationRes = DeviceImageMapper.getDeviceDrawable(deviceInfo.model) val showIllustration = isPixel && illustrationRes != 0 @@ -310,6 +359,141 @@ fun DeviceHeroCard( } } } + } + + if (isPixel) { + Column( + modifier = Modifier + .fillMaxWidth() + .background( + MaterialTheme.colorScheme.surfaceBright, + shape = Shapes.extraSmall + ) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + PixelToolButton( + iconRes = R.drawable.rounded_diagnosis_24, + label = stringResource(id = R.string.label_diagnostics), + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + launchIntent("com.android.devicediagnostics", "com.android.devicediagnostics.MainActivity") + } + ) + PixelToolButton( + iconRes = R.drawable.rounded_search_check_2_24, + label = stringResource(id = R.string.label_device_check), + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + showFlashbangDialog = true + } + ) + } + } + } +} + +@Composable +private fun PixelToolButton( + iconRes: Int, + label: String, + onClick: () -> Unit +) { + androidx.compose.material3.Button( + onClick = onClick, + modifier = Modifier + .fillMaxWidth() + .height(56.dp), + contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 16.dp) + ) { + Icon( + painter = painterResource(id = iconRes), + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = label, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) } } + +@Composable +private fun FlashbangDialog( + onDismiss: () -> Unit, + onContinue: () -> Unit +) { + val context = LocalContext.current + val view = LocalView.current + val imageLoader = remember { + ImageLoader.Builder(context) + .components { + if (Build.VERSION.SDK_INT >= 28) { + add(ImageDecoderDecoder.Factory()) + } else { + add(GifDecoder.Factory()) + } + } + .build() + } + + androidx.compose.material3.AlertDialog( + onDismissRequest = onDismiss, + title = { + Text( + text = stringResource(id = R.string.label_device_check), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + }, + text = { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(id = R.string.msg_flashbang), + style = MaterialTheme.typography.bodyLarge, + textAlign = androidx.compose.ui.text.style.TextAlign.Center + ) + AsyncImage( + model = ImageRequest.Builder(context) + .data(R.drawable.flashbang) + .build(), + imageLoader = imageLoader, + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .clip(MaterialTheme.shapes.medium), + contentScale = ContentScale.Crop + ) + } + }, + confirmButton = { + androidx.compose.material3.Button( + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + onContinue() + } + ) { + Text(text = stringResource(id = R.string.action_continue)) + } + }, + dismissButton = { + androidx.compose.material3.OutlinedButton( + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + onDismiss() + } + ) { + Text(text = stringResource(id = R.string.action_abort)) + } + } + ) +} diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/EssentialsFloatingToolbar.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/EssentialsFloatingToolbar.kt index 88f2eb504..5331d4e6b 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/EssentialsFloatingToolbar.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/EssentialsFloatingToolbar.kt @@ -10,6 +10,8 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -48,6 +50,15 @@ fun EssentialsFloatingToolbar( expanded: Boolean = true ) { val view = LocalView.current + val configuration = LocalConfiguration.current + val fontScale = LocalDensity.current.fontScale + val screenWidth = configuration.screenWidthDp + + // Hide label if font scale is large or screen width is too small + val isLargeFont = fontScale > 1.25f + val isCompactScreen = screenWidth < 400 + + val shouldHideLabel = isLargeFont || (isCompactScreen && items.size > 3) val finalFab: (@Composable () -> Unit)? = when { floatingActionButton != null -> floatingActionButton @@ -151,7 +162,7 @@ fun EssentialsFloatingToolbar( ) val labelWidth by animateDpAsState( - targetValue = if (isSelected) 80.dp else 0.dp, + targetValue = if (isSelected && !shouldHideLabel) 80.dp else 0.dp, animationSpec = spring( dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow @@ -217,7 +228,7 @@ fun EssentialsFloatingToolbar( } } } - if (isSelected) { + if (isSelected && !shouldHideLabel) { Spacer(modifier = Modifier.width(8.dp)) Text( text = stringResource(id = item.labelRes), diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/LocationIconPicker.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/LocationIconPicker.kt new file mode 100644 index 000000000..7e4c7183e --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/LocationIconPicker.kt @@ -0,0 +1,94 @@ +package com.sameerasw.essentials.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.material3.carousel.HorizontalMultiBrowseCarousel +import androidx.compose.material3.carousel.rememberCarouselState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.sameerasw.essentials.R +import com.sameerasw.essentials.utils.HapticUtil + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LocationIconPicker( + selectedIconName: String, + onIconSelected: (String) -> Unit, + modifier: Modifier = Modifier +) { + val icons = listOf( + "round_navigation_24", + "rounded_home_24", + "rounded_work_24", + "rounded_apartment_24", + "rounded_shopping_cart_24", + "rounded_school_24", + "rounded_storefront_24", + "rounded_fork_spoon_24", + "rounded_favorite_24", + "rounded_account_balance_24", + "rounded_garage_home_24", + "rounded_beach_access_24", + "rounded_local_pizza_24", + "rounded_train_24", + "rounded_directions_bus_24", + "rounded_flight_24", + "rounded_directions_boat_24" + ) + + val carouselState = rememberCarouselState { icons.size } + val context = LocalContext.current + val view = androidx.compose.ui.platform.LocalView.current + + Column(modifier = modifier.fillMaxWidth()) { + Text( + text = "Pick an icon", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(bottom = 8.dp) + ) + + HorizontalMultiBrowseCarousel( + state = carouselState, + preferredItemWidth = 64.dp, + itemSpacing = 4.dp, + contentPadding = PaddingValues(horizontal = 0.dp), + modifier = Modifier + .fillMaxWidth() + .height(64.dp) + ) { index -> + val iconName = icons[index] + val isSelected = iconName == selectedIconName + val iconResId = context.resources.getIdentifier(iconName, "drawable", context.packageName) + + Box( + modifier = Modifier + .fillMaxSize() + .maskClip(MaterialTheme.shapes.medium) + .background( + if (isSelected) MaterialTheme.colorScheme.primaryContainer + else MaterialTheme.colorScheme.background + ) + .clickable { + HapticUtil.performVirtualKeyHaptic(view) + onIconSelected(iconName) + }, + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(id = if (iconResId != 0) iconResId else R.drawable.round_navigation_24), + contentDescription = null, + tint = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer + else MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(28.dp) + ) + } + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/cards/AppToggleItem.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/cards/AppToggleItem.kt index 5182a2b4c..7d7aee2d5 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/cards/AppToggleItem.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/cards/AppToggleItem.kt @@ -66,6 +66,7 @@ private val GOOGLE_SYSTEM_USER_APPS = setOf( "com.google.android.cellbroadcastreceiver" ) +@OptIn(androidx.compose.material3.ExperimentalMaterial3ExpressiveApi::class) @Composable fun AppToggleItem( icon: ImageBitmap?, @@ -85,42 +86,67 @@ fun AppToggleItem( isSystemApp || (packageName != null && GOOGLE_SYSTEM_USER_APPS.contains(packageName)) } - Row( - modifier = modifier - .fillMaxWidth() - .background( - color = MaterialTheme.colorScheme.surfaceBright, - shape = RoundedCornerShape(MaterialTheme.shapes.extraSmall.bottomEnd) - ) - .clickable(enabled = !showToggle && enabled) { + val onClickAction = { + if (enabled) { + HapticUtil.performVirtualKeyHaptic(view) + onCheckedChange(!isChecked) + } else if (onDisabledClick != null) { + HapticUtil.performVirtualKeyHaptic(view) + onDisabledClick() + } + } + + if (showToggle) { + androidx.compose.material3.ListItem( + checked = isChecked && enabled, + onCheckedChange = { checked -> if (enabled) { HapticUtil.performVirtualKeyHaptic(view) - onCheckedChange(!isChecked) + onCheckedChange(checked) } else if (onDisabledClick != null) { HapticUtil.performVirtualKeyHaptic(view) onDisabledClick() } - } - .padding(12.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - Spacer(modifier = Modifier.size(2.dp)) - if (icon != null) { - Image( - bitmap = icon, - contentDescription = title, - modifier = Modifier.size(24.dp), - contentScale = ContentScale.Fit - ) - } else { - // Fallback placeholder if needed, or just space - Spacer(modifier = Modifier.size(24.dp)) - } - Spacer(modifier = Modifier.size(2.dp)) - - if (description != null) { - Column(modifier = Modifier.weight(1f)) { + }, + enabled = enabled, + modifier = modifier.fillMaxWidth(), + verticalAlignment = androidx.compose.ui.Alignment.CenterVertically, + leadingContent = { + if (icon != null) { + Image( + bitmap = icon, + contentDescription = title, + modifier = Modifier.size(32.dp), + contentScale = ContentScale.Fit + ) + } else { + Spacer(modifier = Modifier.size(32.dp)) + } + }, + supportingContent = if (description != null) { + { + Text( + text = description, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } else null, + trailingContent = { + Switch( + checked = if (enabled) isChecked else false, + onCheckedChange = null, // Handled by ListItem + enabled = enabled + ) + }, + colors = androidx.compose.material3.ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.surfaceBright + ), + contentPadding = androidx.compose.foundation.layout.PaddingValues( + horizontal = 16.dp, + vertical = 16.dp + ), + content = { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp) @@ -149,66 +175,72 @@ fun AppToggleItem( } } } - Text( - text = description, - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) } - } else { - Row( - modifier = Modifier.weight(1f), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - Text( - text = title, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface - ) - if (shouldShowSystemTag) { - Box( - modifier = Modifier - .size(18.dp) - .background( - color = MaterialTheme.colorScheme.primary, - shape = CircleShape - ), - contentAlignment = Alignment.Center - ) { - androidx.compose.material3.Icon( - painter = painterResource(id = R.drawable.round_android_24), - contentDescription = null, - modifier = Modifier.size(12.dp), - tint = MaterialTheme.colorScheme.surfaceBright - ) - } + ) + } else { + androidx.compose.material3.ListItem( + onClick = onClickAction, + enabled = enabled, + modifier = modifier.fillMaxWidth(), + verticalAlignment = androidx.compose.ui.Alignment.CenterVertically, + leadingContent = { + if (icon != null) { + Image( + bitmap = icon, + contentDescription = title, + modifier = Modifier.size(24.dp), + contentScale = ContentScale.Fit + ) + } else { + Spacer(modifier = Modifier.size(24.dp)) } - } - } - - if (showToggle) { - Box { - Switch( - checked = if (enabled) isChecked else false, - onCheckedChange = { checked -> - if (enabled) { - HapticUtil.performVirtualKeyHaptic(view) - onCheckedChange(checked) + }, + supportingContent = if (description != null) { + { + Text( + text = description, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } else null, + colors = androidx.compose.material3.ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.surfaceBright + ), + contentPadding = androidx.compose.foundation.layout.PaddingValues( + horizontal = 16.dp, + vertical = 16.dp + ), + content = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = title, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface + ) + if (shouldShowSystemTag) { + Box( + modifier = Modifier + .size(18.dp) + .background( + color = MaterialTheme.colorScheme.primary, + shape = CircleShape + ), + contentAlignment = Alignment.Center + ) { + androidx.compose.material3.Icon( + painter = painterResource(id = R.drawable.round_android_24), + contentDescription = null, + modifier = Modifier.size(12.dp), + tint = MaterialTheme.colorScheme.surfaceBright + ) } - }, - enabled = enabled - ) - - if (!enabled && onDisabledClick != null) { - Box(modifier = Modifier - .matchParentSize() - .clickable { - HapticUtil.performVirtualKeyHaptic(view) - onDisabledClick() - }) + } } } - } + ) } } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/cards/FeatureCard.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/cards/FeatureCard.kt index dc2537997..38ed098fe 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/cards/FeatureCard.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/cards/FeatureCard.kt @@ -20,6 +20,10 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.Spacer import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -40,6 +44,7 @@ import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenuItem import com.sameerasw.essentials.utils.ColorUtil import com.sameerasw.essentials.utils.HapticUtil +@OptIn(androidx.compose.material3.ExperimentalMaterial3ExpressiveApi::class) @Composable fun FeatureCard( title: Any, // Can be Int (Resource ID) or String @@ -90,122 +95,73 @@ fun FeatureCard( label = "alpha" ) - Card( - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceBright - ), - shape = MaterialTheme.shapes.extraSmall, + val resolvedTitle = when (title) { + is Int -> stringResource(id = title) + is String -> title + else -> "" + } + + androidx.compose.material3.ListItem( + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + onClick() + }, + onLongClick = { + HapticUtil.performVirtualKeyHaptic(view) + showMenu = true + }, + verticalAlignment = androidx.compose.ui.Alignment.CenterVertically, modifier = modifier .alpha(alpha) - .combinedClickable( - onClick = { - HapticUtil.performVirtualKeyHaptic(view) - onClick() - }, - onLongClick = { - HapticUtil.performVirtualKeyHaptic(view) - showMenu = true + .blur(blurRadius), + leadingContent = if (iconRes != null) { + { + Box( + modifier = Modifier + .size(40.dp) + .background( + color = ColorUtil.getPastelColorFor(resolvedTitle), + shape = CircleShape + ), + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(id = iconRes), + contentDescription = resolvedTitle, + modifier = Modifier.size(24.dp), + tint = ColorUtil.getVibrantColorFor(resolvedTitle) + ) } - )) { - Box( - modifier = Modifier - .fillMaxWidth() - .blur(blurRadius) - .padding(16.dp) - ) { - - val resolvedTitle = when (title) { - is Int -> stringResource(id = title) - is String -> title - else -> "" } - - Row( - modifier = Modifier.align(Alignment.CenterStart), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(10.dp) - ) { - if (iconRes != null) { - Box( - modifier = Modifier - .size(40.dp) - .background( - color = ColorUtil.getPastelColorFor(resolvedTitle), - shape = CircleShape - ), - contentAlignment = Alignment.Center - ) { - Icon( - painter = painterResource(id = iconRes), - contentDescription = resolvedTitle, - modifier = Modifier.size(24.dp), - tint = ColorUtil.getVibrantColorFor(resolvedTitle) - ) - } - } - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(2.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text( - text = resolvedTitle, - color = MaterialTheme.colorScheme.onSurface - ) - if (isBeta) { - Card( - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.background - ), - shape = MaterialTheme.shapes.extraSmall - ) { - Text( - text = stringResource(R.string.label_beta), - modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp), - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.primary - ) - } - } - } - if (descriptionOverride != null) { - Text( - text = descriptionOverride, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } else if (description != null) { - val resolvedDescription = when (description) { - is Int -> stringResource(id = description) - is String -> description - else -> "" - } - Text( - text = resolvedDescription, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } + } else null, + supportingContent = if (descriptionOverride != null || description != null) { + { + val desc = descriptionOverride ?: description + val resolvedDescription = when (desc) { + is Int -> stringResource(id = desc) + is String -> desc + else -> "" } + Text( + text = resolvedDescription, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) } - + } else null, + trailingContent = { Row( - modifier = Modifier.align(Alignment.CenterEnd), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.End ) { - if (hasMoreSettings) { - Icon( + if (showToggle && hasMoreSettings) { + VerticalDivider( modifier = Modifier - .padding(end = 12.dp) - .size(24.dp), - painter = painterResource(id = R.drawable.rounded_chevron_right_24), - contentDescription = "More settings", - tint = MaterialTheme.colorScheme.onSurfaceVariant + .height(32.dp) + .width(1.dp), + color = MaterialTheme.colorScheme.outlineVariant ) + Spacer(modifier = Modifier.width(16.dp)) } if (showToggle) { @@ -222,7 +178,6 @@ fun FeatureCard( ) if (!isToggleEnabled && onDisabledToggleClick != null) { - // Invisible overlay catches taps even if the child consumes them Box(modifier = Modifier .matchParentSize() .clickable { @@ -233,6 +188,39 @@ fun FeatureCard( } } } + }, + colors = androidx.compose.material3.ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.surfaceBright + ), + contentPadding = androidx.compose.foundation.layout.PaddingValues( + horizontal = 16.dp, + vertical = 16.dp + ), + content = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = resolvedTitle, + color = MaterialTheme.colorScheme.onSurface + ) + if (isBeta) { + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.background + ), + shape = MaterialTheme.shapes.extraSmall + ) { + Text( + text = stringResource(R.string.label_beta), + modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.primary + ) + } + } + } SegmentedDropdownMenu( expanded = showMenu, @@ -279,5 +267,5 @@ fun FeatureCard( } } } - } + ) } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/cards/IconToggleItem.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/cards/IconToggleItem.kt index 2e80a78b5..88a23b444 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/cards/IconToggleItem.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/cards/IconToggleItem.kt @@ -20,9 +20,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource +import androidx.compose.material3.VerticalDivider import androidx.compose.ui.unit.dp +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.height import com.sameerasw.essentials.utils.HapticUtil +@OptIn(androidx.compose.material3.ExperimentalMaterial3ExpressiveApi::class) @Composable fun IconToggleItem( iconRes: Int, @@ -33,84 +37,184 @@ fun IconToggleItem( onCheckedChange: (Boolean) -> Unit, enabled: Boolean = true, onDisabledClick: (() -> Unit)? = null, - showToggle: Boolean = true + showToggle: Boolean = true, + onClick: (() -> Unit)? = null ) { val view = LocalView.current - Row( - modifier = modifier - .fillMaxWidth() - .background( - color = MaterialTheme.colorScheme.surfaceBright, - shape = RoundedCornerShape(MaterialTheme.shapes.extraSmall.bottomEnd) + val onClickAction = { + if (enabled) { + HapticUtil.performVirtualKeyHaptic(view) + onCheckedChange(!isChecked) + } else if (onDisabledClick != null) { + HapticUtil.performVirtualKeyHaptic(view) + onDisabledClick() + } + } + + if (showToggle) { + if (onClick != null) { + androidx.compose.material3.ListItem( + onClick = { + if (enabled) { + HapticUtil.performVirtualKeyHaptic(view) + onClick() + } else if (onDisabledClick != null) { + HapticUtil.performVirtualKeyHaptic(view) + onDisabledClick() + } + }, + enabled = enabled, + modifier = modifier.fillMaxWidth(), + verticalAlignment = androidx.compose.ui.Alignment.CenterVertically, + leadingContent = { + Icon( + painter = painterResource(id = iconRes), + contentDescription = title, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.primary + ) + }, + contentPadding = androidx.compose.foundation.layout.PaddingValues( + horizontal = 16.dp, + vertical = 16.dp + ), + supportingContent = if (description != null) { + { + Text( + text = description, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } else null, + trailingContent = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + VerticalDivider( + modifier = Modifier + .height(32.dp) + .width(1.dp), + color = MaterialTheme.colorScheme.outlineVariant + ) + Switch( + checked = if (enabled) isChecked else false, + onCheckedChange = { checked -> + if (enabled) { + HapticUtil.performVirtualKeyHaptic(view) + onCheckedChange(checked) + } + }, + enabled = enabled + ) + } + }, + colors = androidx.compose.material3.ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.surfaceBright + ), + content = { + Text( + text = title, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface + ) + } ) - .clickable(enabled = !showToggle && enabled) { - if (enabled) { - HapticUtil.performVirtualKeyHaptic(view) - onCheckedChange(!isChecked) - } else if (onDisabledClick != null) { - HapticUtil.performVirtualKeyHaptic(view) - onDisabledClick() + } else { + androidx.compose.material3.ListItem( + checked = isChecked && enabled, + onCheckedChange = { checked -> + if (enabled) { + HapticUtil.performVirtualKeyHaptic(view) + onCheckedChange(checked) + } else if (onDisabledClick != null) { + HapticUtil.performVirtualKeyHaptic(view) + onDisabledClick() + } + }, + enabled = enabled, + modifier = modifier.fillMaxWidth(), + verticalAlignment = androidx.compose.ui.Alignment.CenterVertically, + leadingContent = { + Icon( + painter = painterResource(id = iconRes), + contentDescription = title, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.primary + ) + }, + contentPadding = androidx.compose.foundation.layout.PaddingValues( + horizontal = 16.dp, + vertical = 16.dp + ), + supportingContent = if (description != null) { + { + Text( + text = description, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } else null, + trailingContent = { + Switch( + checked = if (enabled) isChecked else false, + onCheckedChange = null, // Handled by ListItem + enabled = enabled + ) + }, + colors = androidx.compose.material3.ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.surfaceBright + ), + content = { + Text( + text = title, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface + ) } - } - .padding(12.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - Spacer(modifier = Modifier.size(2.dp)) - Icon( - painter = painterResource(id = iconRes), - contentDescription = title, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.size(2.dp)) - - if (description != null) { - Column(modifier = Modifier.weight(1f)) { + ) + } + } else { + androidx.compose.material3.ListItem( + onClick = onClickAction, + enabled = enabled, + modifier = modifier.fillMaxWidth(), + verticalAlignment = androidx.compose.ui.Alignment.CenterVertically, + leadingContent = { + Icon( + painter = painterResource(id = iconRes), + contentDescription = title, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.primary + ) + }, + contentPadding = androidx.compose.foundation.layout.PaddingValues( + horizontal = 16.dp, + vertical = 16.dp + ), + supportingContent = if (description != null) { + { + Text( + text = description, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } else null, + colors = androidx.compose.material3.ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.surfaceBright + ), + content = { Text( text = title, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface ) - Text( - text = description, - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } else { - Text( - text = title, - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.weight(1f), - color = MaterialTheme.colorScheme.onSurface - ) - } - - if (showToggle) { - Box { - Switch( - checked = if (enabled) isChecked else false, - onCheckedChange = { checked -> - if (enabled) { - HapticUtil.performVirtualKeyHaptic(view) - onCheckedChange(checked) - } - }, - enabled = enabled - ) - - if (!enabled && onDisabledClick != null) { - Box(modifier = Modifier - .matchParentSize() - .clickable { - HapticUtil.performVirtualKeyHaptic(view) - onDisabledClick() - }) - } } - } + ) } } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/cards/LocationAlarmCard.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/cards/LocationAlarmCard.kt new file mode 100644 index 000000000..1094169ff --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/cards/LocationAlarmCard.kt @@ -0,0 +1,106 @@ +package com.sameerasw.essentials.ui.components.cards + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.sameerasw.essentials.R +import com.sameerasw.essentials.domain.model.LocationAlarm + +@Composable +fun LocationAlarmCard( + alarm: LocationAlarm, + isActive: Boolean, + isAnyTracking: Boolean, + onStart: () -> Unit, + onStop: () -> Unit, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val view = androidx.compose.ui.platform.LocalView.current + + ListItem( + modifier = modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surfaceBright, MaterialTheme.shapes.extraSmall) + .clickable { + com.sameerasw.essentials.utils.HapticUtil.performVirtualKeyHaptic(view) + onClick() + }, + leadingContent = { + val context = androidx.compose.ui.platform.LocalContext.current + val iconResId = context.resources.getIdentifier(alarm.iconResName, "drawable", context.packageName) + Icon( + painter = painterResource(id = if (iconResId != 0) iconResId else R.drawable.round_navigation_24), + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(24.dp) + ) + }, + headlineContent = { + Text( + text = alarm.name.ifEmpty { "Destination" }, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + }, + supportingContent = { + val context = androidx.compose.ui.platform.LocalContext.current + val lastTravelledText = alarm.lastTravelled?.let { + stringResource(R.string.location_reached_last_travelled, com.sameerasw.essentials.utils.TimeUtil.formatRelativeDate(it, context)) + } ?: stringResource(R.string.location_reached_never) + + Text( + text = lastTravelledText, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + }, + trailingContent = { + if (isActive) { + IconButton( + onClick = { + com.sameerasw.essentials.utils.HapticUtil.performVirtualKeyHaptic(view) + onStop() + }, + colors = IconButtonDefaults.iconButtonColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.error + ) + ) { + Icon( + painter = painterResource(R.drawable.rounded_close_24), + contentDescription = "Stop" + ) + } + } else if (!isAnyTracking) { + IconButton( + onClick = { + com.sameerasw.essentials.utils.HapticUtil.performVirtualKeyHaptic(view) + onStart() + }, + colors = IconButtonDefaults.iconButtonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ), + modifier = Modifier.size(48.dp) + ) { + Icon( + painter = painterResource(R.drawable.rounded_play_arrow_24), + contentDescription = "Start" + ) + } + } + }, + colors = ListItemDefaults.colors(containerColor = Color.Transparent) + ) +} diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/cards/PermissionCard.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/cards/PermissionCard.kt index 0f7431413..9892a0776 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/cards/PermissionCard.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/cards/PermissionCard.kt @@ -1,6 +1,7 @@ package com.sameerasw.essentials.ui.components.cards 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 @@ -10,8 +11,10 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.Button import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text @@ -26,7 +29,7 @@ import androidx.compose.ui.unit.dp import com.sameerasw.essentials.R import com.sameerasw.essentials.utils.HapticUtil -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(androidx.compose.material3.ExperimentalMaterial3ExpressiveApi::class) @Composable fun PermissionCard( iconRes: Int, @@ -42,141 +45,144 @@ fun PermissionCard( val grantedGreen = Color(0xFF4CAF50) val view = LocalView.current - Card(modifier = modifier.fillMaxWidth(), shape = MaterialTheme.shapes.extraSmall) { - Column( - modifier = Modifier.padding(12.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - - Spacer(modifier = Modifier.size(12.dp)) - - Icon( - painter = painterResource(id = iconRes), - contentDescription = null, - tint = if (isGranted) grantedGreen else MaterialTheme.colorScheme.primary, - modifier = Modifier.size(36.dp) - ) + val resolvedTitle = when (title) { + is Int -> stringResource(id = title) + is String -> title + else -> "" + } - Spacer(modifier = Modifier.size(24.dp)) + val resolvedActionLabel = when (actionLabel) { + is Int -> stringResource(id = actionLabel) + is String -> actionLabel + else -> "" + } - Column(modifier = Modifier.weight(1f)) { - Row(verticalAlignment = Alignment.CenterVertically) { - val resolvedTitle = when (title) { - is Int -> stringResource(id = title) - is String -> title - else -> "" - } - Text(text = resolvedTitle, style = MaterialTheme.typography.titleMedium) - } + val resolvedSecondaryLabel = when (secondaryActionLabel) { + is Int -> stringResource(id = secondaryActionLabel as Int) + is String -> secondaryActionLabel + else -> null + } - Spacer(modifier = Modifier.height(4.dp)) - Text(text = "Required for:", style = MaterialTheme.typography.bodySmall) - Spacer(modifier = Modifier.height(4.dp)) - // Bulleted list of dependent features - dependentFeatures.forEach { f -> - val resolvedFeature = when (f) { - is Int -> stringResource(id = f) - is String -> f - else -> "" + Card( + modifier = modifier.fillMaxWidth(), + shape = MaterialTheme.shapes.extraSmall, + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceBright + ) + ) { + Column( + modifier = Modifier.padding(bottom = 12.dp, start = 4.dp, end = 4.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + ListItem( + onClick = {}, + modifier = Modifier.fillMaxWidth(), + verticalAlignment = androidx.compose.ui.Alignment.CenterVertically, + leadingContent = { + Icon( + painter = painterResource(id = iconRes), + contentDescription = null, + tint = if (isGranted) grantedGreen else MaterialTheme.colorScheme.primary, + modifier = Modifier.size(36.dp) + ) + }, + supportingContent = { + Column { + Text(text = "Required for:", style = MaterialTheme.typography.bodySmall) + Spacer(modifier = Modifier.height(4.dp)) + dependentFeatures.forEach { f -> + val resolvedFeature = when (f) { + is Int -> stringResource(id = f) + is String -> f + else -> "" + } + Text( + text = "• $resolvedFeature", + style = MaterialTheme.typography.bodyMedium + ) } - Text( - text = "• $resolvedFeature", - style = MaterialTheme.typography.bodyMedium - ) } + }, + colors = androidx.compose.material3.ListItemDefaults.colors( + containerColor = Color.Transparent + ), + contentPadding = androidx.compose.foundation.layout.PaddingValues( + horizontal = 16.dp, + vertical = 16.dp + ), + content = { + Text(text = resolvedTitle, style = MaterialTheme.typography.titleMedium) } - } - - val resolvedActionLabel = when (actionLabel) { - is Int -> stringResource(id = actionLabel) - is String -> actionLabel - else -> "" - } + ) - val resolvedSecondaryLabel = when (secondaryActionLabel) { - is Int -> stringResource(id = secondaryActionLabel as Int) - is String -> secondaryActionLabel - else -> null - } - - if (isGranted) { - if (resolvedSecondaryLabel != null && onSecondaryActionClick != null) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - OutlinedButton( - onClick = { - HapticUtil.performVirtualKeyHaptic(view) - onActionClick() - }, - modifier = Modifier.weight(1f) + Box(modifier = Modifier.padding(horizontal = 12.dp)) { + if (isGranted) { + if (resolvedSecondaryLabel != null && onSecondaryActionClick != null) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - Text(resolvedActionLabel) - } + OutlinedButton( + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + onActionClick() + }, + modifier = Modifier.weight(1f) + ) { + Text(resolvedActionLabel) + } - Button( - onClick = { - HapticUtil.performVirtualKeyHaptic(view) - onSecondaryActionClick() - }, - modifier = Modifier.weight(1f) - ) { - Text(resolvedSecondaryLabel) + Button( + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + onSecondaryActionClick() + }, + modifier = Modifier.weight(1f) + ) { + Text(resolvedSecondaryLabel) + } + } + } else { + OutlinedButton(onClick = { + HapticUtil.performVirtualKeyHaptic(view) + onActionClick() + }, modifier = Modifier.fillMaxWidth()) { + Text(resolvedActionLabel) } } } else { - OutlinedButton(onClick = { - HapticUtil.performVirtualKeyHaptic(view) - onActionClick() - }, modifier = Modifier.fillMaxWidth()) { - Text(resolvedActionLabel) - Spacer(modifier = Modifier.weight(1f)) - Icon( - painter = painterResource(id = R.drawable.rounded_arrow_forward_24), - contentDescription = null - ) - } - } - } else { - // Show buttons - either single or dual buttons - if (resolvedSecondaryLabel != null && onSecondaryActionClick != null) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - OutlinedButton( - onClick = { - HapticUtil.performVirtualKeyHaptic(view) - onActionClick() - }, - modifier = Modifier.weight(1f) + if (resolvedSecondaryLabel != null && onSecondaryActionClick != null) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - Text(resolvedActionLabel) - } + OutlinedButton( + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + onActionClick() + }, + modifier = Modifier.weight(1f) + ) { + Text(resolvedActionLabel) + } - Button( - onClick = { - HapticUtil.performVirtualKeyHaptic(view) - onSecondaryActionClick() - }, - modifier = Modifier.weight(1f) - ) { - Text(resolvedSecondaryLabel) + Button( + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + onSecondaryActionClick() + }, + modifier = Modifier.weight(1f) + ) { + Text(resolvedSecondaryLabel) + } + } + } else { + Button(onClick = { + HapticUtil.performVirtualKeyHaptic(view) + onActionClick() + }, modifier = Modifier.fillMaxWidth()) { + Text(resolvedActionLabel) } - } - } else { - Button(onClick = { - HapticUtil.performVirtualKeyHaptic(view) - onActionClick() - }, modifier = Modifier.fillMaxWidth()) { - Text(resolvedActionLabel) - Spacer(modifier = Modifier.weight(1f)) - Icon( - painter = painterResource(id = R.drawable.rounded_arrow_forward_24), - contentDescription = null - ) } } } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/pickers/CrashReportingPicker.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/pickers/CrashReportingPicker.kt index 5c581909c..316052210 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/pickers/CrashReportingPicker.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/pickers/CrashReportingPicker.kt @@ -33,37 +33,42 @@ fun CrashReportingPicker( Column( modifier = modifier .fillMaxWidth() - .background( - color = MaterialTheme.colorScheme.surfaceBright, - shape = RoundedCornerShape(MaterialTheme.shapes.extraSmall.bottomEnd) - ) - .padding(12.dp), - verticalArrangement = Arrangement.spacedBy(10.dp) + .background(MaterialTheme.colorScheme.surfaceBright), + verticalArrangement = Arrangement.spacedBy(0.dp) ) { - Row( + ListItem( + onClick = {}, + modifier = Modifier.fillMaxWidth(), + leadingContent = { + Icon( + painter = painterResource(id = iconRes), + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.primary + ) + }, + contentPadding = PaddingValues( + horizontal = 16.dp, + vertical = 16.dp + ), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - Spacer(modifier = Modifier.size(2.dp)) - Icon( - painter = painterResource(id = iconRes), - contentDescription = null, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.size(2.dp)) - - Text( - text = stringResource(R.string.sentry_report_mode_title), - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.weight(1f), - color = MaterialTheme.colorScheme.onSurface - ) - } + colors = ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.surfaceBright + ), + content = { + Text( + text = stringResource(R.string.sentry_report_mode_title), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface + ) + } + ) Row( modifier = Modifier - .fillMaxWidth(), + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 16.dp), horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween), ) { options.forEachIndexed { index, option -> diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/pickers/DefaultTabPicker.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/pickers/DefaultTabPicker.kt index 5940557a8..84511cb67 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/pickers/DefaultTabPicker.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/pickers/DefaultTabPicker.kt @@ -2,13 +2,18 @@ package com.sameerasw.essentials.ui.components.pickers import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues 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.RoundedCornerShape import androidx.compose.material3.ButtonGroupDefaults import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.ToggleButton @@ -34,49 +39,82 @@ fun DefaultTabPicker( options: List = DIYTabs.entries ) { - Row( + Column( modifier = modifier - .background( - color = MaterialTheme.colorScheme.surfaceBright, - shape = RoundedCornerShape(MaterialTheme.shapes.extraSmall.bottomEnd) - ) - .padding(10.dp), - horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween), + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surfaceBright), + verticalArrangement = Arrangement.spacedBy(0.dp) ) { - options.forEachIndexed { index, tab -> - val isChecked = selectedTab == tab + ListItem( + onClick = {}, + modifier = Modifier.fillMaxWidth(), + leadingContent = { + Icon( + painter = painterResource(id = R.drawable.rounded_widgets_24), + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.primary + ) + }, + contentPadding = PaddingValues( + horizontal = 16.dp, + vertical = 16.dp + ), + verticalAlignment = Alignment.CenterVertically, + colors = ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.surfaceBright + ), + content = { + Text( + text = stringResource(R.string.label_default_tab), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface + ) + } + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 16.dp), + horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween), + ) { + options.forEachIndexed { index, tab -> + val isChecked = selectedTab == tab - ToggleButton( - checked = isChecked, - onCheckedChange = { - onTabSelected(tab) - }, - modifier = Modifier - .weight(1f) - .semantics { role = Role.RadioButton }, - shapes = when { - index == 0 -> ButtonGroupDefaults.connectedLeadingButtonShapes() - index == options.lastIndex -> ButtonGroupDefaults.connectedTrailingButtonShapes() - else -> ButtonGroupDefaults.connectedMiddleButtonShapes() - }, - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp) + ToggleButton( + checked = isChecked, + onCheckedChange = { + onTabSelected(tab) + }, + modifier = Modifier + .weight(1f) + .semantics { role = Role.RadioButton }, + shapes = when { + index == 0 -> ButtonGroupDefaults.connectedLeadingButtonShapes() + index == options.lastIndex -> ButtonGroupDefaults.connectedTrailingButtonShapes() + else -> ButtonGroupDefaults.connectedMiddleButtonShapes() + }, ) { - Icon( - painter = painterResource(id = tab.iconRes), - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - Text( - text = if (tab == DIYTabs.FREEZE) stringResource(R.string.tab_freeze_title) else stringResource( - tab.title - ), - style = MaterialTheme.typography.labelLarge, - fontWeight = if (isChecked) FontWeight.Bold else FontWeight.Normal, - maxLines = 1 - ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Icon( + painter = painterResource(id = tab.iconRes), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Text( + text = if (tab == DIYTabs.FREEZE) stringResource(R.string.tab_freeze_title) else stringResource( + tab.title + ), + style = MaterialTheme.typography.labelLarge, + fontWeight = if (isChecked) FontWeight.Bold else FontWeight.Normal, + maxLines = 1 + ) + } } } } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/pickers/LanguagePicker.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/pickers/LanguagePicker.kt index baf4b61a4..21d07c27d 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/pickers/LanguagePicker.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/pickers/LanguagePicker.kt @@ -27,69 +27,69 @@ fun LanguagePicker( val languages = LanguageUtils.languages val selectedLanguage = languages.find { it.code == selectedLanguageCode } ?: languages.first() - Row( - modifier = modifier - .fillMaxWidth() - .background( - color = MaterialTheme.colorScheme.surfaceBright, - shape = RoundedCornerShape(MaterialTheme.shapes.extraSmall.bottomEnd) - ) - .padding(12.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Spacer(modifier = Modifier.size(0.dp)) - + ListItem( + onClick = {}, + modifier = modifier.fillMaxWidth(), + leadingContent = { Icon( painter = painterResource(id = R.drawable.rounded_globe_24), contentDescription = null, modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.primary ) - - Column { - Text( - text = stringResource(R.string.label_app_language), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface - ) - } - - ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = { expanded = !expanded }, - // modifier = Modifier.fillMaxWidth() - ) { - OutlinedTextField( - value = "${selectedLanguage.nativeName} (${selectedLanguage.name})", - onValueChange = {}, - readOnly = true, - modifier = Modifier - .fillMaxWidth() - .menuAnchor(MenuAnchorType.PrimaryEditable, true), - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, - colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(), - shape = RoundedCornerShape(12.dp) - ) - - ExposedDropdownMenu( + }, + contentPadding = PaddingValues( + horizontal = 16.dp, + vertical = 16.dp + ), + verticalAlignment = Alignment.CenterVertically, + colors = ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.surfaceBright + ), + trailingContent = { + ExposedDropdownMenuBox( expanded = expanded, - onDismissRequest = { expanded = false } + onExpandedChange = { expanded = !expanded }, ) { - languages.forEach { language -> - DropdownMenuItem( - text = { - Text(text = "${language.nativeName} (${language.name})") - }, - onClick = { - HapticUtil.performVirtualKeyHaptic(view) - onLanguageSelected(language.code) - expanded = false - }, - contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding - ) + OutlinedTextField( + value = "${selectedLanguage.nativeName} (${selectedLanguage.name})", + onValueChange = {}, + readOnly = true, + modifier = Modifier + .widthIn(max = 200.dp) // Limit width to prevent overflow + .menuAnchor(MenuAnchorType.PrimaryEditable, true), + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(), + shape = RoundedCornerShape(12.dp), + textStyle = MaterialTheme.typography.bodySmall + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + languages.forEach { language -> + DropdownMenuItem( + text = { + Text(text = "${language.nativeName} (${language.name})") + }, + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + onLanguageSelected(language.code) + expanded = false + }, + contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding + ) + } } } + }, + content = { + Text( + text = stringResource(R.string.label_app_language), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface + ) } - } + ) } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/LocationReachedBottomSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/LocationReachedBottomSheet.kt new file mode 100644 index 000000000..cbb4feab0 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/LocationReachedBottomSheet.kt @@ -0,0 +1,197 @@ +package com.sameerasw.essentials.ui.components.sheets + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.sameerasw.essentials.R +import com.sameerasw.essentials.domain.model.LocationAlarm +import com.sameerasw.essentials.ui.components.LocationIconPicker +import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer +import com.sameerasw.essentials.viewmodels.LocationReachedViewModel + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun LocationReachedBottomSheet( + viewModel: LocationReachedViewModel, + onDismissRequest: () -> Unit +) { + val tempAlarm by viewModel.tempAlarm + val currentAlarm = tempAlarm + val distance by viewModel.currentDistance + val view = androidx.compose.ui.platform.LocalView.current + + val isProcessing by viewModel.isProcessingCoordinates + + if (currentAlarm == null && !isProcessing) return + + ModalBottomSheet( + onDismissRequest = onDismissRequest, + sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + dragHandle = { BottomSheetDefaults.DragHandle() } + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .padding(bottom = 32.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = if (currentAlarm != null && viewModel.savedAlarms.value.any { it.id == currentAlarm.id }) + stringResource(R.string.location_reached_edit_title) + else stringResource(R.string.location_reached_add_title), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + + if (isProcessing && currentAlarm == null) { + RoundedCardContainer( + containerColor = MaterialTheme.colorScheme.surfaceBright, + modifier = Modifier.fillMaxWidth().height(200.dp) + ) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + LoadingIndicator() + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = stringResource(R.string.location_reached_resolving), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } else if (currentAlarm != null) { + RoundedCardContainer( + containerColor = MaterialTheme.colorScheme.surfaceBright, + modifier = Modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + OutlinedTextField( + value = currentAlarm.name, + onValueChange = { viewModel.setTempAlarm(currentAlarm.copy(name = it)) }, + label = { Text(stringResource(R.string.location_reached_name_label)) }, + placeholder = { Text(stringResource(R.string.location_reached_name_placeholder)) }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + shape = MaterialTheme.shapes.large + ) + + LocationIconPicker( + selectedIconName = currentAlarm.iconResName, + onIconSelected = { + viewModel.setTempAlarm(currentAlarm.copy(iconResName = it)) + } + ) + + // Coordinates Display + Column { + Text( + text = "Coordinates", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = "%.5f, %.5f".format(currentAlarm.latitude, currentAlarm.longitude), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.Medium + ) + } + + // Radius Slider + Column { + Text( + text = stringResource(R.string.location_reached_radius_label, currentAlarm.radius), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Slider( + value = currentAlarm.radius.toFloat(), + onValueChange = { + if (it.toInt() != currentAlarm.radius) { + com.sameerasw.essentials.utils.HapticUtil.performSliderHaptic(view) + } + viewModel.setTempAlarm(currentAlarm.copy(radius = it.toInt())) + }, + valueRange = 100f..5000f, + steps = 49 + ) + } + } + } + } + + val isEditing = currentAlarm != null && viewModel.savedAlarms.value.any { it.id == currentAlarm.id } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (isEditing) { + IconButton( + onClick = { + com.sameerasw.essentials.utils.HapticUtil.performVirtualKeyHaptic(view) + viewModel.deleteAlarm(currentAlarm.id) + onDismissRequest() + }, + colors = IconButtonDefaults.iconButtonColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.error + ), + modifier = Modifier.size(56.dp) // Slightly larger for better touch target + ) { + Icon( + painter = painterResource(R.drawable.rounded_delete_24), + contentDescription = stringResource(R.string.action_delete) + ) + } + } + + OutlinedButton( + onClick = { + com.sameerasw.essentials.utils.HapticUtil.performVirtualKeyHaptic(view) + onDismissRequest() + }, + modifier = Modifier + .weight(1f) + .height(56.dp), + shape = androidx.compose.foundation.shape.CircleShape + ) { + Text(stringResource(R.string.location_reached_cancel_btn)) + } + + Button( + onClick = { + val alarm = tempAlarm + if (alarm != null) { + com.sameerasw.essentials.utils.HapticUtil.performVirtualKeyHaptic(view) + viewModel.saveAlarm(alarm) + } + }, + enabled = currentAlarm != null, + modifier = Modifier + .weight(1f) + .height(56.dp), + shape = androidx.compose.foundation.shape.CircleShape + ) { + Text(stringResource(R.string.location_reached_save_btn)) + } + } + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/UpdateBottomSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/UpdateBottomSheet.kt index d7ab8a7c1..a89dcea7d 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/UpdateBottomSheet.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/UpdateBottomSheet.kt @@ -16,8 +16,8 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.Icon -import androidx.compose.material3.LoadingIndicator +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton @@ -33,6 +33,9 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.material3.Icon +import androidx.compose.material3.LoadingIndicator import androidx.core.net.toUri import com.sameerasw.essentials.R import com.sameerasw.essentials.domain.model.UpdateInfo @@ -101,26 +104,33 @@ fun UpdateBottomSheet( if (isPreRelease) { RoundedCardContainer { - Row( - modifier = Modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.5f)) - .padding(12.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - painter = painterResource(id = R.drawable.rounded_mobile_code_24), - contentDescription = null, - tint = MaterialTheme.colorScheme.error, - modifier = Modifier.size(24.dp) - ) - Text( - text = stringResource(R.string.warning_pre_release), - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onErrorContainer - ) - } + ListItem( + onClick = {}, + modifier = Modifier.fillMaxWidth(), + leadingContent = { + Icon( + painter = painterResource(id = R.drawable.rounded_mobile_code_24), + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(24.dp), + ) + }, + contentPadding = PaddingValues( + horizontal = 16.dp, + vertical = 16.dp + ), + verticalAlignment = Alignment.CenterVertically, + colors = ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.5f) + ), + content = { + Text( + text = stringResource(R.string.warning_pre_release), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onErrorContainer + ) + } + ) } Spacer(modifier = Modifier.height(8.dp)) } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/FreezeGridUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/FreezeGridUI.kt index 01dd03373..f5608af42 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/FreezeGridUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/FreezeGridUI.kt @@ -6,6 +6,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -31,6 +32,8 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -40,14 +43,20 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable 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.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorMatrix +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -56,7 +65,26 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.BorderStroke import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import android.content.Intent +import android.net.Uri +import android.provider.Settings +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.blur +import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenu +import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenuItem +import com.sameerasw.essentials.ui.state.LocalMenuStateManager import com.sameerasw.essentials.R import com.sameerasw.essentials.domain.model.NotificationApp import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer @@ -65,6 +93,8 @@ import com.sameerasw.essentials.utils.HapticUtil import com.sameerasw.essentials.utils.ShortcutUtil import com.sameerasw.essentials.viewmodels.MainViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @OptIn(ExperimentalMaterial3ExpressiveApi::class) @@ -73,16 +103,40 @@ fun FreezeGridUI( viewModel: MainViewModel, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(0.dp), - onGetStartedClick: (() -> Unit)? = null + onGetStartedClick: (() -> Unit)? = null, + onAppLaunched: (() -> Unit)? = null, + onSettingsClick: (() -> Unit)? = null ) { val context = LocalContext.current val view = LocalView.current + val menuState = LocalMenuStateManager.current + val scope = rememberCoroutineScope() val pickedApps by viewModel.freezePickedApps val isPickedAppsLoading by viewModel.isFreezePickedAppsLoading val frozenStates = remember { mutableStateMapOf() } val lifecycleOwner = LocalLifecycleOwner.current + var searchQuery by rememberSaveable { mutableStateOf("") } + val focusRequester = remember { FocusRequester() } + val focusManager = LocalFocusManager.current + var isFocused by remember { mutableStateOf(false) } + + val filteredApps = remember(pickedApps, searchQuery) { + if (searchQuery.isBlank()) { + pickedApps + } else { + pickedApps.filter { app -> + app.appName.contains(searchQuery, ignoreCase = true) || + app.packageName.contains(searchQuery, ignoreCase = true) + } + } + } + + val bestMatch = remember(searchQuery, filteredApps) { + if (searchQuery.isNotBlank() && filteredApps.isNotEmpty()) filteredApps.first() else null + } + // Refresh frozen states when active DisposableEffect(lifecycleOwner) { val observer = LifecycleEventObserver { _, event -> @@ -185,139 +239,210 @@ fun FreezeGridUI( modifier = Modifier .fillMaxSize() .verticalScroll(scrollState) + .pointerInput(Unit) { + detectTapGestures(onTap = { focusManager.clearFocus() }) + } ) { Spacer(modifier = Modifier.height(contentPadding.calculateTopPadding())) - Spacer(modifier = Modifier.height(16.dp)) + + OutlinedTextField( + value = searchQuery, + onValueChange = { searchQuery = it }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp) + .focusRequester(focusRequester) + .onFocusChanged { isFocused = it.isFocused }, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.rounded_search_24), + contentDescription = stringResource(R.string.label_search_content_description), + modifier = Modifier.size(24.dp) + ) + }, + trailingIcon = { + if (searchQuery.isNotEmpty()) { + IconButton(onClick = { + searchQuery = "" + HapticUtil.performVirtualKeyHaptic(view) + }) { + Icon( + painter = painterResource(id = R.drawable.rounded_close_24), + contentDescription = stringResource(R.string.action_stop) + ) + } + } + }, + placeholder = { + Text(stringResource(R.string.search_frozen_apps_placeholder)) + }, + shape = MaterialTheme.shapes.extraExtraLarge, + singleLine = true, + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MaterialTheme.colorScheme.primary, + unfocusedBorderColor = MaterialTheme.colorScheme.onSurfaceVariant, + focusedContainerColor = MaterialTheme.colorScheme.surfaceBright + ), + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Search, + capitalization = KeyboardCapitalization.Words + ), + keyboardActions = KeyboardActions( + onSearch = { + bestMatch?.let { app -> + HapticUtil.performVirtualKeyHaptic(view) + viewModel.launchAndUnfreezeApp(context, app.packageName) + onAppLaunched?.invoke() + } + } + ) + ) + + Spacer(modifier = Modifier.height(8.dp)) RoundedCardContainer( modifier = Modifier .padding(horizontal = 16.dp), ) { - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(2.dp) + Row( + modifier = Modifier + .fillMaxWidth() + .background( + color = MaterialTheme.colorScheme.surfaceBright, + shape = MaterialTheme.shapes.extraSmall + ) + .padding(12.dp), + horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween), + verticalAlignment = Alignment.CenterVertically ) { - Row( - modifier = Modifier - .fillMaxWidth() - .background( - color = MaterialTheme.colorScheme.surfaceBright, - shape = MaterialTheme.shapes.extraSmall - ) - .padding(12.dp), - horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween), - verticalAlignment = Alignment.CenterVertically + // Freeze Button + Button( + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + viewModel.freezeAllAuto(context) + }, + modifier = Modifier.weight(1f), + enabled = isShizukuAvailable && isShizukuPermissionGranted, + shape = ButtonDefaults.shape ) { - // Freeze Button - Button( - onClick = { - HapticUtil.performVirtualKeyHaptic(view) - viewModel.freezeAllAuto(context) - }, - modifier = Modifier.weight(1f), - enabled = isShizukuAvailable && isShizukuPermissionGranted, - shape = ButtonDefaults.shape - ) { - Icon( - painter = painterResource(id = R.drawable.rounded_mode_cool_24), - contentDescription = null, - modifier = Modifier.size(20.dp) - ) - Spacer(Modifier.size(8.dp)) - Text(stringResource(R.string.action_freeze)) - } + Icon( + painter = painterResource(id = R.drawable.rounded_mode_cool_24), + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + Spacer(Modifier.size(8.dp)) + Text(stringResource(R.string.action_freeze)) + } - // Unfreeze Button - Button( - onClick = { - HapticUtil.performVirtualKeyHaptic(view) - viewModel.unfreezeAllAuto(context) - }, - modifier = Modifier.weight(1f), - enabled = isShizukuAvailable && isShizukuPermissionGranted, - shape = ButtonDefaults.shape - ) { - Icon( - painter = painterResource(id = R.drawable.rounded_mode_cool_off_24), - contentDescription = null, - modifier = Modifier.size(20.dp) - ) - Spacer(Modifier.size(8.dp)) - Text(stringResource(R.string.action_unfreeze)) - } + Spacer(Modifier.size(ButtonGroupDefaults.ConnectedSpaceBetween)) + + // Unfreeze Button + Button( + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + viewModel.unfreezeAllAuto(context) + }, + modifier = Modifier.weight(1f), + enabled = isShizukuAvailable && isShizukuPermissionGranted, + shape = ButtonDefaults.shape + ) { + Icon( + painter = painterResource(id = R.drawable.rounded_mode_cool_off_24), + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + Spacer(Modifier.size(8.dp)) + Text(stringResource(R.string.action_unfreeze)) + } + + // More Menu Button + IconButton( + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + isMenuExpanded = true + }, + enabled = isShizukuAvailable && isShizukuPermissionGranted + ) { + Icon( + painter = painterResource(id = R.drawable.rounded_more_vert_24), + contentDescription = stringResource(R.string.content_desc_more_options) + ) - // More Menu Button - IconButton( - onClick = { - HapticUtil.performVirtualKeyHaptic(view) - isMenuExpanded = true - }, - enabled = isShizukuAvailable && isShizukuPermissionGranted + SegmentedDropdownMenu( + expanded = isMenuExpanded, + onDismissRequest = { isMenuExpanded = false } ) { - Icon( - painter = painterResource(id = R.drawable.rounded_more_vert_24), - contentDescription = stringResource(R.string.content_desc_more_options) + SegmentedDropdownMenuItem( + text = { Text(stringResource(R.string.action_freeze_all)) }, + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + viewModel.freezeAllManual(context) + isMenuExpanded = false + }, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.rounded_mode_cool_24), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } ) - - DropdownMenu( - expanded = isMenuExpanded, - onDismissRequest = { isMenuExpanded = false } - ) { - DropdownMenuItem( - text = { Text(stringResource(R.string.action_freeze_all)) }, - onClick = { - HapticUtil.performVirtualKeyHaptic(view) - viewModel.freezeAllManual(context) - isMenuExpanded = false - }, - leadingIcon = { - Icon( - painter = painterResource(id = R.drawable.rounded_mode_cool_24), - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - } - ) - DropdownMenuItem( - text = { Text(stringResource(R.string.action_unfreeze_all)) }, - onClick = { - HapticUtil.performVirtualKeyHaptic(view) - viewModel.unfreezeAllManual(context) - isMenuExpanded = false - }, - leadingIcon = { - Icon( - painter = painterResource(id = R.drawable.rounded_mode_cool_off_24), - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - } - ) - DropdownMenuItem( - text = { Text(stringResource(R.string.action_export_freeze)) }, - onClick = { - HapticUtil.performVirtualKeyHaptic(view) - exportLauncher.launch("freeze_apps_backup.json") - isMenuExpanded = false - }, - leadingIcon = { - Icon( - painter = painterResource(id = R.drawable.rounded_arrow_warm_up_24), - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - } - ) - DropdownMenuItem( - text = { Text(stringResource(R.string.action_import_freeze)) }, + SegmentedDropdownMenuItem( + text = { Text(stringResource(R.string.action_unfreeze_all)) }, + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + viewModel.unfreezeAllManual(context) + isMenuExpanded = false + }, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.rounded_mode_cool_off_24), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } + ) + SegmentedDropdownMenuItem( + text = { Text(stringResource(R.string.action_export_freeze)) }, + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + exportLauncher.launch("freeze_apps_backup.json") + isMenuExpanded = false + }, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.rounded_arrow_warm_up_24), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } + ) + SegmentedDropdownMenuItem( + text = { Text(stringResource(R.string.action_import_freeze)) }, + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + importLauncher.launch(arrayOf("application/json")) + isMenuExpanded = false + }, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.rounded_arrow_cool_down_24), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } + ) + onSettingsClick?.let { onSettings -> + SegmentedDropdownMenuItem( + text = { Text(stringResource(R.string.label_settings)) }, onClick = { HapticUtil.performVirtualKeyHaptic(view) - importLauncher.launch(arrayOf("application/json")) + onSettings() isMenuExpanded = false }, leadingIcon = { Icon( - painter = painterResource(id = R.drawable.rounded_arrow_cool_down_24), + painter = painterResource(id = R.drawable.rounded_settings_heart_24), contentDescription = null, modifier = Modifier.size(18.dp) ) @@ -326,18 +451,49 @@ fun FreezeGridUI( } } } + } + } - // App Grid Items - val chunkedApps = pickedApps.chunked(4) + Spacer(modifier = Modifier.height(16.dp)) + + // App Grid Items + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + if (filteredApps.isEmpty() && searchQuery.isNotEmpty()) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 32.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "¯\\_(ツ)_/¯", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = stringResource(id = R.string.search_no_results, searchQuery), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 8.dp) + ) + } + } else { + val chunkedApps = filteredApps.chunked(4) Column( modifier = Modifier .fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(2.dp) + verticalArrangement = Arrangement.spacedBy(4.dp) ) { chunkedApps.forEach { rowApps -> Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(2.dp) + horizontalArrangement = Arrangement.spacedBy(4.dp) ) { rowApps.forEach { app -> Box(modifier = Modifier.weight(1f)) { @@ -345,15 +501,31 @@ fun FreezeGridUI( app = app, isFrozen = frozenStates[app.packageName] ?: false, isAutoFreezeEnabled = app.isEnabled, + isHighlighted = (app == bestMatch && searchQuery.isNotEmpty()), + menuState = menuState, onClick = { HapticUtil.performVirtualKeyHaptic(view) viewModel.launchAndUnfreezeApp( context, app.packageName ) + onAppLaunched?.invoke() }, - onLongClick = { - ShortcutUtil.pinAppShortcut(context, app) + onToggleFreeze = { + scope.launch(Dispatchers.IO) { + val isCurrentlyFrozen = frozenStates[app.packageName] ?: false + if (isCurrentlyFrozen) { + FreezeManager.unfreezeApp(context, app.packageName) + } else { + FreezeManager.freezeApp(context, app.packageName) + } + withContext(Dispatchers.Main) { + frozenStates[app.packageName] = !isCurrentlyFrozen + } + } + }, + onRemove = { + viewModel.updateFreezeAppEnabled(context, app.packageName, false) } ) } @@ -379,17 +551,58 @@ fun AppGridItem( app: NotificationApp, isFrozen: Boolean, isAutoFreezeEnabled: Boolean, + isHighlighted: Boolean = false, + menuState: com.sameerasw.essentials.ui.state.MenuStateManager, onClick: () -> Unit, - onLongClick: () -> Unit + onToggleFreeze: () -> Unit, + onRemove: () -> Unit ) { val view = LocalView.current + val context = LocalContext.current + var showMenu by remember { mutableStateOf(false) } + + val isBlurred = menuState.activeId != null && menuState.activeId != app.packageName + val blurRadius by animateDpAsState( + targetValue = if (isBlurred) 10.dp else 0.dp, + animationSpec = tween(durationMillis = 500), + label = "blur" + ) + val alpha by animateFloatAsState( + targetValue = if (isBlurred) 0.5f else 1f, + animationSpec = tween(durationMillis = 500), + label = "alpha" + ) + + DisposableEffect(showMenu) { + if (showMenu) { + menuState.activeId = app.packageName + } else { + if (menuState.activeId == app.packageName) { + menuState.activeId = null + } + } + onDispose { + if (menuState.activeId == app.packageName) { + menuState.activeId = null + } + } + } + val grayscaleMatrix = remember { ColorMatrix().apply { setToSaturation(0.4f) } } + + val borderColor by animateColorAsState( + targetValue = if (isHighlighted) MaterialTheme.colorScheme.primary else Color.Transparent, + label = "borderColorAnimation" + ) Surface( - shape = RoundedCornerShape(4.dp), + shape = RoundedCornerShape(16.dp), color = MaterialTheme.colorScheme.surfaceBright, + border = if (isHighlighted) BorderStroke(2.dp, borderColor) else null, modifier = Modifier .fillMaxWidth() + .alpha(alpha) + .blur(blurRadius) .combinedClickable( onClick = { HapticUtil.performVirtualKeyHaptic(view) @@ -397,7 +610,7 @@ fun AppGridItem( }, onLongClick = { HapticUtil.performVirtualKeyHaptic(view) - onLongClick() + showMenu = true } ) ) { @@ -447,16 +660,89 @@ fun AppGridItem( } } - Spacer(modifier = Modifier.height(10.dp)) - Text( text = app.appName, style = MaterialTheme.typography.labelSmall, textAlign = TextAlign.Center, maxLines = 1, overflow = TextOverflow.Ellipsis, - color = if (isFrozen) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurface + modifier = Modifier.padding(top = 8.dp), + color = MaterialTheme.colorScheme.onSurface ) + + SegmentedDropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false } + ) { + SegmentedDropdownMenuItem( + text = { + Text(if (isFrozen) stringResource(R.string.action_unfreeze) else stringResource(R.string.action_freeze)) + }, + onClick = { + showMenu = false + onToggleFreeze() + }, + leadingIcon = { + Icon( + painter = painterResource(id = if (isFrozen) R.drawable.rounded_mode_cool_off_24 else R.drawable.rounded_mode_cool_24), + contentDescription = null + ) + } + ) + + SegmentedDropdownMenuItem( + text = { + Text(stringResource(R.string.action_remove)) + }, + onClick = { + showMenu = false + onRemove() + }, + enabled = !isFrozen, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.rounded_delete_24), + contentDescription = null + ) + } + ) + + SegmentedDropdownMenuItem( + text = { + Text(stringResource(R.string.action_create_shortcut)) + }, + onClick = { + showMenu = false + ShortcutUtil.pinAppShortcut(context, app) + }, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.rounded_home_health_24), + contentDescription = null + ) + } + ) + + SegmentedDropdownMenuItem( + text = { + Text(stringResource(R.string.action_app_info)) + }, + onClick = { + showMenu = false + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", app.packageName, null) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity(intent) + }, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.rounded_info_24), + contentDescription = null + ) + } + ) + } } } } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/BatteriesSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/BatteriesSettingsUI.kt index 141df8b6a..ddc6f7671 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/BatteriesSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/BatteriesSettingsUI.kt @@ -14,12 +14,15 @@ 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.material3.Slider +import androidx.compose.material3.Text +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Slider -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -69,49 +72,58 @@ fun BatteriesSettingsUI( } ) } else { - Row( - modifier = Modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.surfaceBright) - .padding(12.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - Spacer(modifier = Modifier.size(2.dp)) - Icon( - painter = painterResource(R.drawable.rounded_laptop_mac_24), - contentDescription = null, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.size(2.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = stringResource(R.string.download_airsync), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface + ListItem( + onClick = {}, + modifier = Modifier.fillMaxWidth(), + leadingContent = { + Icon( + painter = painterResource(R.drawable.rounded_laptop_mac_24), + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.primary ) + }, + contentPadding = PaddingValues( + horizontal = 16.dp, + vertical = 16.dp + ), + verticalAlignment = Alignment.CenterVertically, + colors = ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.surfaceBright + ), + trailingContent = { + Button( + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + val intent = Intent( + Intent.ACTION_VIEW, + Uri.parse("https://play.google.com/store/apps/details?id=com.sameerasw.airsync") + ) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + context.startActivity(intent) + }, + colors = ButtonDefaults.filledTonalButtonColors() + ) { + Text(stringResource(R.string.action_download)) + } + }, + content = { + Column { + Text( + text = stringResource(R.string.download_airsync), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface + ) + } + }, + supportingContent = { Text( text = stringResource(R.string.download_airsync_summary), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) } - Button( - onClick = { - HapticUtil.performVirtualKeyHaptic(view) - val intent = Intent( - Intent.ACTION_VIEW, - Uri.parse("https://play.google.com/store/apps/details?id=com.sameerasw.airsync") - ) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - context.startActivity(intent) - }, - colors = ButtonDefaults.filledTonalButtonColors() - ) { - Text(stringResource(R.string.action_download)) - } - } + ) } // Bluetooth Devices @@ -176,43 +188,50 @@ fun BatteriesSettingsUI( ) RoundedCardContainer { - Column( - modifier = Modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.surfaceBright) - .padding(horizontal = 16.dp, vertical = 12.dp) - ) { - Text( - text = stringResource(R.string.limit_max_devices_summary), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(modifier = Modifier.height(8.dp)) - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { - Slider( - value = viewModel.batteryWidgetMaxDevices.intValue.toFloat(), - onValueChange = { - val newInt = it.toInt() - if (newInt != viewModel.batteryWidgetMaxDevices.intValue) { - HapticUtil.performVirtualKeyHaptic(view) - viewModel.setBatteryWidgetMaxDevices(newInt, context) - } - }, - valueRange = 1f..8f, - steps = 6, - modifier = Modifier.weight(1f) - ) - Spacer(modifier = Modifier.width(12.dp)) + ListItem( + onClick = {}, + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues( + horizontal = 16.dp, + vertical = 16.dp + ), + colors = ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.surfaceBright + ), + content = { Text( - text = viewModel.batteryWidgetMaxDevices.intValue.toString(), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary + text = stringResource(R.string.limit_max_devices_summary), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant ) + }, + supportingContent = { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Slider( + value = viewModel.batteryWidgetMaxDevices.intValue.toFloat(), + onValueChange = { + val newInt = it.toInt() + if (newInt != viewModel.batteryWidgetMaxDevices.intValue) { + HapticUtil.performVirtualKeyHaptic(view) + viewModel.setBatteryWidgetMaxDevices(newInt, context) + } + }, + valueRange = 1f..8f, + steps = 6, + modifier = Modifier.weight(1f) + ) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = viewModel.batteryWidgetMaxDevices.intValue.toString(), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary + ) + } } - } + ) } } } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/FreezeSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/FreezeSettingsUI.kt index 825affa4c..4207a37d6 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/FreezeSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/FreezeSettingsUI.kt @@ -2,16 +2,21 @@ package com.sameerasw.essentials.ui.composables.configs import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState 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.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults @@ -34,6 +39,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource @@ -41,6 +47,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.sameerasw.essentials.R import com.sameerasw.essentials.domain.model.FreezeMode +import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenu +import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenuItem import com.sameerasw.essentials.ui.components.cards.AppToggleItem import com.sameerasw.essentials.ui.components.cards.FeatureCard import com.sameerasw.essentials.ui.components.cards.IconToggleItem @@ -74,6 +82,11 @@ fun FreezeSettingsUI( var isMenuExpanded by remember { mutableStateOf(false) } + val pagerState = rememberPagerState(pageCount = { 2 }) + LaunchedEffect(viewModel.freezeMode.intValue) { + pagerState.animateScrollToPage(viewModel.freezeMode.intValue) + } + val exportLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.CreateDocument("application/json") ) { uri -> @@ -198,11 +211,11 @@ fun FreezeSettingsUI( contentDescription = stringResource(R.string.content_desc_more_options) ) - DropdownMenu( + SegmentedDropdownMenu( expanded = isMenuExpanded, onDismissRequest = { isMenuExpanded = false } ) { - DropdownMenuItem( + SegmentedDropdownMenuItem( text = { Text(stringResource(R.string.action_freeze_all)) }, onClick = { HapticUtil.performVirtualKeyHaptic(view) @@ -217,7 +230,7 @@ fun FreezeSettingsUI( ) } ) - DropdownMenuItem( + SegmentedDropdownMenuItem( text = { Text(stringResource(R.string.action_unfreeze_all)) }, onClick = { HapticUtil.performVirtualKeyHaptic(view) @@ -232,7 +245,7 @@ fun FreezeSettingsUI( ) } ) - DropdownMenuItem( + SegmentedDropdownMenuItem( text = { Text(stringResource(R.string.action_export_freeze)) }, onClick = { HapticUtil.performVirtualKeyHaptic(view) @@ -247,7 +260,7 @@ fun FreezeSettingsUI( ) } ) - DropdownMenuItem( + SegmentedDropdownMenuItem( text = { Text(stringResource(R.string.action_import_freeze)) }, onClick = { HapticUtil.performVirtualKeyHaptic(view) @@ -320,6 +333,93 @@ fun FreezeSettingsUI( ) }, ) + + Column( + modifier = Modifier + .fillMaxWidth() + .background( + color = MaterialTheme.colorScheme.surfaceBright, + ) + .padding(8.dp) + ) { + HorizontalPager( + state = pagerState, + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.Top + ) { page -> + Column( + modifier = Modifier + .fillMaxWidth() + .background( + color = MaterialTheme.colorScheme.surfaceContainer, + shape = RoundedCornerShape(20.dp) + ) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + if (page == 0) { + Text( + text = stringResource(R.string.freeze_mode_description_freeze_title), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.primary, + ) + Text( + text = stringResource(R.string.freeze_mode_description_freeze_body), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = stringResource(R.string.freeze_mode_description_freeze_warning), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.error, + fontWeight = FontWeight.Bold + ) + } else { + Text( + text = stringResource(R.string.freeze_mode_description_suspend_title), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.primary, + ) + Text( + text = stringResource(R.string.freeze_mode_description_suspend_body), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = stringResource(R.string.freeze_mode_description_suspend_footer), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontWeight = FontWeight.Medium + ) + } + } + } + + // Pagination Indicators + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + repeat(2) { iteration -> + val isActive = pagerState.currentPage == iteration + val color by animateColorAsState( + targetValue = if (isActive) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f), + label = "dotColor" + ) + + Box( + modifier = Modifier + .padding(4.dp) + .size(if (isActive) 8.dp else 6.dp) + .background(color, CircleShape) + ) + } + } + } } Text( diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/LocationReachedSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/LocationReachedSettingsUI.kt index 905a0f745..fc5d83680 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/LocationReachedSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/LocationReachedSettingsUI.kt @@ -1,32 +1,21 @@ package com.sameerasw.essentials.ui.composables.configs -import android.content.Intent -import android.net.Uri import android.os.Build import androidx.compose.foundation.background -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.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.width -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.LinearWavyProgressIndicator -import androidx.compose.material3.LoadingIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Slider -import androidx.compose.material3.Text +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -34,12 +23,15 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.sameerasw.essentials.R -import com.sameerasw.essentials.ui.components.cards.IconToggleItem +import com.sameerasw.essentials.ui.components.cards.LocationAlarmCard import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer +import com.sameerasw.essentials.ui.components.sheets.LocationReachedBottomSheet +import com.sameerasw.essentials.ui.components.sheets.PermissionsBottomSheet +import com.sameerasw.essentials.utils.HapticUtil import com.sameerasw.essentials.viewmodels.LocationReachedViewModel import com.sameerasw.essentials.viewmodels.MainViewModel -@OptIn(androidx.compose.material3.ExperimentalMaterial3ExpressiveApi::class) +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun LocationReachedSettingsUI( mainViewModel: MainViewModel, @@ -48,10 +40,15 @@ fun LocationReachedSettingsUI( ) { val context = LocalContext.current val locationViewModel: LocationReachedViewModel = viewModel() - val alarm by locationViewModel.alarm + val savedAlarms by locationViewModel.savedAlarms + val activeAlarmId by locationViewModel.activeAlarmId + val lastTrip by locationViewModel.lastTrip val distance by locationViewModel.currentDistance - val isProcessing by locationViewModel.isProcessingCoordinates val startDistance by locationViewModel.startDistance + val showBottomSheet by locationViewModel.showBottomSheet + val isProcessing by locationViewModel.isProcessingCoordinates + val remainingTimeMinutes by locationViewModel.remainingTimeMinutes + val view = androidx.compose.ui.platform.LocalView.current DisposableEffect(locationViewModel) { locationViewModel.startUiTracking() @@ -60,311 +57,315 @@ fun LocationReachedSettingsUI( } } - Column( - modifier = modifier - .fillMaxSize() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally + Box( + modifier = modifier.fillMaxSize() ) { - if (isProcessing) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - LoadingIndicator() - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = stringResource(R.string.location_reached_processing), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + top = statusBarHeight + 8.dp, + bottom = 150.dp, + start = 16.dp, + end = 16.dp + ), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + + // Top Progress / Last Trip Card + item { + val activeAlarm = savedAlarms.find { it.id == activeAlarmId } + TopStatusCard( + activeAlarm = activeAlarm, + lastTrip = lastTrip, + distance = distance, + remainingTimeMinutes = remainingTimeMinutes, + startDistance = startDistance, + onStop = { locationViewModel.stopTracking() }, + onStart = { + HapticUtil.performVirtualKeyHaptic(view) + locationViewModel.startTracking(it) + } ) } - } else if (alarm.latitude != 0.0 && alarm.longitude != 0.0) { - // Destination Set State - RoundedCardContainer( - modifier = Modifier, - cornerRadius = 28.dp - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .background( - color = MaterialTheme.colorScheme.surfaceBright, - shape = androidx.compose.foundation.shape.RoundedCornerShape(28.dp) - ) - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - if (alarm.isEnabled) { - // TRACKING STATE - val distanceText = distance?.let { - if (it < 1000) stringResource( - R.string.location_reached_dist_m, - it.toInt() - ) - else stringResource(R.string.location_reached_dist_km, it / 1000f) - } ?: stringResource(R.string.location_reached_calculating) + // List Header + if (savedAlarms.isNotEmpty()) { + item { + Text( + text = stringResource(R.string.location_reached_saved_destinations), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(start = 8.dp, top = 8.dp), + color = MaterialTheme.colorScheme.onSurface + ) + } + } + + // Destinations List + if (savedAlarms.isNotEmpty()) { + item { + RoundedCardContainer( + modifier = Modifier.fillMaxWidth(), + ) { + savedAlarms.forEachIndexed { index, alarm -> + LocationAlarmCard( + alarm = alarm, + isActive = activeAlarmId == alarm.id, + isAnyTracking = activeAlarmId != null, + onStart = { + HapticUtil.performVirtualKeyHaptic(view) + locationViewModel.startTracking(alarm.id) + }, + onStop = { + HapticUtil.performVirtualKeyHaptic(view) + locationViewModel.stopTracking() + }, + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + locationViewModel.setTempAlarm(alarm) + locationViewModel.setShowBottomSheet(true) + } + ) + } + } + } + } + if (savedAlarms.isEmpty() && !isProcessing) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 48.dp), + contentAlignment = Alignment.Center + ) { Text( - text = stringResource(R.string.location_reached_dist_remaining), - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = distanceText, - style = MaterialTheme.typography.displayMedium, - color = MaterialTheme.colorScheme.primary, - fontWeight = FontWeight.Bold + text = stringResource(R.string.location_reached_no_saved_dest), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = androidx.compose.ui.text.style.TextAlign.Center ) + } + } + } - if (distance != null && startDistance > 0) { - val progress = - (1.0f - (distance!! / startDistance)).coerceIn(0.0f, 1.0f) - Spacer(modifier = Modifier.height(24.dp)) + // Instructional Description + item { + Text( + text = stringResource(R.string.location_reached_instructional_desc), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + textAlign = androidx.compose.ui.text.style.TextAlign.Start, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp) + ) + } - LinearWavyProgressIndicator( - progress = { progress }, - modifier = Modifier - .fillMaxWidth() - .height(12.dp), - color = MaterialTheme.colorScheme.primary, - trackColor = MaterialTheme.colorScheme.primaryContainer, - wavelength = 20.dp, - amplitude = { 1.0f } // Normalized amplitude - ) - } + // Permission Warning + item { + val isFSIGranted by mainViewModel.isFullScreenIntentPermissionGranted + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !isFSIGranted) { + RoundedCardContainer( + modifier = Modifier.fillMaxWidth(), + containerColor = MaterialTheme.colorScheme.errorContainer + ) { + com.sameerasw.essentials.ui.components.cards.IconToggleItem( + title = stringResource(R.string.location_reached_fsi_title), + description = stringResource(R.string.location_reached_fsi_desc), + isChecked = false, + onCheckedChange = { mainViewModel.requestFullScreenIntentPermission(context) }, + iconRes = R.drawable.rounded_info_24, + showToggle = false + ) + } + } + } + + item { + Spacer(modifier = Modifier.height(100.dp)) + } + } + } - Spacer(modifier = Modifier.height(32.dp)) + if (showBottomSheet) { + LocationReachedBottomSheet( + viewModel = locationViewModel, + onDismissRequest = { locationViewModel.setShowBottomSheet(false) } + ) + } +} - Button( - onClick = { locationViewModel.stopTracking() }, - modifier = Modifier - .fillMaxWidth() - .height(56.dp), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.errorContainer, - contentColor = MaterialTheme.colorScheme.error - ), - shape = androidx.compose.foundation.shape.CircleShape - ) { - Icon( - painterResource(R.drawable.rounded_pause_24), - contentDescription = null - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(stringResource(R.string.location_reached_stop_tracking)) - } +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun TopStatusCard( + activeAlarm: com.sameerasw.essentials.domain.model.LocationAlarm?, + lastTrip: com.sameerasw.essentials.domain.model.LocationAlarm?, + distance: Float?, + remainingTimeMinutes: Int?, + startDistance: Float, + onStop: () -> Unit, + onStart: (String) -> Unit +) { + val isTracking = activeAlarm != null + val displayAlarm = activeAlarm ?: lastTrip - } else { - // READY STATE (Not Tracking) - Icon( - painter = painterResource(id = R.drawable.rounded_my_location_24), - contentDescription = null, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = stringResource(R.string.location_reached_dest_ready), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = "${alarm.latitude}, ${alarm.longitude}", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + RoundedCardContainer( + modifier = Modifier.fillMaxWidth(), + cornerRadius = 32.dp, + containerColor = MaterialTheme.colorScheme.surfaceBright + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surfaceBright) + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + if (isTracking) { + val context = LocalContext.current + val iconResId = context.resources.getIdentifier(displayAlarm?.iconResName ?: "round_navigation_24", "drawable", context.packageName) - Spacer(modifier = Modifier.height(32.dp)) + Icon( + painter = painterResource(id = if (iconResId != 0) iconResId else R.drawable.round_navigation_24), + contentDescription = null, + modifier = Modifier.size(56.dp), + tint = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.height(16.dp)) - Button( - onClick = { locationViewModel.startTracking() }, - modifier = Modifier - .fillMaxWidth() - .height(56.dp), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary - ), - shape = androidx.compose.foundation.shape.CircleShape - ) { - Icon( - painterResource(R.drawable.rounded_play_arrow_24), - contentDescription = null - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(stringResource(R.string.location_reached_start_tracking)) - } - } + Text( + text = stringResource(R.string.location_reached_tracking_now), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = displayAlarm?.name?.ifEmpty { "Destination" } ?: "Destination", + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.height(24.dp)) + if (distance == null) { + LoadingIndicator() Spacer(modifier = Modifier.height(16.dp)) + } - // Secondary Actions - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - Button( - onClick = { - val gmmIntentUri = - Uri.parse("geo:${alarm.latitude},${alarm.longitude}") - val mapIntent = Intent(Intent.ACTION_VIEW, gmmIntentUri) - mapIntent.setPackage("com.google.android.apps.maps") - try { - context.startActivity(mapIntent) - } catch (e: android.content.ActivityNotFoundException) { - try { - mapIntent.setPackage(null) - context.startActivity(mapIntent) - } catch (ex: android.content.ActivityNotFoundException) { - android.widget.Toast.makeText(context, R.string.error_app_uninstalled, android.widget.Toast.LENGTH_SHORT).show() - } - } - }, - modifier = Modifier.weight(1f), - shape = androidx.compose.foundation.shape.CircleShape, - colors = ButtonDefaults.filledTonalButtonColors() - ) { - Icon( - painterResource(R.drawable.rounded_map_24), - contentDescription = null, - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(stringResource(R.string.location_reached_view_map)) - } + val distanceText = distance?.let { + if (it < 1000) "${it.toInt()} m" else "%.1f km".format(it / 1000f) + } ?: stringResource(R.string.location_reached_calculating) + + Text( + text = distanceText, + style = MaterialTheme.typography.displayMedium, + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold + ) - Button( - onClick = { locationViewModel.clearAlarm() }, - modifier = Modifier.weight(1f), - shape = androidx.compose.foundation.shape.CircleShape, - colors = ButtonDefaults.filledTonalButtonColors() - ) { - Icon( - painterResource(R.drawable.rounded_delete_24), - contentDescription = null, - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(stringResource(R.string.location_reached_clear)) - } + remainingTimeMinutes?.let { mins -> + val etaText = if (mins >= 60) { + stringResource(R.string.location_reached_eta_hr_min, mins / 60, mins % 60) + } else { + stringResource(R.string.location_reached_eta_min, mins) } - } - } - } else { - // Empty State - RoundedCardContainer( - modifier = Modifier.fillMaxWidth(), - cornerRadius = 28.dp - ) { - Column( - modifier = Modifier - .padding(16.dp) - .fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Icon( - painter = painterResource(id = R.drawable.rounded_add_location_alt_24), - contentDescription = null, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.surfaceVariant - ) - Spacer(modifier = Modifier.height(16.dp)) + Text( - text = stringResource(R.string.location_reached_no_dest), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = etaText, + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.SemiBold ) - Spacer(modifier = Modifier.height(8.dp)) Text( - text = stringResource(R.string.location_reached_how_to), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), - textAlign = androidx.compose.ui.text.style.TextAlign.Center + text = stringResource(R.string.location_reached_to_go).uppercase(), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), + fontWeight = FontWeight.Bold ) + } + + if (distance != null && startDistance > 0) { + val progress = (1.0f - (distance / startDistance)).coerceIn(0.0f, 1.0f) Spacer(modifier = Modifier.height(24.dp)) - Button( - onClick = { - val gmmIntentUri = Uri.parse("geo:0,0?q=") - val mapIntent = Intent(Intent.ACTION_VIEW, gmmIntentUri) - mapIntent.setPackage("com.google.android.apps.maps") - try { - context.startActivity(mapIntent) - } catch (e: android.content.ActivityNotFoundException) { - try { - mapIntent.setPackage(null) - context.startActivity(mapIntent) - } catch (ex: android.content.ActivityNotFoundException) { - android.widget.Toast.makeText(context, R.string.error_app_uninstalled, android.widget.Toast.LENGTH_SHORT).show() - } - } - }, - shape = androidx.compose.foundation.shape.CircleShape, - colors = ButtonDefaults.filledTonalButtonColors() - ) { - Text(stringResource(R.string.location_reached_open_maps)) - } + LinearWavyProgressIndicator( + progress = { progress }, + modifier = Modifier + .fillMaxWidth() + .height(12.dp), + color = MaterialTheme.colorScheme.primary, + trackColor = MaterialTheme.colorScheme.primaryContainer, + wavelength = 20.dp, + amplitude = { 1.0f } + ) } - } - } - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(32.dp)) - Text( - text = stringResource(R.string.location_reached_radius_title, alarm.radius), - style = MaterialTheme.typography.titleMedium, - modifier = Modifier - .fillMaxWidth() - .padding(start = 16.dp, bottom = 8.dp), - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + Button( + onClick = onStop, + modifier = Modifier + .fillMaxWidth() + .height(56.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.error + ), + shape = androidx.compose.foundation.shape.CircleShape + ) { + Icon(painterResource(R.drawable.rounded_close_24), contentDescription = null) + Spacer(modifier = Modifier.width(8.dp)) + Text(stringResource(R.string.action_stop)) + } + } else if (lastTrip != null) { + val context = LocalContext.current + val iconResId = context.resources.getIdentifier(lastTrip.iconResName, "drawable", context.packageName) - RoundedCardContainer( - modifier = Modifier, - cornerRadius = 28.dp - ) { - Column( - modifier = Modifier - .background(MaterialTheme.colorScheme.surfaceBright) - .padding(16.dp) - ) { - Slider( - value = alarm.radius.toFloat(), - onValueChange = { newVal -> - locationViewModel.updateAlarm(alarm.copy(radius = newVal.toInt())) - }, - valueRange = 100f..5000f, - steps = 49 + Icon( + painter = painterResource(id = if (iconResId != 0) iconResId else R.drawable.round_navigation_24), + contentDescription = null, + modifier = Modifier.size(40.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) ) - } - } - - Spacer(modifier = Modifier.height(24.dp)) - - val isFSIGranted by mainViewModel.isFullScreenIntentPermissionGranted - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !isFSIGranted) { - RoundedCardContainer( - modifier = Modifier.background( - color = MaterialTheme.colorScheme.errorContainer, - shape = androidx.compose.foundation.shape.RoundedCornerShape(28.dp) - ), - cornerRadius = 28.dp - ) { - IconToggleItem( - title = stringResource(R.string.location_reached_fsi_title), - description = stringResource(R.string.location_reached_fsi_desc), - isChecked = false, - onCheckedChange = { mainViewModel.requestFullScreenIntentPermission(context) }, - iconRes = R.drawable.rounded_info_24, - showToggle = false + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(R.string.location_reached_last_trip), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = lastTrip.name.ifEmpty { "Destination" }, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface + ) + Spacer(modifier = Modifier.height(16.dp)) + Button( + onClick = { onStart(lastTrip.id) }, + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + shape = androidx.compose.foundation.shape.CircleShape + ) { + Text(stringResource(R.string.location_reached_restart_btn)) + } + } else { + // Completely empty state for top card + Icon( + painter = painterResource(R.drawable.round_navigation_24), + contentDescription = null, + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = stringResource(R.string.feat_location_reached_title), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } } } } - diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/QuickSettingsTilesSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/QuickSettingsTilesSettingsUI.kt index a9c23b376..09f8213c7 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/QuickSettingsTilesSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/QuickSettingsTilesSettingsUI.kt @@ -289,6 +289,13 @@ fun QuickSettingsTilesSettingsUI( ChargeQuickTileService::class.java, if (ShellUtils.isRootEnabled(context)) listOf("ROOT") else listOf("SHIZUKU"), R.string.about_desc_charge_optimization + ), + QSTileInfo( + R.string.tile_location_reached, + R.drawable.rounded_navigation_24, + com.sameerasw.essentials.services.tiles.LocationReachedTileService::class.java, + listOf("LOCATION", "BACKGROUND_LOCATION", "USE_FULL_SCREEN_INTENT"), + R.string.about_desc_location_reached ) ) diff --git a/app/src/main/java/com/sameerasw/essentials/ui/modifiers/ProgressiveBlurModifier.kt b/app/src/main/java/com/sameerasw/essentials/ui/modifiers/ProgressiveBlurModifier.kt index a2738e1d2..490fa2f73 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/modifiers/ProgressiveBlurModifier.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/modifiers/ProgressiveBlurModifier.kt @@ -3,8 +3,12 @@ package com.sameerasw.essentials.ui.modifiers import android.graphics.RenderEffect import android.graphics.RuntimeShader import android.os.Build -import androidx.annotation.RequiresApi +import androidx.compose.material3.MaterialTheme import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asComposeRenderEffect import androidx.compose.ui.graphics.graphicsLayer import org.intellij.lang.annotations.Language @@ -75,12 +79,13 @@ private val PROGRESSIVE_BLUR_SKSL = """ fun Modifier.progressiveBlur( blurRadius: Float, height: Float, - direction: BlurDirection = BlurDirection.TOP -): Modifier = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - this.then( + direction: BlurDirection = BlurDirection.TOP, + showGradientOverlay: Boolean = true +): Modifier = composed { + val overlayColor = MaterialTheme.colorScheme.surfaceContainer.copy(alpha = 0.65f) + + val blurModifier = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && blurRadius > 0f) { Modifier.graphicsLayer { - if (blurRadius <= 0f) return@graphicsLayer - val shader = RuntimeShader(PROGRESSIVE_BLUR_SKSL) shader.setFloatUniform("blurRadius", blurRadius) shader.setFloatUniform("height", height) @@ -90,7 +95,28 @@ fun Modifier.progressiveBlur( renderEffect = RenderEffect.createRuntimeShaderEffect(shader, "content") .asComposeRenderEffect() } - ) -} else { - this + } else Modifier + + val gradientModifier = if (showGradientOverlay) { + Modifier.drawWithContent { + drawContent() + val (brush, _) = when (direction) { + BlurDirection.TOP -> { + Brush.verticalGradient( + colors = listOf(overlayColor, Color.Transparent), + endY = height + ) to height + } + BlurDirection.BOTTOM -> { + Brush.verticalGradient( + colors = listOf(Color.Transparent, overlayColor), + startY = size.height - height + ) to height + } + } + drawRect(brush = brush) + } + } else Modifier + + this.then(blurModifier).then(gradientModifier) } diff --git a/app/src/main/java/com/sameerasw/essentials/utils/TimeUtil.kt b/app/src/main/java/com/sameerasw/essentials/utils/TimeUtil.kt index efe3a8987..316246090 100644 --- a/app/src/main/java/com/sameerasw/essentials/utils/TimeUtil.kt +++ b/app/src/main/java/com/sameerasw/essentials/utils/TimeUtil.kt @@ -12,23 +12,39 @@ object TimeUtil { val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US) inputFormat.timeZone = TimeZone.getTimeZone("UTC") val date = inputFormat.parse(githubDate) ?: return githubDate - val now = System.currentTimeMillis() - val diff = now - date.time + formatRelativeDate(date.time, context) + } catch (e: Exception) { + githubDate + } + } - when { - diff < 60000 -> context.getString(R.string.time_just_now) - diff < 3600000 -> context.getString(R.string.time_min_ago, diff / 60000) - diff < 86400000 -> context.getString(R.string.time_hour_ago, diff / 3600000) - diff < 2592000000L -> context.getString(R.string.time_day_ago, diff / 86400000) - diff < 31536000000L -> context.getString( - R.string.time_month_ago, - diff / 2592000000L - ) + fun formatRelativeDate(timestamp: Long, context: Context): String { + val now = System.currentTimeMillis() + val diff = now - timestamp - else -> context.getString(R.string.time_year_ago, diff / 31536000000L) + val days = diff / 86400000L + return when { + diff < 60000L -> context.getString(R.string.time_just_now) + diff < 3600000L -> context.getString(R.string.time_min_ago, (diff / 60000).toInt()) + diff < 86400000L && isSameDay(now, timestamp) -> { + if (diff < 3600000L * 24) { + context.getString(R.string.time_hour_ago, (diff / 3600000L).toInt()) + } else { + context.getString(R.string.today) + } } - } catch (e: Exception) { - githubDate + days == 1L || (days == 0L && !isSameDay(now, timestamp)) -> context.getString(R.string.yesterday) + days < 7L -> context.getString(R.string.time_days_ago, days.toInt()) + days < 30L -> context.getString(R.string.time_weeks_ago, (days / 7).toInt()) + days < 365L -> context.getString(R.string.time_months_ago, (days / 30).toInt()) + else -> context.getString(R.string.time_year_ago, (days / 365).toInt()) } } + + private fun isSameDay(t1: Long, t2: Long): Boolean { + val cal1 = java.util.Calendar.getInstance().apply { timeInMillis = t1 } + val cal2 = java.util.Calendar.getInstance().apply { timeInMillis = t2 } + return cal1.get(java.util.Calendar.YEAR) == cal2.get(java.util.Calendar.YEAR) && + cal1.get(java.util.Calendar.DAY_OF_YEAR) == cal2.get(java.util.Calendar.DAY_OF_YEAR) + } } diff --git a/app/src/main/java/com/sameerasw/essentials/viewmodels/LocationReachedViewModel.kt b/app/src/main/java/com/sameerasw/essentials/viewmodels/LocationReachedViewModel.kt index 3c3f8a4a8..c261fc2b2 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/LocationReachedViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/LocationReachedViewModel.kt @@ -1,6 +1,8 @@ package com.sameerasw.essentials.viewmodels import android.app.Application +import android.content.Intent +import android.widget.Toast import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope @@ -23,11 +25,24 @@ import kotlin.math.pow import kotlin.math.sin import kotlin.math.sqrt +@androidx.annotation.Keep class LocationReachedViewModel(application: Application) : AndroidViewModel(application) { private val repository = LocationReachedRepository(application) private val fusedLocationClient = LocationServices.getFusedLocationProviderClient(application) - var alarm = mutableStateOf(repository.getAlarm()) + var savedAlarms = mutableStateOf>(emptyList()) + private set + + var activeAlarmId = mutableStateOf(null) + private set + + var lastTrip = mutableStateOf(repository.getLastTrip()) + private set + + var tempAlarm = mutableStateOf(null) + private set + + var showBottomSheet = mutableStateOf(false) private set var isProcessingCoordinates = mutableStateOf(false) @@ -39,12 +54,13 @@ class LocationReachedViewModel(application: Application) : AndroidViewModel(appl var startDistance = mutableStateOf(repository.getStartDistance()) private set - init { - // Initial distance check - if (alarm.value.latitude != 0.0 && alarm.value.longitude != 0.0) { - updateCurrentDistance() - } + var remainingTimeMinutes = mutableStateOf(null) + private set + var startTime = mutableStateOf(repository.getStartTime()) + private set + + init { // Observe shared state for real-time updates across activities viewModelScope.launch { LocationReachedRepository.isProcessing.collect { @@ -53,56 +69,116 @@ class LocationReachedViewModel(application: Application) : AndroidViewModel(appl } viewModelScope.launch { - LocationReachedRepository.alarmFlow.collect { newAlarm -> - newAlarm?.let { - alarm.value = it - // Start distance might need refresh if destination changed + LocationReachedRepository.tempAlarm.collect { + tempAlarm.value = it + } + } + + viewModelScope.launch { + LocationReachedRepository.showBottomSheet.collect { + showBottomSheet.value = it + } + } + + viewModelScope.launch { + LocationReachedRepository.alarmsFlow.collect { alarms -> + savedAlarms.value = alarms + } + } + + viewModelScope.launch { + LocationReachedRepository.activeAlarmId.collect { id -> + activeAlarmId.value = id + if (id != null) { + updateCurrentDistance() + } else { + currentDistance.value = null } } } } - fun clearAlarm() { - val clearedAlarm = LocationAlarm(0.0, 0.0, 1000, false) - alarm.value = clearedAlarm - startDistance.value = 0f - repository.saveAlarm(clearedAlarm) - repository.saveStartDistance(0f) - LocationReachedService.stop(getApplication()) - currentDistance.value = null + fun setShowBottomSheet(show: Boolean) { + repository.setShowBottomSheet(show) } - fun startTracking() { - val currentAlarm = alarm.value - if (currentAlarm.latitude != 0.0 && currentAlarm.longitude != 0.0) { - val enabledAlarm = currentAlarm.copy(isEnabled = true) - alarm.value = enabledAlarm - repository.saveAlarm(enabledAlarm) - LocationReachedService.start(getApplication()) - - // Refreshed start distance logic - fusedLocationClient.getCurrentLocation(Priority.PRIORITY_BALANCED_POWER_ACCURACY, null) - .addOnSuccessListener { location -> - location?.let { - val dist = calculateDistance( - it.latitude, it.longitude, - enabledAlarm.latitude, enabledAlarm.longitude - ) - startDistance.value = dist - repository.saveStartDistance(dist) - } - } + fun setTempAlarm(alarm: LocationAlarm?) { + repository.setTempAlarm(alarm) + } + + fun saveAlarm(alarm: LocationAlarm) { + val currentList = savedAlarms.value.toMutableList() + val index = currentList.indexOfFirst { it.id == alarm.id } + if (index != -1) { + currentList[index] = alarm + } else { + currentList.add(alarm) + } + repository.saveAlarms(currentList) + repository.setShowBottomSheet(false) + repository.setTempAlarm(null) + } + + fun deleteAlarm(alarmId: String) { + if (activeAlarmId.value == alarmId) { + stopTracking() } + val currentList = savedAlarms.value.filter { it.id != alarmId } + repository.saveAlarms(currentList) + } + + fun startTracking(alarmId: String) { + val alarm = savedAlarms.value.find { it.id == alarmId } ?: return + + // Stop any previous tracking + if (activeAlarmId.value != null && activeAlarmId.value != alarmId) { + stopTracking() + } + + repository.saveActiveAlarmId(alarmId) + LocationReachedService.start(getApplication()) + + val now = System.currentTimeMillis() + repository.saveStartTime(now) + startTime.value = now + repository.updateLastTravelled(alarmId, now) + + // Refreshed start distance logic + fusedLocationClient.getCurrentLocation(Priority.PRIORITY_BALANCED_POWER_ACCURACY, null) + .addOnSuccessListener { location -> + location?.let { + val dist = calculateDistance( + it.latitude, it.longitude, + alarm.latitude, alarm.longitude + ) + startDistance.value = dist + repository.saveStartDistance(dist) + } + } + + // Clear last trip when starting new + lastTrip.value = null + repository.saveLastTrip(null) } fun stopTracking() { - val currentAlarm = alarm.value - val disabledAlarm = currentAlarm.copy(isEnabled = false) - alarm.value = disabledAlarm - repository.saveAlarm(disabledAlarm) + val id = activeAlarmId.value ?: return + val alarm = savedAlarms.value.find { it.id == id } + + if (alarm != null) { + // Save as last trip + lastTrip.value = alarm + repository.saveLastTrip(alarm) + } + + repository.saveActiveAlarmId(null) LocationReachedService.stop(getApplication()) - // Keep start distance for potential restart? Or maybe just keep coordinates. - // User said "keep last track in memory (only destination)". + currentDistance.value = null + remainingTimeMinutes.value = null + startDistance.value = 0f + repository.saveStartDistance(0f) + repository.saveStartTime(0L) + startTime.value = 0L } private var distanceTrackingJob: kotlinx.coroutines.Job? = null @@ -112,8 +188,7 @@ class LocationReachedViewModel(application: Application) : AndroidViewModel(appl distanceTrackingJob = viewModelScope.launch { while (true) { - // Tracking should only happen if coordinates exist - if (alarm.value.latitude != 0.0 && alarm.value.longitude != 0.0) { + if (activeAlarmId.value != null) { updateCurrentDistance() } else { currentDistance.value = null @@ -134,20 +209,44 @@ class LocationReachedViewModel(application: Application) : AndroidViewModel(appl } @android.annotation.SuppressLint("MissingPermission") - private fun updateCurrentDistance() { + fun updateCurrentDistance() { + val id = activeAlarmId.value + val activeAlarm = savedAlarms.value.find { it.id == id } ?: tempAlarm.value ?: return + fusedLocationClient.getCurrentLocation(Priority.PRIORITY_BALANCED_POWER_ACCURACY, null) .addOnSuccessListener { location -> location?.let { val distance = calculateDistance( it.latitude, it.longitude, - alarm.value.latitude, alarm.value.longitude + activeAlarm.latitude, activeAlarm.longitude ) currentDistance.value = distance + calculateEta(distance) } } } - private fun calculateDistance(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Float { + private fun calculateEta(currentDistMeters: Float) { + val startDistMeters = startDistance.value + val startT = startTime.value + if (startDistMeters <= 0 || startT <= 0L) { + remainingTimeMinutes.value = null + return + } + + val elapsedMillis = System.currentTimeMillis() - startT + val distanceTravelled = startDistMeters - currentDistMeters + + if (distanceTravelled <= 0 || elapsedMillis <= 0) { + remainingTimeMinutes.value = null + return + } + + val remainingMillis = (currentDistMeters * elapsedMillis / distanceTravelled).toLong() + remainingTimeMinutes.value = (remainingMillis / 60000).toInt().coerceAtLeast(1) + } + + fun calculateDistance(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Float { val r = 6371e3 // Earth's radius in meters val phi1 = lat1 * PI / 180 val phi2 = lat2 * PI / 180 @@ -162,18 +261,7 @@ class LocationReachedViewModel(application: Application) : AndroidViewModel(appl return (r * c).toFloat() } - fun updateAlarm(newAlarm: LocationAlarm) { - val oldAlarm = alarm.value - alarm.value = newAlarm - repository.saveAlarm(newAlarm) - - // If coordinates changed, refresh distance immediately - if (oldAlarm.latitude != newAlarm.latitude || oldAlarm.longitude != newAlarm.longitude) { - updateCurrentDistance() - } - } - - fun handleIntent(intent: android.content.Intent): Boolean { + fun handleIntent(intent: Intent): Boolean { val action = intent.action val type = intent.type val data = intent.data @@ -184,15 +272,15 @@ class LocationReachedViewModel(application: Application) : AndroidViewModel(appl ) val textToParse = when { - action == android.content.Intent.ACTION_SEND && type == "text/plain" -> { - intent.getStringExtra(android.content.Intent.EXTRA_TEXT) + action == Intent.ACTION_SEND && type == "text/plain" -> { + intent.getStringExtra(Intent.EXTRA_TEXT) } - action == android.content.Intent.ACTION_VIEW && data?.scheme == "geo" -> { + action == Intent.ACTION_VIEW && data?.scheme == "geo" -> { data.toString() } - action == android.content.Intent.ACTION_VIEW && (data?.host?.contains("google.com") == true || data?.host?.contains( + action == Intent.ACTION_VIEW && (data?.host?.contains("google.com") == true || data?.host?.contains( "goo.gl" ) == true) -> { data.toString() @@ -205,19 +293,16 @@ class LocationReachedViewModel(application: Application) : AndroidViewModel(appl // Check if it's a shortened URL that needs resolution if (textToParse.contains("maps.app.goo.gl") || textToParse.contains("goo.gl/maps")) { + repository.setShowBottomSheet(true) resolveAndParse(textToParse) - return true // Navigate to settings while resolving + return true } return tryParseAndSet(textToParse) } private fun tryParseAndSet(text: String): Boolean { - // Broad regex for coordinates: looks for two floats separated by a comma - // Supports: "40.7127, -74.0059", "geo:40.7127,-74.0059", "@40.7127,-74.0059", "q=40.7127,-74.0059" val commaRegex = Regex("(-?\\d+\\.\\d+)\\s*,\\s*(-?\\d+\\.\\d+)") - - // Pattern for Google Maps data URLs: !3d40.7127!4d-74.0059 val dataRegex = Regex("!3d(-?\\d+\\.\\d+)!4d(-?\\d+\\.\\d+)") val match = commaRegex.find(text) ?: dataRegex.find(text) @@ -228,13 +313,14 @@ class LocationReachedViewModel(application: Application) : AndroidViewModel(appl if (lat != 0.0 && lng != 0.0) { android.util.Log.d("LocationReachedVM", "Parsed coordinates: $lat, $lng") - // Staging mode: don't enable yet - updateAlarm(alarm.value.copy(latitude = lat, longitude = lng, isEnabled = false)) - android.widget.Toast.makeText( - getApplication(), getApplication().getString( - R.string.location_reached_toast_set, lat, lng - ), android.widget.Toast.LENGTH_SHORT - ).show() + repository.setTempAlarm(LocationAlarm( + latitude = lat, + longitude = lng, + name = "New Destination", + isEnabled = false + )) + repository.setShowBottomSheet(true) + updateCurrentDistance() repository.setIsProcessing(false) return true } @@ -263,30 +349,20 @@ class LocationReachedViewModel(application: Application) : AndroidViewModel(appl } android.util.Log.d("LocationReachedVM", "Resolved URL: $resolvedUrl") if (!tryParseAndSet(resolvedUrl)) { - // Additional check for @lat,lng which might not have spaces or exactly match the above val pathRegex = Regex("@(-?\\d+\\.\\d+),(-?\\d+\\.\\d+)") val pathMatch = pathRegex.find(resolvedUrl) if (pathMatch != null) { val lat = pathMatch.groupValues[1].toDoubleOrNull() ?: 0.0 val lng = pathMatch.groupValues[2].toDoubleOrNull() ?: 0.0 if (lat != 0.0 && lng != 0.0) { - // Staging mode: don't enable yet - updateAlarm( - alarm.value.copy( - latitude = lat, - longitude = lng, - isEnabled = false - ) - ) - android.widget.Toast.makeText( - getApplication(), - getApplication().getString( - R.string.location_reached_toast_set, - lat, - lng - ), - android.widget.Toast.LENGTH_SHORT - ).show() + repository.setTempAlarm(LocationAlarm( + latitude = lat, + longitude = lng, + name = "New Destination", + isEnabled = false + )) + repository.setShowBottomSheet(true) + updateCurrentDistance() } } repository.setIsProcessing(false) diff --git a/app/src/main/res/drawable/flashbang.gif b/app/src/main/res/drawable/flashbang.gif new file mode 100644 index 000000000..7aab74c5a Binary files /dev/null and b/app/src/main/res/drawable/flashbang.gif differ diff --git a/app/src/main/res/drawable/round_navigation_24.xml b/app/src/main/res/drawable/round_navigation_24.xml new file mode 100644 index 000000000..52778d649 --- /dev/null +++ b/app/src/main/res/drawable/round_navigation_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/round_play_arrow_24.xml b/app/src/main/res/drawable/round_play_arrow_24.xml new file mode 100644 index 000000000..42c14a76c --- /dev/null +++ b/app/src/main/res/drawable/round_play_arrow_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_account_balance_24.xml b/app/src/main/res/drawable/rounded_account_balance_24.xml new file mode 100644 index 000000000..0dc270577 --- /dev/null +++ b/app/src/main/res/drawable/rounded_account_balance_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_apartment_24.xml b/app/src/main/res/drawable/rounded_apartment_24.xml new file mode 100644 index 000000000..a9bb974bf --- /dev/null +++ b/app/src/main/res/drawable/rounded_apartment_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_beach_access_24.xml b/app/src/main/res/drawable/rounded_beach_access_24.xml new file mode 100644 index 000000000..cd3c2e5b8 --- /dev/null +++ b/app/src/main/res/drawable/rounded_beach_access_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_diagnosis_24.xml b/app/src/main/res/drawable/rounded_diagnosis_24.xml new file mode 100644 index 000000000..54c9ea95d --- /dev/null +++ b/app/src/main/res/drawable/rounded_diagnosis_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_directions_boat_24.xml b/app/src/main/res/drawable/rounded_directions_boat_24.xml new file mode 100644 index 000000000..a0057825e --- /dev/null +++ b/app/src/main/res/drawable/rounded_directions_boat_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_directions_bus_24.xml b/app/src/main/res/drawable/rounded_directions_bus_24.xml new file mode 100644 index 000000000..e58032b7b --- /dev/null +++ b/app/src/main/res/drawable/rounded_directions_bus_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_favorite_24.xml b/app/src/main/res/drawable/rounded_favorite_24.xml index a73733a03..a9e9a09bf 100644 --- a/app/src/main/res/drawable/rounded_favorite_24.xml +++ b/app/src/main/res/drawable/rounded_favorite_24.xml @@ -1,5 +1,5 @@ - + diff --git a/app/src/main/res/drawable/rounded_fork_spoon_24.xml b/app/src/main/res/drawable/rounded_fork_spoon_24.xml new file mode 100644 index 000000000..bc43432cd --- /dev/null +++ b/app/src/main/res/drawable/rounded_fork_spoon_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_garage_home_24.xml b/app/src/main/res/drawable/rounded_garage_home_24.xml new file mode 100644 index 000000000..82ab52e7f --- /dev/null +++ b/app/src/main/res/drawable/rounded_garage_home_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_history_24.xml b/app/src/main/res/drawable/rounded_history_24.xml new file mode 100644 index 000000000..948b8d34c --- /dev/null +++ b/app/src/main/res/drawable/rounded_history_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_home_24.xml b/app/src/main/res/drawable/rounded_home_24.xml new file mode 100644 index 000000000..d971995e4 --- /dev/null +++ b/app/src/main/res/drawable/rounded_home_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_home_health_24.xml b/app/src/main/res/drawable/rounded_home_health_24.xml new file mode 100644 index 000000000..43c5c05a1 --- /dev/null +++ b/app/src/main/res/drawable/rounded_home_health_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_local_pizza_24.xml b/app/src/main/res/drawable/rounded_local_pizza_24.xml new file mode 100644 index 000000000..7c555d8a8 --- /dev/null +++ b/app/src/main/res/drawable/rounded_local_pizza_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_school_24.xml b/app/src/main/res/drawable/rounded_school_24.xml new file mode 100644 index 000000000..fdf7a026f --- /dev/null +++ b/app/src/main/res/drawable/rounded_school_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_search_check_2_24.xml b/app/src/main/res/drawable/rounded_search_check_2_24.xml new file mode 100644 index 000000000..a185dd830 --- /dev/null +++ b/app/src/main/res/drawable/rounded_search_check_2_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_shopping_cart_24.xml b/app/src/main/res/drawable/rounded_shopping_cart_24.xml new file mode 100644 index 000000000..fffc5b47f --- /dev/null +++ b/app/src/main/res/drawable/rounded_shopping_cart_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_snowflake_24.xml b/app/src/main/res/drawable/rounded_snowflake_24.xml new file mode 100644 index 000000000..765e8054a --- /dev/null +++ b/app/src/main/res/drawable/rounded_snowflake_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_storefront_24.xml b/app/src/main/res/drawable/rounded_storefront_24.xml new file mode 100644 index 000000000..545e73bde --- /dev/null +++ b/app/src/main/res/drawable/rounded_storefront_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_train_24.xml b/app/src/main/res/drawable/rounded_train_24.xml new file mode 100644 index 000000000..442a455f4 --- /dev/null +++ b/app/src/main/res/drawable/rounded_train_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_work_24.xml b/app/src/main/res/drawable/rounded_work_24.xml new file mode 100644 index 000000000..66c3b8a69 --- /dev/null +++ b/app/src/main/res/drawable/rounded_work_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/raw/keep.xml b/app/src/main/res/raw/keep.xml new file mode 100644 index 000000000..7b638acc7 --- /dev/null +++ b/app/src/main/res/raw/keep.xml @@ -0,0 +1,20 @@ + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 285755d2f..09f762b01 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -11,6 +11,7 @@ Flashlight Pulse Check for pre-releases Might be unstable + Default tab Security @@ -117,6 +118,16 @@ App Control Freeze Unfreeze + Remove + Create shortcut + App info + What is Freeze? + App freezing disables the app\'s launch activity which removes it from the app list and updates. It will prevent the app from starting at all until it\'s unfrozen which saves resources but you will need to unfreeze from here or manually re-enable. + DO NOT FREEZE COMMUNICATION APPS + + What is Suspend? + Suspending an app used to pause the app activity and prevent background executions but with recent Android changes, it only pauses the notifications from appearing and that\'s pretty much it. But does allows you to unpause from the launcher app list as they will be still available as grayscale paused app icons. + Should work the same as the native app pause/ focus mode features. More options Freeze all apps Unfreeze all apps @@ -605,6 +616,7 @@ Search Stop Search + Search frozen apps Back @@ -660,7 +672,30 @@ Automatically toggle your screen blue light filter based on the foreground app. Enhance security when your device is locked.\n\nRestrict access to some sensitive QS tiles preventing unauthorized network modifications and further preventing them re-attempting to do so by increasing the animation speed to prevent touch spam.\n\nThis feature is not robust and may have flaws such as some tiles which allow toggling directly such as bluetooth or flight mode not being able to be prevented. Secure your apps with a secondary authentication layer.\n\nYour device lock screen authentication method will be used as long as it meets the class 3 biometric security level by Android standards. - Get notified when you get closer to your destination to ensure you never miss the stop.\n\nGo to Google Maps, long press a pin nearby to your destination and make sure it says "Dropped pin" (Otherwise the distance calculation might not be accurate), And then share the location to the Essentials app and start tracking. + Get notified when you get closer to your destination to ensure you never miss the stop.\n\nGo to Google Maps, long press a pin nearby to your destination and make sure it says \"Dropped pin\" (Otherwise the distance calculation might not be accurate), And then share the location to the Essentials app and start tracking. + Add Destination + Edit Destination + Home, Office, etc. + Name + Save + Cancel + Resolving location… + Last Trip + Saved Destinations + No destinations saved yet. + Delete Destination + Tracking Now + Re-Start + Share coordinates (Dropped pin) from Google Maps to Essentials to save as a destination.\n\nThe distance shown is the direct distance to the destination, not the distance along the roads.\n\nTake all calculations of time and distance with a grain of salt as they are not always accurate. + Are we there yet? + Radius: %1$d m + Distance to target: %1$s + Last: %1$s + Never + To go + %1$d min + %1$d hr %2$d min + %1$s (%2$d%%) • %3$s to go Freeze apps to stop them from running in the background.\n\nPrevent battery drain and data usage by completely freezing apps when you are not using them. They will be unfrozen instantly when you launch them. The apps will not show up in the app drawer and also will not show up for app updates in Play Store while frozen. A custom input method no-one asked for.\n\nIt is just an experiment. Multiple languages may not get support as it is a very complex and time consuming implementation. Monitor battery levels of all your connected devices.\n\nSee the battery status of your Bluetooth headphones, watch, and other accessories in one place. Connect with AirSync application to display your mac battery level as well. @@ -1137,10 +1172,15 @@ just now + Today + Yesterday %1$dm ago %1$dh ago %1$dd ago + %1$d days ago + %1$d weeks ago %1$dmo ago + %1$d months ago %1$dy ago Retry @@ -1282,4 +1322,11 @@ Feedback sent successfully! Thanks for helping us improve the app. Alternatively + + Diagnostics + Device Check + Get ready to be flashbanged! + Abort + Continue + \ No newline at end of file