{NAME} is a peak in California's {RANGE} range. It ranks #{RANK} among the California Fourteeners.
The summit is {ELEV_FEET} feet high ({ELEV_METERS} meters) and has a prominence of {PROM_FEET} feet ({PROM_METERS} meters).
" + TextPopupElement( + TextElementState( + value = tempText, + id = 42 + ) + ) +} + diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/ui/ExpandableCard.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/ui/ExpandableCard.kt new file mode 100644 index 000000000..7198fef60 --- /dev/null +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/ui/ExpandableCard.kt @@ -0,0 +1,180 @@ +/* + * Copyright 2024 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arcgismaps.toolkit.popup.internal.ui + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.Crossfade +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ExpandLess +import androidx.compose.material.icons.rounded.ExpandMore +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.getValue +import androidx.compose.runtime.mutableStateOf +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.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.arcgismaps.toolkit.popup.R + +/** + * Composable Card that has the ability to expand and collapse its [content]. + * + * @since 200.5.0 + */ +@Composable +internal fun ExpandableCard( + title: String = "", + description: String = "", + toggleable: Boolean = true, + content: @Composable () -> Unit = {} +) { + // TODO: promote to public theme. + val shapes = ExpandableCardDefaults.shapes() + val colors = ExpandableCardDefaults.colors() + var expanded by rememberSaveable { mutableStateOf(true) } + + Card( + colors = CardDefaults.cardColors( + containerColor = colors.containerColor + ), + border = BorderStroke(shapes.borderThickness, colors.borderColor), + shape = shapes.containerShape, + modifier = Modifier + .fillMaxWidth() + .padding(shapes.padding) + ) { + Column { + ExpandableHeader( + title = title, + description = description, + expandable = toggleable, + isExpanded = expanded + ) { + if (toggleable) { + expanded = !expanded + } + } + + AnimatedVisibility(visible = expanded) { + content() + } + + } + } +} + +@Composable +private fun ExpandableHeader( + title: String = "", + description: String = "", + expandable: Boolean, + isExpanded: Boolean, + onClick: () -> Unit +) { + if (title.isEmpty() && description.isEmpty() && !expandable) return + val shapes = ExpandableCardDefaults.shapes() + Row( + Modifier + .fillMaxWidth() + .applyIf(expandable) { + clickable { + onClick() + } + } + .background(MaterialTheme.colorScheme.surface), + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(shapes.padding) + .weight(0.5f) + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + if (description.isNotEmpty() && isExpanded) { + Text( + text = description, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Normal, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + } + + if (expandable) { + Crossfade(targetState = isExpanded, label = "expandPopupElement") { + Icon( + modifier = Modifier + .padding(16.dp), + imageVector = if (it) Icons.Rounded.ExpandLess else Icons.Rounded.ExpandMore, + contentDescription = stringResource(R.string.show_or_hide_popup_element_content) + ) + } + } + } +} + +@Preview +@Composable +internal fun ExpandableHeaderPreview() { + ExpandableHeader( + title = "The Title", + description = "the description", + expandable = true, + isExpanded = true + ) {} +} + +@Preview +@Composable +private fun ExpandableCardPreview() { + ExpandableCard( + description = "Foo", + title = "Title" + ) { + Text( + "Hello World", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(16.dp) + ) + } +} + diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/ui/ExpandableCardDefaults.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/ui/ExpandableCardDefaults.kt new file mode 100644 index 000000000..ccd7fbb49 --- /dev/null +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/ui/ExpandableCardDefaults.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2024 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arcgismaps.toolkit.popup.internal.ui + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + + +internal object ExpandableCardDefaults { + @Composable fun shapes(): ExpandableCardShapes = ExpandableCardShapes( + padding = 16.dp, + containerShape = RoundedCornerShape(5.dp), + borderThickness = 1.dp + ) + @Composable + fun colors() : AttachmentElementColors = AttachmentElementColors( + containerColor = MaterialTheme.colorScheme.background, + galleryContainerColor = MaterialTheme.colorScheme.onBackground, + borderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.6f) + ) +} + +internal data class ExpandableCardShapes( + val padding: Dp, + val containerShape: RoundedCornerShape, + val borderThickness: Dp +) + +internal data class AttachmentElementColors( + val containerColor : Color, + val galleryContainerColor: Color, + val borderColor : Color, +) + diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/ui/Modifier.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/ui/Modifier.kt new file mode 100644 index 000000000..83c349d3b --- /dev/null +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/ui/Modifier.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2024 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arcgismaps.toolkit.popup.internal.ui + +import androidx.compose.ui.Modifier + +internal fun Modifier.applyIf(condition: Boolean, then: Modifier.() -> Modifier): Modifier = + if (condition) { + then() + } else { + this + } diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/ui/fileviewer/FileViewer.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/ui/fileviewer/FileViewer.kt new file mode 100644 index 000000000..ee88fbab8 --- /dev/null +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/ui/fileviewer/FileViewer.kt @@ -0,0 +1,262 @@ +/* + * Copyright 2024 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.arcgismaps.toolkit.popup.internal.ui.fileviewer + +import android.content.Intent +import android.util.Log +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +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.material.icons.Icons +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material.icons.rounded.MoreVert +import androidx.compose.material.icons.rounded.Save +import androidx.compose.material.icons.rounded.Share +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.core.content.FileProvider +import androidx.media3.common.MediaItem +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.ui.PlayerView +import coil.compose.AsyncImage +import com.arcgismaps.toolkit.popup.R +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import java.io.File + +/** + * A file viewer that can display different type of files. + * + * @since 200.5.0 + */ +@Composable +internal fun FileViewer(scope: CoroutineScope, fileState: ViewableFile, onDismissRequest: () -> Unit) { + if (fileState.type !is ViewableFileType.Other) { + Dialog( + onDismissRequest = onDismissRequest, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + TopAppBar(fileState, scope, onDismissRequest) + } + ) { + FileViewerContent(Modifier.padding(it), fileState) + } + } + } else { + val uri = FileProvider.getUriForFile( + LocalContext.current.applicationContext, + "${LocalContext.current.applicationContext.applicationInfo.packageName}.arcgis.popup.fileprovider", + File(fileState.path) + ) + + val intent = Intent(Intent.ACTION_VIEW).apply { + setDataAndType(uri, fileState.contentType) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + LocalContext.current.startActivity(intent) + onDismissRequest() + } +} + +@Composable +private fun FileViewerContent( + modifier: Modifier, + fileState: ViewableFile +) { + Box( + modifier = modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), + contentAlignment = Alignment.Center + ) { + when (fileState.type) { + is ViewableFileType.Image -> + AsyncImage( + modifier = Modifier.fillMaxSize(), + model = fileState.path, + contentDescription = stringResource(id = R.string.image), + ) + + is ViewableFileType.Video, ViewableFileType.Audio -> VideoViewer(fileState.path) + else -> { + throw UnsupportedOperationException("Cannot view this file type") + } + } + } +} + +@Composable +private fun TopAppBar(fileState: ViewableFile, scope: CoroutineScope, onDismissRequest: () -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(64.dp) + .background(MaterialTheme.colorScheme.surface), + verticalAlignment = Alignment.CenterVertically, + ) { + IconButton(onClick = { onDismissRequest() }) { + Icon( + Icons.Rounded.Close, + contentDescription = stringResource(id = R.string.close), + tint = MaterialTheme.colorScheme.onSurface + ) + } + Text( + modifier = Modifier.weight(1f), + text = fileState.name, + color = MaterialTheme.colorScheme.onSurface, + fontSize = MaterialTheme.typography.headlineSmall.fontSize, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + ViewerActions( + coroutineScope = scope, + viewableFile = fileState, + ) + } +} + +@Composable +private fun ViewerActions( + coroutineScope: CoroutineScope, + modifier: Modifier = Modifier, + viewableFile: ViewableFile, +) { + val context = LocalContext.current + val expanded = remember { mutableStateOf(false) } + Box(modifier = modifier) { + IconButton(onClick = { expanded.value = true }) { + Icon( + Icons.Rounded.MoreVert, + contentDescription = stringResource(id = R.string.more), + tint = MaterialTheme.colorScheme.onSurface + ) + } + + DropdownMenu(expanded = expanded.value, onDismissRequest = { expanded.value = false }) { + DropdownMenuItem( + text = { Text(stringResource(id = R.string.share), color = MaterialTheme.colorScheme.onSurface) }, + onClick = { + expanded.value = false + coroutineScope.launch { viewableFile.share(context) } + }, + leadingIcon = { + Icon( + Icons.Rounded.Share, + contentDescription = stringResource(id = R.string.share), + tint = MaterialTheme.colorScheme.onSurface + ) + } + ) + + DropdownMenuItem( + text = { + Text(text = stringResource(id = R.string.save), color = MaterialTheme.colorScheme.onSurface) + }, + onClick = { + expanded.value = false + coroutineScope.launch { + val saveResult = viewableFile.saveToDevice(context) + saveResult.onSuccess { + Toast.makeText(context, context.getString(R.string.save_successful), Toast.LENGTH_SHORT) + .show() + }.onFailure { + Toast.makeText(context, context.getString(R.string.save_failed), Toast.LENGTH_SHORT).show() + Log.e("ArcGISMapsSDK", "Failed to save file: $it") + } + } + }, + leadingIcon = { + Icon( + Icons.Rounded.Save, + contentDescription = stringResource(id = R.string.save), + tint = MaterialTheme.colorScheme.onSurface + ) + } + ) + } + } +} + +@Composable +internal fun VideoViewer(path: String) { + val context = LocalContext.current + val exoPlayer = remember { + ExoPlayer.Builder(context).build().apply { + val mediaItem = MediaItem.Builder() + .setUri(path) + .build() + setMediaItem(mediaItem) + prepare() + } + } + + AndroidView( + factory = { + PlayerView(context).apply { + player = exoPlayer + } + } + ) + DisposableEffect(Unit) { + onDispose { + exoPlayer.release() + } + } +} + +@Preview +@Composable +private fun FileViewerPreview() { + FileViewer( + scope = rememberCoroutineScope(), + fileState = ViewableFile( + path = "path", + name = "ArcGIS Pro", + size = 0, + type = ViewableFileType.Image, + contentType = "image/jpeg", + ), onDismissRequest = {} + ) +} diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/ui/fileviewer/ViewableFile.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/ui/fileviewer/ViewableFile.kt new file mode 100644 index 000000000..ad66758c8 --- /dev/null +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/ui/fileviewer/ViewableFile.kt @@ -0,0 +1,153 @@ +/* + * Copyright 2024 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.arcgismaps.toolkit.popup.internal.ui.fileviewer + +import android.annotation.SuppressLint +import android.content.ContentValues +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Parcel +import android.os.Parcelable +import android.provider.MediaStore +import androidx.core.content.FileProvider +import com.arcgismaps.mapping.popup.PopupAttachmentType +import com.arcgismaps.toolkit.popup.R +import com.arcgismaps.toolkit.popup.internal.element.attachment.PopupAttachmentState +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.parcelize.Parceler +import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.TypeParceler +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException + +/** + * A file that can be viewed in the [FileViewer]. + */ +@Parcelize +internal data class ViewableFile( + val name: String, + val size: Long = 0, + val path: String, + @TypeParceler