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