diff --git a/app/build.gradle b/app/build.gradle index 132504f6..3200670b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -68,7 +68,7 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.recyclerview:recyclerview:1.1.0' implementation 'com.google.maps.android:android-maps-utils:0.5' - implementation 'com.github.johan12345:CustomBottomSheetBehavior:8ec7fee516' + implementation 'com.github.johan12345:CustomBottomSheetBehavior:f5195b3266' implementation 'com.google.android.gms:play-services-maps:17.0.0' implementation 'com.google.android.gms:play-services-location:17.0.0' implementation 'com.google.android.libraries.places:places:2.2.0' diff --git a/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt b/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt index cff1a1c8..74576cbe 100644 --- a/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt +++ b/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt @@ -13,6 +13,7 @@ import net.vonforst.evmap.R import net.vonforst.evmap.api.availability.ChargepointStatus import net.vonforst.evmap.api.goingelectric.ChargeLocation import net.vonforst.evmap.api.goingelectric.Chargepoint +import net.vonforst.evmap.viewmodel.FavoritesViewModel interface Equatable { override fun equals(other: Any?): Boolean; @@ -29,15 +30,15 @@ abstract class DataBindingAdapter() : } override fun onBindViewHolder(holder: ViewHolder, position: Int) = - holder.bind(getItem(position)) + bind(holder, getItem(position)) - class ViewHolder(private val binding: ViewDataBinding) : + class ViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) { + } - fun bind(item: T) { - binding.setVariable(BR.item, item) - binding.executePendingBindings() - } + open fun bind(holder: ViewHolder, item: T) { + holder.binding.setVariable(BR.item, item) + holder.binding.executePendingBindings() } class DiffCallback : DiffUtil.ItemCallback() { @@ -112,3 +113,13 @@ fun buildDetails(loc: ChargeLocation?, ctx: Context): List else null ) } + + +class FavoritesAdapter(val vm: FavoritesViewModel) : DataBindingAdapter() { + override fun getItemViewType(position: Int): Int = R.layout.item_favorite + + override fun bind(holder: ViewHolder, item: ChargeLocation) { + holder.binding.setVariable(BR.vm, vm) + super.bind(holder, item) + } +} \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricModel.kt b/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricModel.kt index 64281e75..fbd01804 100644 --- a/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricModel.kt +++ b/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricModel.kt @@ -44,7 +44,7 @@ data class ChargeLocation( //val chargecards: Boolean? @Embedded val openinghours: OpeningHours?, @Embedded val cost: Cost? -) : ChargepointListItem() { +) : ChargepointListItem(), Equatable { val maxPower: Double get() { return chargepoints.map { it.power }.max() ?: 0.0 diff --git a/app/src/main/java/net/vonforst/evmap/fragment/FavoritesFragment.kt b/app/src/main/java/net/vonforst/evmap/fragment/FavoritesFragment.kt new file mode 100644 index 00000000..45ede5db --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/fragment/FavoritesFragment.kt @@ -0,0 +1,94 @@ +package net.vonforst.evmap.fragment + +import android.Manifest +import android.content.pm.PackageManager +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import androidx.core.content.ContextCompat +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Observer +import androidx.navigation.fragment.findNavController +import androidx.navigation.ui.setupWithNavController +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.gms.location.FusedLocationProviderClient +import com.google.android.gms.location.LocationServices +import com.google.android.gms.maps.model.LatLng +import net.vonforst.evmap.MapsActivity +import net.vonforst.evmap.R +import net.vonforst.evmap.adapter.FavoritesAdapter +import net.vonforst.evmap.databinding.FragmentFavoritesBinding +import net.vonforst.evmap.viewmodel.FavoritesViewModel +import net.vonforst.evmap.viewmodel.viewModelFactory + +class FavoritesFragment : Fragment() { + private lateinit var binding: FragmentFavoritesBinding + private lateinit var fusedLocationClient: FusedLocationProviderClient + + private val vm: FavoritesViewModel by viewModels(factoryProducer = { + viewModelFactory { + FavoritesViewModel( + requireActivity().application, + getString(R.string.goingelectric_key) + ) + } + }) + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = DataBindingUtil.inflate( + inflater, + R.layout.fragment_favorites, container, false + ) + binding.lifecycleOwner = this + binding.vm = vm + + fusedLocationClient = LocationServices.getFusedLocationProviderClient(requireContext()) + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val toolbar = view.findViewById(R.id.toolbar) as Toolbar + + val navController = findNavController() + toolbar.setupWithNavController( + navController, + (requireActivity() as MapsActivity).appBarConfiguration + ) + + binding.favsList.apply { + adapter = FavoritesAdapter(vm) + layoutManager = + LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) + addItemDecoration( + DividerItemDecoration( + context, LinearLayoutManager.VERTICAL + ) + ) + } + + vm.favorites.observe(viewLifecycleOwner, Observer { + print(it.toString()) + }) + + if (ContextCompat.checkSelfPermission( + requireContext(), + Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + ) { + fusedLocationClient.lastLocation.addOnSuccessListener { location -> + vm.location.value = LatLng(location.latitude, location.longitude) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/fragment/MapFragment.kt b/app/src/main/java/net/vonforst/evmap/fragment/MapFragment.kt index d19166ea..1d241fd0 100644 --- a/app/src/main/java/net/vonforst/evmap/fragment/MapFragment.kt +++ b/app/src/main/java/net/vonforst/evmap/fragment/MapFragment.kt @@ -78,6 +78,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac private lateinit var clusterIconGenerator: ClusterIconGenerator private lateinit var chargerIconGenerator: ChargerIconGenerator private lateinit var animator: MarkerAnimator + private lateinit var favToggle: MenuItem override fun onCreateView( inflater: LayoutInflater, @@ -115,6 +116,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac bottomSheetBehavior = BottomSheetBehaviorGoogleMapsLike.from(binding.bottomSheet) detailAppBarBehavior = MergedAppBarLayoutBehavior.from(binding.detailAppBar) + binding.detailAppBar.toolbar.inflateMenu(R.menu.detail) + favToggle = binding.detailAppBar.toolbar.menu.findItem(R.id.menu_fav) + setupObservers() setupClickListeners() setupAdapters() @@ -173,6 +177,25 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac binding.detailAppBar.toolbar.setNavigationOnClickListener { bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED } + binding.detailAppBar.toolbar.setOnMenuItemClickListener { + when (it.itemId) { + R.id.menu_fav -> { + toggleFavorite() + true + } + else -> false + } + } + } + + private fun toggleFavorite() { + val favs = vm.favorites.value ?: return + val charger = vm.chargerSparse.value ?: return + if (favs.find { it.id == charger.id } != null) { + vm.deleteFavorite(charger) + } else { + vm.insertFavorite(charger) + } } private fun setupObservers() { @@ -193,6 +216,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac } binding.fabDirections.show() detailAppBarBehavior.setToolbarTitle(it.name) + updateFavoriteToggle() } else { bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN } @@ -201,6 +225,19 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac val chargepoints = it.data if (chargepoints != null) updateMap(chargepoints) }) + vm.favorites.observe(viewLifecycleOwner, Observer { + updateFavoriteToggle() + }) + } + + private fun updateFavoriteToggle() { + val favs = vm.favorites.value ?: return + val charger = vm.chargerSparse.value ?: return + if (favs.find { it.id == charger.id } != null) { + favToggle.setIcon(R.drawable.ic_fav) + } else { + favToggle.setIcon(R.drawable.ic_fav_no) + } } private fun setupAdapters() { @@ -340,8 +377,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac return ContextCompat.checkSelfPermission( context, Manifest.permission.ACCESS_FINE_LOCATION - ) == - PackageManager.PERMISSION_GRANTED + ) == PackageManager.PERMISSION_GRANTED } private fun updateMap(chargepoints: List) { diff --git a/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt b/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt index dfbf02ef..0e2a080b 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt @@ -7,10 +7,10 @@ import net.vonforst.evmap.api.goingelectric.ChargeLocation @Dao interface ChargeLocationsDao { @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insert(vararg locations: ChargeLocation) + suspend fun insert(vararg locations: ChargeLocation) @Delete - fun delete(vararg locations: ChargeLocation) + suspend fun delete(vararg locations: ChargeLocation) @Query("SELECT * FROM chargelocation") fun getAllChargeLocations(): LiveData> diff --git a/app/src/main/java/net/vonforst/evmap/storage/TypeConverters.kt b/app/src/main/java/net/vonforst/evmap/storage/TypeConverters.kt index aae1f246..e416d095 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/TypeConverters.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/TypeConverters.kt @@ -19,23 +19,23 @@ class Converters { } @TypeConverter - fun fromChargepointList(value: List): String { + fun fromChargepointList(value: List?): String { return chargepointListAdapter.toJson(value) } @TypeConverter - fun toChargepointList(value: String): List { - return chargepointListAdapter.fromJson(value)!! + fun toChargepointList(value: String): List? { + return chargepointListAdapter.fromJson(value) } @TypeConverter - fun fromChargerPhotoList(value: List): String { + fun fromChargerPhotoList(value: List?): String { return chargerPhotoListAdapter.toJson(value) } @TypeConverter - fun toChargerPhotoList(value: String): List { - return chargerPhotoListAdapter.fromJson(value)!! + fun toChargerPhotoList(value: String): List? { + return chargerPhotoListAdapter.fromJson(value) } @TypeConverter diff --git a/app/src/main/java/net/vonforst/evmap/viewmodel/FavoritesViewModel.kt b/app/src/main/java/net/vonforst/evmap/viewmodel/FavoritesViewModel.kt new file mode 100644 index 00000000..89a13c8e --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/viewmodel/FavoritesViewModel.kt @@ -0,0 +1,54 @@ +package net.vonforst.evmap.viewmodel + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import com.google.android.gms.maps.model.LatLng +import kotlinx.coroutines.launch +import net.vonforst.evmap.api.goingelectric.ChargeLocation +import net.vonforst.evmap.api.goingelectric.GoingElectricApi +import net.vonforst.evmap.storage.AppDatabase + +class FavoritesViewModel(application: Application, geApiKey: String) : + AndroidViewModel(application) { + private var api = GoingElectricApi.create(geApiKey) + private var db = AppDatabase.getInstance(application) + + val favorites: LiveData> by lazy { + db.chargeLocationsDao().getAllChargeLocations() + } + + val location: MutableLiveData by lazy { + MutableLiveData() + } + + /*val availability: MediatorLiveData>> by lazy { + MediatorLiveData>>().apply { + addSource(favorites) { chargers -> + if (chargers != null) { + viewModelScope.launch { + chargers.map { + availability.value = Resource.loading(null) + } + } + } else { + value = null + } + } + } + }*/ + + fun insertFavorite(charger: ChargeLocation) { + viewModelScope.launch { + db.chargeLocationsDao().insert(charger) + } + } + + fun deleteFavorite(charger: ChargeLocation) { + viewModelScope.launch { + db.chargeLocationsDao().delete(charger) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/viewmodel/MapViewModel.kt b/app/src/main/java/net/vonforst/evmap/viewmodel/MapViewModel.kt index 5878ddb9..b8f5dd1e 100644 --- a/app/src/main/java/net/vonforst/evmap/viewmodel/MapViewModel.kt +++ b/app/src/main/java/net/vonforst/evmap/viewmodel/MapViewModel.kt @@ -91,11 +91,15 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode } fun insertFavorite(charger: ChargeLocation) { - db.chargeLocationsDao().insert(charger) + viewModelScope.launch { + db.chargeLocationsDao().insert(charger) + } } fun deleteFavorite(charger: ChargeLocation) { - db.chargeLocationsDao().delete(charger) + viewModelScope.launch { + db.chargeLocationsDao().delete(charger) + } } private fun loadChargepoints(mapPosition: MapPosition) { diff --git a/app/src/main/res/drawable/ic_fav_no.xml b/app/src/main/res/drawable/ic_fav_no.xml new file mode 100644 index 00000000..9cb290d3 --- /dev/null +++ b/app/src/main/res/drawable/ic_fav_no.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/fragment_favorites.xml b/app/src/main/res/layout/fragment_favorites.xml new file mode 100644 index 00000000..cf6a0bf3 --- /dev/null +++ b/app/src/main/res/layout/fragment_favorites.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_map.xml b/app/src/main/res/layout/fragment_map.xml index de29d9a8..f9a40a25 100644 --- a/app/src/main/res/layout/fragment_map.xml +++ b/app/src/main/res/layout/fragment_map.xml @@ -145,6 +145,7 @@ android:id="@+id/detail_app_bar" android:layout_width="match_parent" android:layout_height="wrap_content" - app:layout_behavior="@string/MergedAppBarLayoutBehavior"> + app:layout_behavior="@string/MergedAppBarLayoutBehavior" + android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar" /> \ No newline at end of file diff --git a/app/src/main/res/layout/item_favorite.xml b/app/src/main/res/layout/item_favorite.xml new file mode 100644 index 00000000..7660b473 --- /dev/null +++ b/app/src/main/res/layout/item_favorite.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/detail.xml b/app/src/main/res/menu/detail.xml new file mode 100644 index 00000000..dd8ddd7a --- /dev/null +++ b/app/src/main/res/menu/detail.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index e3399a3b..aca6f439 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -12,7 +12,7 @@ tools:layout="@layout/fragment_map"> + android:label="@string/about" + tools:layout="@layout/fragment_preference" /> + android:label="GalleryFragment" + tools:layout="@layout/fragment_gallery" /> + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 6bbd2bca..48c038b4 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -36,4 +36,7 @@ ©2020 Johan von Forstner Sonstiges Datenschutzerklärung + Zu Favoriten hinzufügen + Aus Favoriten entfernen + %.1f km \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 87275027..a1c91ac3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -35,4 +35,7 @@ ©2020 Johan von Forstner Other Privacy Notice + Add to favorites + Remove from favorites + %.1f km