diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/main/MainActivity.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/main/MainActivity.kt index 690064c93b..88777ec216 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/screens/main/MainActivity.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/screens/main/MainActivity.kt @@ -11,7 +11,6 @@ import android.graphics.Color import android.graphics.PointF import android.location.Location import android.os.Bundle -import android.text.Html import android.view.View import android.view.ViewGroup import android.view.WindowManager @@ -30,7 +29,6 @@ import androidx.compose.ui.geometry.Offset import androidx.core.graphics.Insets import androidx.core.net.toUri import androidx.core.os.bundleOf -import androidx.core.text.parseAsHtml import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.commit @@ -69,9 +67,7 @@ import de.westnordost.streetcomplete.data.quest.QuestAutoSyncer import de.westnordost.streetcomplete.data.quest.QuestKey import de.westnordost.streetcomplete.data.quest.QuestType import de.westnordost.streetcomplete.data.quest.VisibleQuestsSource -import de.westnordost.streetcomplete.data.urlconfig.UrlConfigController import de.westnordost.streetcomplete.data.user.UserLoginSource -import de.westnordost.streetcomplete.data.visiblequests.QuestPresetsSource import de.westnordost.streetcomplete.databinding.ActivityMainBinding import de.westnordost.streetcomplete.databinding.EffectQuestPlopBinding import de.westnordost.streetcomplete.osm.level.levelsIntersect @@ -92,7 +88,6 @@ import de.westnordost.streetcomplete.screens.main.bottom_sheet.IsMapPositionAwar import de.westnordost.streetcomplete.screens.main.bottom_sheet.MoveNodeFragment import de.westnordost.streetcomplete.screens.main.bottom_sheet.SplitWayFragment import de.westnordost.streetcomplete.screens.main.controls.LocationState -import de.westnordost.streetcomplete.screens.main.controls.MapControls import de.westnordost.streetcomplete.screens.main.edithistory.EditHistoryViewModel import de.westnordost.streetcomplete.screens.main.edithistory.icon import de.westnordost.streetcomplete.screens.main.map.MainMapFragment @@ -120,7 +115,6 @@ import de.westnordost.streetcomplete.util.location.LocationRequestFragment import de.westnordost.streetcomplete.util.math.area import de.westnordost.streetcomplete.util.math.enclosingBoundingBox import de.westnordost.streetcomplete.util.math.enlargedBy -import de.westnordost.streetcomplete.util.parseGeoUri import de.westnordost.streetcomplete.view.dialogs.RequestLoginDialog import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -255,7 +249,7 @@ class MainActivity : setContentView(binding.root) binding.controls.content { - MapControls( + MainScreen( viewModel = controlsViewModel, editHistoryViewModel = editHistoryViewModel, onClickZoomIn = ::onClickZoomIn, diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/main/controls/MapControls.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/main/MainScreen.kt similarity index 65% rename from app/src/main/java/de/westnordost/streetcomplete/screens/main/controls/MapControls.kt rename to app/src/main/java/de/westnordost/streetcomplete/screens/main/MainScreen.kt index 97f64b1a36..c436a74b17 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/screens/main/controls/MapControls.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/screens/main/MainScreen.kt @@ -1,4 +1,4 @@ -package de.westnordost.streetcomplete.screens.main.controls +package de.westnordost.streetcomplete.screens.main import android.content.Intent import android.widget.Toast @@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.absoluteOffset import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.material.ButtonDefaults @@ -45,9 +46,18 @@ import androidx.compose.ui.unit.dp import de.westnordost.streetcomplete.R import de.westnordost.streetcomplete.data.messages.Message import de.westnordost.streetcomplete.screens.about.AboutActivity -import de.westnordost.streetcomplete.screens.main.MainMenuDialog -import de.westnordost.streetcomplete.screens.main.MainViewModel -import de.westnordost.streetcomplete.screens.main.RequestLoginDialog +import de.westnordost.streetcomplete.screens.main.controls.CompassButton +import de.westnordost.streetcomplete.screens.main.controls.Crosshair +import de.westnordost.streetcomplete.screens.main.controls.LocationStateButton +import de.westnordost.streetcomplete.screens.main.controls.MainMenuButton +import de.westnordost.streetcomplete.screens.main.controls.MapAttribution +import de.westnordost.streetcomplete.screens.main.controls.MapButton +import de.westnordost.streetcomplete.screens.main.controls.MessagesButton +import de.westnordost.streetcomplete.screens.main.controls.OverlaySelectionButton +import de.westnordost.streetcomplete.screens.main.controls.PointerPinButton +import de.westnordost.streetcomplete.screens.main.controls.StarsCounter +import de.westnordost.streetcomplete.screens.main.controls.UploadButton +import de.westnordost.streetcomplete.screens.main.controls.findClosestIntersection import de.westnordost.streetcomplete.screens.main.edithistory.EditHistorySidebar import de.westnordost.streetcomplete.screens.main.edithistory.EditHistoryViewModel import de.westnordost.streetcomplete.screens.main.errors.HandleLastCrash @@ -75,14 +85,10 @@ import kotlinx.coroutines.launch import kotlin.math.PI import kotlin.math.abs -// NOTE: this will eventually grow into something that should be renamed to MainScreen, i.e. -// replacing MainActivity completely. For now, it is mostly only the controls and dialogs -// and dropdowns triggered by that. But since this is big, we should take care to put any -// elements that can meaningfully be put into an own composable in another file /** Map controls shown on top of the map. */ @Composable -fun MapControls( +fun MainScreen( viewModel: MainViewModel, editHistoryViewModel: EditHistoryViewModel, onClickZoomIn: () -> Unit, @@ -140,7 +146,6 @@ fun MapControls( var showTeamModeWizard by remember { mutableStateOf(false) } var showMainMenuDialog by remember { mutableStateOf(false) } var showLoginDialog by remember { mutableStateOf(false) } - var shownMessage by remember { mutableStateOf(null) } val showEditHistorySidebar by editHistoryViewModel.isShowingSidebar.collectAsState() @@ -186,7 +191,7 @@ fun MapControls( } } - Box { + Box(modifier) { if (selectedOverlay?.isCreateNodeEnabled == true) { Crosshair() } @@ -210,154 +215,161 @@ fun MapControls( ) { Image(painterResource(R.drawable.location_dot_small), null) } } - Box( - modifier - .fillMaxSize() - .safeDrawingPadding() - .onGloballyPositioned { pointerPinRects["frame"] = it.boundsInRoot() } - .padding(bottom = 22.dp) + Column(Modifier + .fillMaxSize() + .safeDrawingPadding() + .onGloballyPositioned { pointerPinRects["frame"] = it.boundsInRoot() } ) { - // top-start controls - Box(modifier = Modifier - .align(Alignment.TopStart) - .onGloballyPositioned { pointerPinRects["top-start"] = it.boundsInRoot() } - ) { - // stars counter - StarsCounter( - count = starsCount, - modifier = Modifier - .defaultMinSize(minWidth = 96.dp) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null - ) { viewModel.toggleShowingCurrentWeek() }, - isCurrentWeek = isShowingStarsCurrentWeek, - showProgress = isUploadingOrDownloading - ) - } - - // top-end controls - Row( - modifier = Modifier - .align(Alignment.TopEnd) - .padding(4.dp) - .onGloballyPositioned { pointerPinRects["top-end"] = it.boundsInRoot() }, - horizontalArrangement = Arrangement.spacedBy(8.dp) + Box(Modifier + .fillMaxWidth() + .weight(1f) ) { - if (messagesCount > 0) { - MessagesButton( - onClick = ::onClickMessages, - messagesCount = messagesCount - ) - } - if (!isAutoSync) { - UploadButton( - onClick = onClickUpload, - unsyncedEditsCount = unsyncedEditsCount, - enabled = !isUploadingOrDownloading - ) - } - Box { - OverlaySelectionButton( - onClick = ::onClickOverlays, - overlay = selectedOverlay - ) - OverlaySelectionDropdownMenu( - expanded = showOverlaysDropdown, - onDismissRequest = { showOverlaysDropdown = false }, - overlays = viewModel.overlays, - onSelect = { viewModel.selectOverlay(it) } + // top-start controls + Box(Modifier + .align(Alignment.TopStart) + .onGloballyPositioned { pointerPinRects["top-start"] = it.boundsInRoot() } + ) { + // stars counter + StarsCounter( + count = starsCount, + modifier = Modifier + .defaultMinSize(minWidth = 96.dp) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { viewModel.toggleShowingCurrentWeek() }, + isCurrentWeek = isShowingStarsCurrentWeek, + showProgress = isUploadingOrDownloading ) } - MainMenuButton( - onClick = { showMainMenuDialog = true }, - indexInTeam = if (isTeamMode) indexInTeam else null - ) - } - - // bottom-end controls - Column( - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(4.dp) - .onGloballyPositioned { pointerPinRects["bottom-end"] = it.boundsInRoot() }, - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - val isCompassVisible = abs(mapRotation) >= 1.0 || abs(mapTilt) >= 1.0 - AnimatedVisibility( - visible = isCompassVisible, - enter = fadeIn(), - exit = fadeOut() + // top-end controls + Row( + modifier = Modifier + .align(Alignment.TopEnd) + .padding(4.dp) + .onGloballyPositioned { pointerPinRects["top-end"] = it.boundsInRoot() }, + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - CompassButton( - onClick = onClickCompass, - modifier = Modifier.graphicsLayer( - rotationZ = -mapRotation.toFloat(), - rotationX = mapTilt.toFloat() + if (messagesCount > 0) { + MessagesButton( + onClick = ::onClickMessages, + messagesCount = messagesCount + ) + } + if (!isAutoSync) { + UploadButton( + onClick = onClickUpload, + unsyncedEditsCount = unsyncedEditsCount, + enabled = !isUploadingOrDownloading + ) + } + Box { + OverlaySelectionButton( + onClick = ::onClickOverlays, + overlay = selectedOverlay ) + OverlaySelectionDropdownMenu( + expanded = showOverlaysDropdown, + onDismissRequest = { showOverlaysDropdown = false }, + overlays = viewModel.overlays, + onSelect = { viewModel.selectOverlay(it) } + ) + } + + MainMenuButton( + onClick = { showMainMenuDialog = true }, + indexInTeam = if (isTeamMode) indexInTeam else null ) } - MapButton(onClick = onClickZoomIn) { ZoomInIcon() } - MapButton(onClick = onClickZoomOut) { ZoomOutIcon() } - LocationStateButton( - onClick = onClickLocation, - state = locationState, - isNavigationMode = isNavigationMode, - isFollowing = isFollowingPosition, - ) - } - if (selectedOverlay?.isCreateNodeEnabled == true) { - MapButton( - onClick = { - if ((mapCamera?.zoom ?: 0.0) >= 17.0) { - onClickCreate() - } else { - context.toast(R.string.download_area_too_big, Toast.LENGTH_LONG) - } - }, + // bottom-end controls + Column( modifier = Modifier - .align(BiasAlignment(0.333f, 1f)) - .padding(4.dp), - colors = ButtonDefaults.buttonColors( - backgroundColor = MaterialTheme.colors.secondaryVariant, - ), + .align(Alignment.BottomEnd) + .padding(4.dp) + .onGloballyPositioned { pointerPinRects["bottom-end"] = it.boundsInRoot() }, + verticalArrangement = Arrangement.spacedBy(8.dp) ) { - LargeCreateIcon() + val isCompassVisible = abs(mapRotation) >= 1.0 || abs(mapTilt) >= 1.0 + AnimatedVisibility( + visible = isCompassVisible, + enter = fadeIn(), + exit = fadeOut() + ) { + CompassButton( + onClick = onClickCompass, + modifier = Modifier.graphicsLayer( + rotationZ = -mapRotation.toFloat(), + rotationX = mapTilt.toFloat() + ) + ) + } + MapButton(onClick = onClickZoomIn) { ZoomInIcon() } + MapButton(onClick = onClickZoomOut) { ZoomOutIcon() } + LocationStateButton( + onClick = onClickLocation, + state = locationState, + isNavigationMode = isNavigationMode, + isFollowing = isFollowingPosition, + ) } - } - // bottom-start controls - Column( - modifier = Modifier - .align(Alignment.BottomStart) - .padding(4.dp) - .onGloballyPositioned { pointerPinRects["bottom-start"] = it.boundsInRoot() }, - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - if (isRecordingTracks) { + if (selectedOverlay?.isCreateNodeEnabled == true) { MapButton( - onClick = onClickStopTrackRecording, + onClick = { + if ((mapCamera?.zoom ?: 0.0) >= 17.0) { + onClickCreate() + } else { + context.toast(R.string.download_area_too_big, Toast.LENGTH_LONG) + } + }, + modifier = Modifier + .align(BiasAlignment(0.333f, 1f)) + .padding(4.dp), colors = ButtonDefaults.buttonColors( backgroundColor = MaterialTheme.colors.secondaryVariant, ), ) { - StopRecordingIcon() + LargeCreateIcon() } } - if (hasEdits) { - MapButton( - onClick = { editHistoryViewModel.showSidebar() }, - // Don't allow undoing while uploading. Should prevent race conditions. - // (Undoing quest while also uploading it at the same time) - enabled = !isUploadingOrDownloading, - ) { - UndoIcon() + // bottom-start controls + Column( + modifier = Modifier + .align(Alignment.BottomStart) + .padding(4.dp) + .onGloballyPositioned { + pointerPinRects["bottom-start"] = it.boundsInRoot() + }, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + if (isRecordingTracks) { + MapButton( + onClick = onClickStopTrackRecording, + colors = ButtonDefaults.buttonColors( + backgroundColor = MaterialTheme.colors.secondaryVariant, + ), + ) { + StopRecordingIcon() + } + } + + if (hasEdits) { + MapButton( + onClick = { editHistoryViewModel.showSidebar() }, + // Don't allow undoing while uploading. Should prevent race conditions. + // (Undoing quest while also uploading it at the same time) + enabled = !isUploadingOrDownloading, + ) { + UndoIcon() + } } } } + + MapAttribution(Modifier.padding(8.dp)) } val dir = LocalLayoutDirection.current.dir diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/main/controls/MapAttribution.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/main/controls/MapAttribution.kt new file mode 100644 index 0000000000..e63aeb4364 --- /dev/null +++ b/app/src/main/java/de/westnordost/streetcomplete/screens/main/controls/MapAttribution.kt @@ -0,0 +1,61 @@ +package de.westnordost.streetcomplete.screens.main.controls + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.material.ContentAlpha +import androidx.compose.material.MaterialTheme +import androidx.compose.material.ProvideTextStyle +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shadow +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import de.westnordost.streetcomplete.R +import de.westnordost.streetcomplete.ui.common.dialogs.ConfirmationDialog + +/** Shows (hardcoded) map attribution and opens links on click */ +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun MapAttribution(modifier: Modifier = Modifier) { + var shownLink by remember { mutableStateOf(null) } + + FlowRow( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + ProvideTextStyle(MaterialTheme.typography.caption.copy( + color = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.medium), + shadow = Shadow( + color = MaterialTheme.colors.surface, + blurRadius = 6f + ) + )) { + Text( + text = stringResource(R.string.map_attribution_osm), + modifier = Modifier.clickable { shownLink ="https://www.openstreetmap.org/copyright" } + ) + Text( + text = "© JawgMaps", + modifier = Modifier.clickable { shownLink = "https://www.jawg.io" } + ) + } + } + + shownLink?.let { url -> + val uriHandler = LocalUriHandler.current + ConfirmationDialog( + onDismissRequest = { shownLink = null }, + onConfirmed = { uriHandler.openUri(url) }, + title = { Text(stringResource(R.string.open_url)) }, + text = { Text(url) } + ) + } +} diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/main/map/MapFragment.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/main/map/MapFragment.kt index 592bb7f17c..1abe0d5140 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/screens/main/map/MapFragment.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/screens/main/map/MapFragment.kt @@ -3,7 +3,6 @@ package de.westnordost.streetcomplete.screens.main.map import android.graphics.PointF import android.os.Bundle import android.view.View -import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import de.westnordost.streetcomplete.ApplicationConstants @@ -25,11 +24,8 @@ import de.westnordost.streetcomplete.screens.main.map.maplibre.toLatLon import de.westnordost.streetcomplete.screens.main.map.maplibre.updateCamera import de.westnordost.streetcomplete.util.ktx.dpToPx import de.westnordost.streetcomplete.util.ktx.nowAsEpochMilliseconds -import de.westnordost.streetcomplete.util.ktx.openUri -import de.westnordost.streetcomplete.util.ktx.setMargins import de.westnordost.streetcomplete.util.ktx.viewLifecycleScope import de.westnordost.streetcomplete.util.viewBinding -import de.westnordost.streetcomplete.view.insets_animation.respectSystemInsets import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.koin.android.ext.android.inject @@ -76,11 +72,6 @@ open class MapFragment : Fragment(R.layout.fragment_map) { binding.map.onCreate(savedInstanceState) binding.map.foreground = view.context.getDrawable(R.color.background) - binding.openstreetmapLink.setOnClickListener { showOpenUrlDialog("https://www.openstreetmap.org/copyright") } - binding.mapTileProviderLink.setOnClickListener { showOpenUrlDialog("https://www.jawg.io") } - - binding.attributionContainer.respectSystemInsets(View::setMargins) - initOfflineCacheSize() cleanOldOfflineRegions() @@ -105,15 +96,6 @@ open class MapFragment : Fragment(R.layout.fragment_map) { } } - private fun showOpenUrlDialog(url: String) { - AlertDialog.Builder(requireContext()) - .setTitle(R.string.open_url) - .setMessage(url) - .setPositiveButton(android.R.string.ok) { _, _ -> openUri(url) } - .setNegativeButton(android.R.string.cancel, null) - .show() - } - override fun onStart() { super.onStart() // sceneMapComponent might actually be null if map style not initialized yet diff --git a/app/src/main/res/layout/fragment_map.xml b/app/src/main/res/layout/fragment_map.xml index 3cbcea5922..5d7b452766 100644 --- a/app/src/main/res/layout/fragment_map.xml +++ b/app/src/main/res/layout/fragment_map.xml @@ -1,46 +1,6 @@ - - - - - - - - - - - - - + android:layout_height="match_parent" /> diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 35aa480f54..54ee789926 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -54,7 +54,6 @@ #ddd - #444 #fff