diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/data/room/AppDatabase.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/data/room/AppDatabase.kt index c2a8b6e1a25281da91795d82984b9ba468e048b8..982b33558e66f8a49c239f39476e3f792ff508ed 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/data/room/AppDatabase.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/data/room/AppDatabase.kt @@ -28,8 +28,8 @@ import androidx.sqlite.db.SupportSQLiteDatabase import earth.maps.cardinal.data.DownloadStatusConverter @Database( - entities = [OfflineArea::class, RoutingProfile::class, DownloadedTile::class, SavedList::class, SavedPlace::class, ListItem::class], - version = 10, + entities = [OfflineArea::class, RoutingProfile::class, DownloadedTile::class, SavedList::class, SavedPlace::class, ListItem::class, RecentSearch::class], + version = 11, exportSchema = false ) @TypeConverters(TileTypeConverter::class, DownloadStatusConverter::class, ItemTypeConverter::class) @@ -40,6 +40,7 @@ abstract class AppDatabase : RoomDatabase() { abstract fun savedListDao(): SavedListDao abstract fun savedPlaceDao(): SavedPlaceDao abstract fun listItemDao(): ListItemDao + abstract fun recentSearchDao(): RecentSearchDao companion object { @Volatile @@ -190,6 +191,30 @@ abstract class AppDatabase : RoomDatabase() { } } + private val MIGRATION_10_11 = object : Migration(10, 11) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + """ + CREATE TABLE IF NOT EXISTS recent_searches ( + id TEXT PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + description TEXT NOT NULL, + icon TEXT NOT NULL, + latitude REAL NOT NULL, + longitude REAL NOT NULL, + houseNumber TEXT, + road TEXT, + city TEXT, + state TEXT, + postcode TEXT, + country TEXT, + countryCode TEXT, + tappedAt INTEGER NOT NULL + ) + """.trimIndent() + ) + } + } fun getDatabase(context: Context): AppDatabase { return INSTANCE ?: synchronized(this) { @@ -204,6 +229,7 @@ abstract class AppDatabase : RoomDatabase() { MIGRATION_7_8, MIGRATION_8_9, MIGRATION_9_10, + MIGRATION_10_11, ).build() INSTANCE = instance instance diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/data/room/RecentSearch.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/data/room/RecentSearch.kt new file mode 100644 index 0000000000000000000000000000000000000000..d48bfeba610fa1981caac53200bba43861bd9d4d --- /dev/null +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/data/room/RecentSearch.kt @@ -0,0 +1,66 @@ +/* + * Cardinal Maps + * Copyright (C) 2025 Cardinal Maps Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package earth.maps.cardinal.data.room + +import androidx.room.Entity +import androidx.room.PrimaryKey +import earth.maps.cardinal.data.Place +import java.util.UUID + +@Entity(tableName = "recent_searches") +data class RecentSearch( + @PrimaryKey val id: String, // UUID string + val name: String, + val description: String, + val icon: String, + val latitude: Double, + val longitude: Double, + // Address fields + val houseNumber: String? = null, + val road: String? = null, + val city: String? = null, + val state: String? = null, + val postcode: String? = null, + val country: String? = null, + val countryCode: String? = null, + val tappedAt: Long +) { + companion object { + fun fromPlace(place: Place): RecentSearch { + val timestamp = System.currentTimeMillis() + + return RecentSearch( + id = UUID.randomUUID().toString(), + name = place.name, + description = place.description, + icon = place.icon, + latitude = place.latLng.latitude, + longitude = place.latLng.longitude, + houseNumber = place.address?.houseNumber, + road = place.address?.road, + city = place.address?.city, + state = place.address?.state, + postcode = place.address?.postcode, + country = place.address?.country, + countryCode = place.address?.countryCode, + tappedAt = timestamp, + ) + } + } +} diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/data/room/RecentSearchDao.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/data/room/RecentSearchDao.kt new file mode 100644 index 0000000000000000000000000000000000000000..d17be4c0e713ed8d8f9c7d8d3d80c5c453a0ed47 --- /dev/null +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/data/room/RecentSearchDao.kt @@ -0,0 +1,43 @@ +/* + * Cardinal Maps + * Copyright (C) 2025 Cardinal Maps Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package earth.maps.cardinal.data.room + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import kotlinx.coroutines.flow.Flow + +@Dao +interface RecentSearchDao { + @Query("SELECT * FROM recent_searches ORDER BY tappedAt DESC") + fun getRecentSearches(): Flow> + + @Insert + suspend fun insertSearch(search: RecentSearch) + + @Delete + suspend fun deleteSearch(search: RecentSearch) + + @Query("DELETE FROM recent_searches WHERE id NOT IN (SELECT id FROM recent_searches ORDER BY tappedAt DESC LIMIT :keepCount)") + suspend fun deleteOldSearches(keepCount: Int) + + @Query("DELETE FROM recent_searches") + suspend fun clearAllSearches() +} diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/data/room/RecentSearchRepository.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/data/room/RecentSearchRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..5ae8ffc1c9a9864b2030c0eb4e64828f15ec942e --- /dev/null +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/data/room/RecentSearchRepository.kt @@ -0,0 +1,107 @@ +/* + * Cardinal Maps + * Copyright (C) 2025 Cardinal Maps Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package earth.maps.cardinal.data.room + +import earth.maps.cardinal.data.Address +import earth.maps.cardinal.data.LatLng +import earth.maps.cardinal.data.Place +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class RecentSearchRepository @Inject constructor( + database: AppDatabase, +) { + private val searchDao = database.recentSearchDao() + + companion object { + const val MAX_RECENT_SEARCHES = 20 + } + + /** + * Adds a recent search and ensures we don't exceed the maximum count. + */ + suspend fun addRecentSearch(place: Place): Result = withContext(Dispatchers.IO) { + try { + val recentSearch = RecentSearch.fromPlace(place) + searchDao.insertSearch(recentSearch) + searchDao.deleteOldSearches(MAX_RECENT_SEARCHES) + Result.success(Unit) + } catch (e: Exception) { + Result.failure(e) + } + } + + /** + * Converts a RecentSearch back to a Place for UI consumption. + */ + fun toPlace(recentSearch: RecentSearch): Place { + return Place( + id = recentSearch.id, + name = recentSearch.name, + description = recentSearch.description, + icon = recentSearch.icon, + latLng = LatLng( + latitude = recentSearch.latitude, longitude = recentSearch.longitude + ), + address = if (recentSearch.houseNumber != null || recentSearch.road != null || recentSearch.city != null || recentSearch.state != null || recentSearch.postcode != null || recentSearch.country != null || recentSearch.countryCode != null) { + Address( + houseNumber = recentSearch.houseNumber, + road = recentSearch.road, + city = recentSearch.city, + state = recentSearch.state, + postcode = recentSearch.postcode, + country = recentSearch.country, + countryCode = recentSearch.countryCode + ) + } else { + null + } + ) + } + + /** + * Gets recent searches with an optional limit (defaults to 10 for UI display). + */ + fun getRecentSearches(limit: Int = 10): Flow> { + return searchDao.getRecentSearches().map { list -> + list.distinctBy { it.copy(id = "", tappedAt = 0) }.take(limit) + } + } + + /** + * Remove a a RecentSearch from the database, along with all duplicates that may have different IDs or timestamps. + */ + suspend fun removeRecentSearch(searchToDelete: RecentSearch) { + searchDao.deleteSearch(searchToDelete) + + // A subtle point: There may be duplicates filtered out by the + // distinctBy logic above, and they should be removed too. + searchDao.getRecentSearches().firstOrNull()?.filter { + it.copy(id = "", tappedAt = 0) == searchToDelete.copy(id = "", tappedAt = 0) + }?.forEach { + searchDao.deleteSearch(it) + } + } +} diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/di/DatabaseModule.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/di/DatabaseModule.kt index 9740124276b653aeddae6684d017b1d23252dbe1..4898451f286eec299a8beb889413d4e470663b1d 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/di/DatabaseModule.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/di/DatabaseModule.kt @@ -28,6 +28,7 @@ import earth.maps.cardinal.data.room.AppDatabase import earth.maps.cardinal.data.room.DownloadedTileDao import earth.maps.cardinal.data.room.ListItemDao import earth.maps.cardinal.data.room.OfflineAreaDao +import earth.maps.cardinal.data.room.RecentSearchDao import earth.maps.cardinal.data.room.SavedListDao import earth.maps.cardinal.data.room.SavedPlaceDao import javax.inject.Singleton @@ -66,4 +67,9 @@ object DatabaseModule { fun provideListItemDao(appDatabase: AppDatabase): ListItemDao { return appDatabase.listItemDao() } + + @Provides + fun provideRecentSearchDao(appDatabase: AppDatabase): RecentSearchDao { + return appDatabase.recentSearchDao() + } } diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/directions/DirectionsScreen.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/directions/DirectionsScreen.kt index 78c3cb10deec51275080f07262db078819b1e5ec..3b4d7d4f01d1b49bfdac10a1ae722a458fc7ff15 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/directions/DirectionsScreen.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/directions/DirectionsScreen.kt @@ -619,6 +619,8 @@ private fun updatePlaceForField( } else { viewModel.updateToPlace(place) } + // Track as recent search when selected from search results + viewModel.onPlaceSelectedFromSearch(place) } private fun handleMyLocationSelected( diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/directions/DirectionsViewModel.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/directions/DirectionsViewModel.kt index d0d9aaa2a1e95855820a6291983894444b971d3e..44ec17045b3feb2c52eb568bf9780a594791462a 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/directions/DirectionsViewModel.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/directions/DirectionsViewModel.kt @@ -37,6 +37,7 @@ import earth.maps.cardinal.data.LocationRepository import earth.maps.cardinal.data.Place import earth.maps.cardinal.data.RoutingMode import earth.maps.cardinal.data.ViewportRepository +import earth.maps.cardinal.data.room.RecentSearchRepository import earth.maps.cardinal.data.room.RoutingProfile import earth.maps.cardinal.data.room.RoutingProfileRepository import earth.maps.cardinal.data.room.SavedPlaceDao @@ -97,6 +98,7 @@ class DirectionsViewModel @Inject constructor( private val routeRepository: RouteRepository, private val appPreferenceRepository: AppPreferenceRepository, private val transitousService: TransitousService, + private val recentSearchRepository: RecentSearchRepository, ) : ViewModel() { // Search query flow for debouncing @@ -536,6 +538,16 @@ class DirectionsViewModel @Inject constructor( return locationRepository.createMyLocationPlace(latLng) } + /** + * Called when a search result is selected from directions search. + * Adds the place to recent searches. + */ + fun onPlaceSelectedFromSearch(place: Place) { + viewModelScope.launch { + recentSearchRepository.addRecentSearch(place) + } + } + companion object { private const val TAG = "DirectionsViewModel" } diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/home/HomeScreen.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/home/HomeScreen.kt index 832f59eb07c6940ff35496c51b53ee7b1ed8cb17..66909228ec4ff505f0f45f1b5591b25f7e7309ba 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/home/HomeScreen.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/home/HomeScreen.kt @@ -26,6 +26,7 @@ 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.lazy.LazyColumn @@ -47,6 +48,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester @@ -62,16 +64,15 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import earth.maps.cardinal.R.dimen import earth.maps.cardinal.R.drawable import earth.maps.cardinal.R.string import earth.maps.cardinal.data.AddressFormatter import earth.maps.cardinal.data.GeocodeResult import earth.maps.cardinal.data.Place +import earth.maps.cardinal.ui.core.TOOLBAR_HEIGHT_DP import earth.maps.cardinal.ui.place.SearchResultItem -import earth.maps.cardinal.ui.saved.SavedPlacesList -import earth.maps.cardinal.ui.saved.SavedPlacesViewModel +import kotlinx.coroutines.launch import kotlin.math.abs @SuppressLint("ConfigurationScreenWidthHeight") @@ -148,7 +149,11 @@ private fun SearchPanelContent( ContentBelow( homeInSearchScreen = homeInSearchScreen, geocodePlaces = geocodePlaces, - onPlaceSelected = onPlaceSelected, + viewModel = viewModel, + onPlaceSelected = { place -> + viewModel.onPlaceSelected(place) + onPlaceSelected(place) + }, addressFormatter = addressFormatter ) } @@ -286,9 +291,13 @@ private fun PinnedPlacesRow( private fun ContentBelow( homeInSearchScreen: Boolean, geocodePlaces: List, + viewModel: HomeViewModel, onPlaceSelected: (Place) -> Unit, addressFormatter: AddressFormatter, ) { + val recentSearches by viewModel.recentSearches().collectAsState(emptyList()) + val coroutineScope = rememberCoroutineScope() + if (homeInSearchScreen) { LazyColumn { items(geocodePlaces) { @@ -301,8 +310,24 @@ private fun ContentBelow( } Spacer(modifier = Modifier.fillMaxSize()) } else { - val savedPlacesViewModel = hiltViewModel() - SavedPlacesList(savedPlacesViewModel, onPlaceSelected = onPlaceSelected) + LazyColumn { + // Show recent searches if any. + items(recentSearches) { recentSearch -> + SearchResultItem( + addressFormatter = addressFormatter, + place = viewModel.searchToPlace(recentSearch), + onPlaceSelected = onPlaceSelected, + onRemoveTapped = { + coroutineScope.launch { viewModel.removeRecentSearch(recentSearch) } + } + ) + } + item { + Spacer(modifier = Modifier + .fillMaxWidth() + .height(TOOLBAR_HEIGHT_DP)) + } + } } } diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/home/HomeViewModel.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/home/HomeViewModel.kt index 695a283327f889e5c57df517897180339f10053c..2da9cb922766a7265057239cddd8856d470eeda7 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/home/HomeViewModel.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/home/HomeViewModel.kt @@ -30,6 +30,8 @@ import earth.maps.cardinal.data.LocationRepository import earth.maps.cardinal.data.Place import earth.maps.cardinal.data.ViewportRepository import earth.maps.cardinal.data.deduplicateSearchResults +import earth.maps.cardinal.data.room.RecentSearch +import earth.maps.cardinal.data.room.RecentSearchRepository import earth.maps.cardinal.data.room.SavedPlaceDao import earth.maps.cardinal.data.room.SavedPlaceRepository import earth.maps.cardinal.geocoding.GeocodingService @@ -54,6 +56,7 @@ class HomeViewModel @Inject constructor( private val viewportRepository: ViewportRepository, private val locationRepository: LocationRepository, private val savedPlaceRepository: SavedPlaceRepository, + private val recentSearchRepository: RecentSearchRepository, ) : ViewModel() { // Whether the home screen is in a search state. @@ -135,4 +138,29 @@ class HomeViewModel @Inject constructor( fun expandSearch() { _searchExpanded.value = true } + + /** + * Called when a search result is selected/tapped. + * Adds the place to recent searches. + */ + fun onPlaceSelected(place: Place) { + viewModelScope.launch { + recentSearchRepository.addRecentSearch(place) + } + } + + /** + * Gets recent searches. + */ + fun recentSearches(): Flow> { + return recentSearchRepository.getRecentSearches() + } + + fun searchToPlace(search: RecentSearch): Place { + return recentSearchRepository.toPlace(search) + } + + suspend fun removeRecentSearch(recentSearch: RecentSearch) { + recentSearchRepository.removeRecentSearch(recentSearch) + } } diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/place/SearchResults.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/place/SearchResults.kt index 694e80162eb27716babba10d2943c589ba78d5ab..7cb1a36ebf095a520533dbc588645f9c93d3ef21 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/place/SearchResults.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/place/SearchResults.kt @@ -30,6 +30,7 @@ import androidx.compose.foundation.lazy.items import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -38,15 +39,16 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.dimensionResource 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 earth.maps.cardinal.R import earth.maps.cardinal.R.dimen import earth.maps.cardinal.R.drawable import earth.maps.cardinal.data.AddressFormatter import earth.maps.cardinal.data.GeocodeResult import earth.maps.cardinal.data.Place import earth.maps.cardinal.data.format -import earth.maps.cardinal.ui.place.SearchResultsViewModel @Composable fun SearchResults( @@ -72,7 +74,8 @@ fun SearchResults( fun SearchResultItem( addressFormatter: AddressFormatter, place: Place, - onPlaceSelected: (Place) -> Unit + onPlaceSelected: (Place) -> Unit, + onRemoveTapped: (() -> Unit)? = null, ) { Card( modifier = Modifier @@ -123,6 +126,17 @@ fun SearchResultItem( ) } } + + onRemoveTapped?.let { onRemoveTapped -> + IconButton(onClick = onRemoveTapped) { + Icon( + painter = painterResource(drawable.ic_close), + contentDescription = stringResource( + R.string.remove_recent_search + ) + ) + } + } } } } diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/saved/SavedPlaces.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/saved/SavedPlaces.kt deleted file mode 100644 index 609c284e8da920b26ed227166bf6864ee6670488..0000000000000000000000000000000000000000 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/saved/SavedPlaces.kt +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Cardinal Maps - * Copyright (C) 2025 Cardinal Maps Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package earth.maps.cardinal.ui.saved - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.dimensionResource -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 earth.maps.cardinal.R.dimen -import earth.maps.cardinal.R.drawable -import earth.maps.cardinal.R.string -import earth.maps.cardinal.data.Place -import earth.maps.cardinal.data.room.SavedPlace -import earth.maps.cardinal.ui.core.TOOLBAR_HEIGHT_DP - -@Composable -fun SavedPlacesList( - viewModel: SavedPlacesViewModel, onPlaceSelected: (Place) -> Unit -) { - val savedPlaces by viewModel.observeAllPlaces().collectAsState(emptyList()) - - Box(modifier = Modifier.fillMaxWidth()) { - if (savedPlaces.isEmpty()) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.align( - Alignment.Center - ) - ) { - Text( - stringResource(string.no_saved_places_yet), - style = MaterialTheme.typography.bodyLarge - ) - Icon( - modifier = Modifier - .size(64.dp) - .padding(dimensionResource(dimen.padding)), - painter = painterResource(drawable.ic_add_location), - contentDescription = stringResource(string.add_save_places_and_they_ll_show_up_here) - ) - } - } else { - LazyColumn { - items(savedPlaces) { place -> - PlaceItem(place = place, onClick = { - onPlaceSelected(viewModel.convertToPlace(place)) - }) - } - item { - Spacer( - modifier = Modifier - .fillMaxWidth() - .height(TOOLBAR_HEIGHT_DP + dimensionResource(dimen.padding_minor)) - ) - } - } - } - } -} - -@Composable -private fun PlaceItem(place: SavedPlace, onClick: () -> Unit) { - val name = place.customName ?: place.name - val description = place.customDescription ?: place.type - Card( - modifier = Modifier - .fillMaxWidth() - .padding( - vertical = dimensionResource(dimen.padding) / 2, - ) - .clickable( - true, onClick = onClick - ), onClick = onClick, elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(dimensionResource(dimen.padding)), - verticalAlignment = Alignment.CenterVertically - ) { - // Place icon (simplified) - // Place icon and pinned indicator - Box( - modifier = Modifier - .size(40.dp) - .padding(dimensionResource(dimen.padding) / 2), - contentAlignment = Alignment.Center - ) { - Row { - Icon( - painter = painterResource( - when (place.icon) { - "home" -> drawable.ic_home - "work" -> drawable.ic_work - else -> drawable.ic_location_on - } - ), - contentDescription = place.name, - modifier = Modifier.size(dimensionResource(dimen.icon_size)), - tint = MaterialTheme.colorScheme.primary - ) - if (place.isPinned) { - Icon( - painter = painterResource(drawable.ic_bookmark_star), - contentDescription = stringResource(string.pin_place), - modifier = Modifier.size(dimensionResource(dimen.icon_size)), - tint = MaterialTheme.colorScheme.primary - ) - } - } - } - - // Place details - Column( - modifier = Modifier - .weight(1f) - .padding(start = dimensionResource(dimen.padding)) - ) { - Text( - text = name, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold - ) - Text( - text = description, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } -} diff --git a/cardinal-android/app/src/main/res/values/strings.xml b/cardinal-android/app/src/main/res/values/strings.xml index b50911f83813d1b0b9b159e9fad21cc248ba6e4a..7cb44f3de20e9c642427c0f4dac23bfe65deaf35 100644 --- a/cardinal-android/app/src/main/res/values/strings.xml +++ b/cardinal-android/app/src/main/res/values/strings.xml @@ -248,4 +248,5 @@ Zoom in Zoom out Searching… + Remove recent search