From 42961be502003a93ecb135a0d028888bcda3008a Mon Sep 17 00:00:00 2001 From: Sebastiano Barezzi Date: Tue, 8 Aug 2023 14:39:43 +0200 Subject: [PATCH] Glimpse: Media info bottom sheet dialog Change-Id: Iadcb08b09cfc07139fc69ce69e605eb4f604c760 --- app/Android.bp | 1 + app/build.gradle.kts | 3 + .../lineageos/glimpse/ext/ContentResolver.kt | 6 + .../java/org/lineageos/glimpse/ext/Double.kt | 40 +++ .../lineageos/glimpse/ext/ExifInterface.kt | 79 ++++++ .../glimpse/fragments/MediaViewerFragment.kt | 13 + .../java/org/lineageos/glimpse/ui/ListItem.kt | 76 +++++ .../glimpse/ui/MediaInfoBottomSheetDialog.kt | 262 ++++++++++++++++++ app/src/main/res/drawable/ic_camera.xml | 16 ++ app/src/main/res/drawable/ic_image.xml | 15 + app/src/main/res/drawable/ic_info.xml | 15 + app/src/main/res/drawable/ic_location_on.xml | 18 ++ app/src/main/res/drawable/ic_person.xml | 15 + .../res/drawable/ic_video_camera_back.xml | 15 + .../main/res/layout/fragment_media_viewer.xml | 5 + app/src/main/res/layout/list_item.xml | 85 ++++++ .../layout/media_info_bottom_sheet_dialog.xml | 93 +++++++ app/src/main/res/values/attrs.xml | 14 + app/src/main/res/values/strings.xml | 7 + app/src/main/res/values/themes.xml | 20 ++ 20 files changed, 798 insertions(+) create mode 100644 app/src/main/java/org/lineageos/glimpse/ext/Double.kt create mode 100644 app/src/main/java/org/lineageos/glimpse/ext/ExifInterface.kt create mode 100644 app/src/main/java/org/lineageos/glimpse/ui/ListItem.kt create mode 100644 app/src/main/java/org/lineageos/glimpse/ui/MediaInfoBottomSheetDialog.kt create mode 100644 app/src/main/res/drawable/ic_camera.xml create mode 100644 app/src/main/res/drawable/ic_image.xml create mode 100644 app/src/main/res/drawable/ic_info.xml create mode 100644 app/src/main/res/drawable/ic_location_on.xml create mode 100644 app/src/main/res/drawable/ic_person.xml create mode 100644 app/src/main/res/drawable/ic_video_camera_back.xml create mode 100644 app/src/main/res/layout/list_item.xml create mode 100644 app/src/main/res/layout/media_info_bottom_sheet_dialog.xml create mode 100644 app/src/main/res/values/attrs.xml diff --git a/app/Android.bp b/app/Android.bp index 803c18f..3017fb5 100644 --- a/app/Android.bp +++ b/app/Android.bp @@ -21,6 +21,7 @@ android_app { "androidx-constraintlayout_constraintlayout", "androidx.preference_preference", "Glimpse_com.google.android.material_material", + "androidx.exifinterface_exifinterface", "Glimpse_androidx.media3_media3-exoplayer", "Glimpse_androidx.media3_media3-ui", "androidx.navigation_navigation-fragment-ktx", diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9dae520..3ba69a3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -65,6 +65,9 @@ dependencies { implementation("androidx.preference:preference:1.2.1") implementation("com.google.android.material:material:1.9.0") + // EXIF + implementation("androidx.exifinterface:exifinterface:1.3.6") + // Media3 implementation("androidx.media3:media3-exoplayer:1.1.1") implementation("androidx.media3:media3-ui:1.1.1") diff --git a/app/src/main/java/org/lineageos/glimpse/ext/ContentResolver.kt b/app/src/main/java/org/lineageos/glimpse/ext/ContentResolver.kt index 7a6d9a1..9cf36ca 100644 --- a/app/src/main/java/org/lineageos/glimpse/ext/ContentResolver.kt +++ b/app/src/main/java/org/lineageos/glimpse/ext/ContentResolver.kt @@ -39,6 +39,12 @@ fun ContentResolver.createTrashRequest(value: Boolean, vararg uris: Uri) = MediaStore.createTrashRequest(this, uris.toCollection(ArrayList()), value) ).build() +@RequiresApi(Build.VERSION_CODES.R) +fun ContentResolver.createWriteRequest(vararg uris: Uri) = + IntentSenderRequest.Builder( + MediaStore.createWriteRequest(this, uris.toCollection(ArrayList())) + ).build() + fun ContentResolver.uriFlow(uri: Uri) = callbackFlow { val observer = object : ContentObserver(Handler(Looper.getMainLooper())) { override fun onChange(selfChange: Boolean) { diff --git a/app/src/main/java/org/lineageos/glimpse/ext/Double.kt b/app/src/main/java/org/lineageos/glimpse/ext/Double.kt new file mode 100644 index 0000000..3ff8b49 --- /dev/null +++ b/app/src/main/java/org/lineageos/glimpse/ext/Double.kt @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2023 The LineageOS Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.lineageos.glimpse.ext + +import kotlin.math.abs +import kotlin.math.floor +import kotlin.math.round +import kotlin.math.roundToInt + +fun Double.toFraction(tolerance: Double = 1.0E-1): String { + if (this < 0) { + return "-" + (-this).toFraction() + } + var h1 = 1.0 + var h2 = 0.0 + var k1 = 0.0 + var k2 = 1.0 + var b = this + do { + val a = floor(b) + var aux = h1 + h1 = a * h1 + h2 + h2 = aux + aux = k1 + k1 = a * k1 + k2 + k2 = aux + b = 1 / (b - a) + } while (abs(this - h1 / k1) > this * tolerance) + + return "${h1.roundToInt()}/${k1.roundToInt()}" +} + +fun Double.round(decimals: Int): Double { + var multiplier = 1.0 + repeat(decimals) { multiplier *= 10 } + return round(this * multiplier) / multiplier +} diff --git a/app/src/main/java/org/lineageos/glimpse/ext/ExifInterface.kt b/app/src/main/java/org/lineageos/glimpse/ext/ExifInterface.kt new file mode 100644 index 0000000..f55a4ca --- /dev/null +++ b/app/src/main/java/org/lineageos/glimpse/ext/ExifInterface.kt @@ -0,0 +1,79 @@ +/* + * SPDX-FileCopyrightText: 2023 The LineageOS Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.lineageos.glimpse.ext + +import android.util.Size +import androidx.exifinterface.media.ExifInterface + +private const val DEFAULT_VALUE_INT = -1 +private const val DEFAULT_VALUE_DOUBLE = -1.0 + +fun ExifInterface.getAttributeInt(tag: String) = + getAttributeInt(tag, DEFAULT_VALUE_INT).takeIf { + it != DEFAULT_VALUE_INT + } + +fun ExifInterface.getAttributeDouble(tag: String): Double? = + getAttributeDouble(tag, DEFAULT_VALUE_DOUBLE).takeIf { + it != DEFAULT_VALUE_DOUBLE + } + +val ExifInterface.artist + get() = getAttribute(ExifInterface.TAG_ARTIST) + +val ExifInterface.apertureValue + get() = getAttributeDouble(ExifInterface.TAG_APERTURE_VALUE) + +val ExifInterface.copyright + get() = getAttribute(ExifInterface.TAG_COPYRIGHT) + +val ExifInterface.exposureTime + get() = getAttributeDouble(ExifInterface.TAG_EXPOSURE_TIME) + +val ExifInterface.isoSpeed + get() = getAttributeInt(ExifInterface.TAG_ISO_SPEED) + +val ExifInterface.focalLength + get() = getAttributeDouble(ExifInterface.TAG_FOCAL_LENGTH) + +val ExifInterface.make + get() = getAttribute(ExifInterface.TAG_MAKE) + +val ExifInterface.model + get() = getAttribute(ExifInterface.TAG_MODEL) + +val ExifInterface.pixelXDimension + get() = getAttributeInt(ExifInterface.TAG_PIXEL_X_DIMENSION) + +val ExifInterface.pixelYDimension + get() = getAttributeInt(ExifInterface.TAG_PIXEL_Y_DIMENSION) + +val ExifInterface.size + get() = pixelXDimension?.let { x -> pixelYDimension?.let { y -> Size(x, y) } } + +val ExifInterface.software + get() = getAttribute(ExifInterface.TAG_SOFTWARE) + +var ExifInterface.userComment + get() = getAttribute(ExifInterface.TAG_USER_COMMENT) + set(value) { + setAttribute(ExifInterface.TAG_USER_COMMENT, value) + } + +val ExifInterface.isSupportedFormatForSavingAttributes: Boolean + get() { + val mimeType = ExifInterface::class.java.getDeclaredField("mMimeType").apply { + isAccessible = true + }.get(this) as Int + + val isSupportedFormatForSavingAttributes = ExifInterface::class.java.getDeclaredMethod( + "isSupportedFormatForSavingAttributes", Int::class.java + ).apply { + isAccessible = true + } + + return isSupportedFormatForSavingAttributes.invoke(null, mimeType) as Boolean + } diff --git a/app/src/main/java/org/lineageos/glimpse/fragments/MediaViewerFragment.kt b/app/src/main/java/org/lineageos/glimpse/fragments/MediaViewerFragment.kt index 8ce725a..898bdd9 100644 --- a/app/src/main/java/org/lineageos/glimpse/fragments/MediaViewerFragment.kt +++ b/app/src/main/java/org/lineageos/glimpse/fragments/MediaViewerFragment.kt @@ -42,6 +42,7 @@ import org.lineageos.glimpse.models.Album import org.lineageos.glimpse.models.Media import org.lineageos.glimpse.models.MediaType import org.lineageos.glimpse.thumbnail.MediaViewerAdapter +import org.lineageos.glimpse.ui.MediaInfoBottomSheetDialog import org.lineageos.glimpse.utils.PermissionsUtils import org.lineageos.glimpse.viewmodels.MediaViewModel import java.text.SimpleDateFormat @@ -62,6 +63,7 @@ class MediaViewerFragment : Fragment(R.layout.fragment_media_viewer) { private val dateTextView by getViewProperty(R.id.dateTextView) private val deleteButton by getViewProperty(R.id.deleteButton) private val favoriteButton by getViewProperty(R.id.favoriteButton) + private val infoButton by getViewProperty(R.id.infoButton) private val shareButton by getViewProperty(R.id.shareButton) private val timeTextView by getViewProperty(R.id.timeTextView) private val topSheetConstraintLayout by getViewProperty(R.id.topSheetConstraintLayout) @@ -210,6 +212,9 @@ class MediaViewerFragment : Fragment(R.layout.fragment_media_viewer) { } } + private val mediaInfoBottomSheetDialogCallbacks = + MediaInfoBottomSheetDialog.Callbacks(this) + override fun onResume() { super.onResume() @@ -308,6 +313,14 @@ class MediaViewerFragment : Fragment(R.layout.fragment_media_viewer) { } } + infoButton.setOnClickListener { + mediaViewerAdapter.getItemAtPosition(viewPager.currentItem).let { + MediaInfoBottomSheetDialog( + requireContext(), it, mediaInfoBottomSheetDialogCallbacks + ).show() + } + } + if (!permissionsUtils.mainPermissionsGranted()) { mainPermissionsRequestLauncher.launch(PermissionsUtils.mainPermissions) } else { diff --git a/app/src/main/java/org/lineageos/glimpse/ui/ListItem.kt b/app/src/main/java/org/lineageos/glimpse/ui/ListItem.kt new file mode 100644 index 0000000..8956e3e --- /dev/null +++ b/app/src/main/java/org/lineageos/glimpse/ui/ListItem.kt @@ -0,0 +1,76 @@ +/* + * SPDX-FileCopyrightText: 2023 The LineageOS Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.lineageos.glimpse.ui + +import android.content.Context +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.TextView +import androidx.core.view.isVisible +import com.google.android.material.divider.MaterialDivider +import org.lineageos.glimpse.R + +/** + * A poor man's M3 ListItem implementation + * https://m3.material.io/components/lists/overview + */ +class ListItem @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null +) : FrameLayout(context, attrs) { + private val divider by lazy { findViewById(R.id.divider) } + private val headlineTextView by lazy { findViewById(R.id.headlineTextView) } + private val leadingIconImageView by lazy { findViewById(R.id.leadingIconImageView) } + private val supportingTextView by lazy { findViewById(R.id.supportingTextView) } + private val trailingSupportingTextView by lazy { findViewById(R.id.trailingSupportingTextView) } + + var headlineText: CharSequence? + get() = headlineTextView.text + set(value) { + headlineTextView.text = value + headlineTextView.isVisible = !headlineText.isNullOrEmpty() + } + var leadingIconImage: Drawable? + get() = leadingIconImageView.drawable + set(value) { + leadingIconImageView.setImageDrawable(value) + leadingIconImageView.isVisible = leadingIconImageView.drawable != null + } + var showDivider: Boolean = true + set(value) { + field = value + divider.isVisible = value + } + var supportingText: CharSequence? + get() = supportingTextView.text + set(value) { + supportingTextView.text = value + supportingTextView.isVisible = !supportingText.isNullOrEmpty() + } + var trailingSupportingText: CharSequence? + get() = trailingSupportingTextView.text + set(value) { + trailingSupportingTextView.text = value + trailingSupportingTextView.isVisible = !trailingSupportingText.isNullOrEmpty() + } + + init { + inflate(context, R.layout.list_item, this) + + context.obtainStyledAttributes(attrs, R.styleable.ListItem, 0, 0).apply { + try { + headlineText = getString(R.styleable.ListItem_headlineText) + leadingIconImage = getDrawable(R.styleable.ListItem_leadingIconImage) + showDivider = getBoolean(R.styleable.ListItem_showDivider, false) + supportingText = getString(R.styleable.ListItem_supportingText) + trailingSupportingText = getString(R.styleable.ListItem_trailingSupportingText) + } finally { + recycle() + } + } + } +} diff --git a/app/src/main/java/org/lineageos/glimpse/ui/MediaInfoBottomSheetDialog.kt b/app/src/main/java/org/lineageos/glimpse/ui/MediaInfoBottomSheetDialog.kt new file mode 100644 index 0000000..5da22f3 --- /dev/null +++ b/app/src/main/java/org/lineageos/glimpse/ui/MediaInfoBottomSheetDialog.kt @@ -0,0 +1,262 @@ +/* + * SPDX-FileCopyrightText: 2023 The LineageOS Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.lineageos.glimpse.ui + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.location.Address +import android.location.Geocoder +import android.net.Uri +import android.os.Build +import android.text.InputType +import android.widget.EditText +import android.widget.TextView +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.res.ResourcesCompat +import androidx.core.view.isVisible +import androidx.exifinterface.media.ExifInterface +import androidx.fragment.app.Fragment +import com.google.android.material.bottomsheet.BottomSheetDialog +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import org.lineageos.glimpse.R +import org.lineageos.glimpse.ext.* +import org.lineageos.glimpse.models.Media +import org.lineageos.glimpse.models.MediaType +import java.text.SimpleDateFormat +import java.util.Locale + +class MediaInfoBottomSheetDialog( + context: Context, + media: Media, + callbacks: Callbacks, +) : BottomSheetDialog(context) { + // Views + private val artistInfoListItem by lazy { findViewById(R.id.artistInfoListItem)!! } + private val cameraInfoListItem by lazy { findViewById(R.id.cameraInfoListItem)!! } + private val dateTextView by lazy { findViewById(R.id.dateTextView)!! } + private val descriptionEditText by lazy { findViewById(R.id.descriptionEditText)!! } + private val locationInfoListItem by lazy { findViewById(R.id.locationInfoListItem)!! } + private val mediaInfoListItem by lazy { findViewById(R.id.mediaInfoListItem)!! } + private val timeTextView by lazy { findViewById(R.id.timeTextView)!! } + + // Coroutines + private val mainScope = CoroutineScope(Job() + Dispatchers.Main) + private val ioScope = CoroutineScope(Job() + Dispatchers.IO) + + // Geocoder + private val geocoder by lazy { Geocoder(context) } + + private val unknownString: String + get() = context.resources.getString(R.string.media_info_unknown) + + init { + setContentView(R.layout.media_info_bottom_sheet_dialog) + + descriptionEditText.setOnEditorActionListener { _, _, _ -> + callbacks.onEditDescription(media, descriptionEditText.text.toString().trim()) + + false + } + + val unknownString = unknownString + + dateTextView.text = dateFormatter.format(media.dateAdded) + timeTextView.text = timeFormatter.format(media.dateAdded) + + mediaInfoListItem.leadingIconImage = ResourcesCompat.getDrawable( + context.resources, + when (media.mediaType) { + MediaType.IMAGE -> R.drawable.ic_image + MediaType.VIDEO -> R.drawable.ic_video_camera_back + }, + null + ) + mediaInfoListItem.headlineText = media.displayName + + val contentResolver = context.contentResolver + + contentResolver.openInputStream(media.externalContentUri)?.use { inputStream -> + val exifInterface = ExifInterface(inputStream) + + val userComment = exifInterface.userComment?.takeIf { + it.isNotBlank() + } + val isSupportedFormatForSavingAttributes = + exifInterface.isSupportedFormatForSavingAttributes + descriptionEditText.setText(userComment ?: "") + descriptionEditText.inputType = when (isSupportedFormatForSavingAttributes) { + true -> InputType.TYPE_CLASS_TEXT + false -> InputType.TYPE_NULL + } + descriptionEditText.isVisible = + userComment != null || isSupportedFormatForSavingAttributes + + artistInfoListItem.headlineText = exifInterface.artist ?: unknownString + + artistInfoListItem.supportingText = listOfNotNull( + exifInterface.software, + exifInterface.copyright, + ).joinToString(SEPARATOR) + + artistInfoListItem.isVisible = listOf( + artistInfoListItem.headlineText, + artistInfoListItem.supportingText, + ).any { !it.isNullOrBlank() && it != unknownString } + + cameraInfoListItem.headlineText = listOfNotNull( + exifInterface.make, + exifInterface.model, + ).joinToString(" ").takeIf { it.isNotBlank() } ?: unknownString + + cameraInfoListItem.supportingText = listOfNotNull( + exifInterface.exposureTime?.let { "${it.toFraction()}s" }, + exifInterface.apertureValue?.let { "ƒ/${it.round(2)}" }, + exifInterface.isoSpeed?.let { "ISO $it" }, + exifInterface.focalLength?.let { "${it.round(2)}mm" }, + ).joinToString(SEPARATOR) + + cameraInfoListItem.isVisible = listOf( + cameraInfoListItem.headlineText, + cameraInfoListItem.supportingText, + ).any { !it.isNullOrBlank() && it != unknownString } + + mediaInfoListItem.supportingText = mutableListOf( + media.mimeType, + ).apply { + exifInterface.size?.let { + add("${((it.width.toDouble() * it.height) / 1024000).round(1)}MP") + add("${it.width} x ${it.height}") + } + }.joinToString(SEPARATOR) + + exifInterface.latLong?.let { + val (lat, long) = it + + locationInfoListItem.setOnClickListener { + val intent = Intent( + Intent.ACTION_VIEW, + Uri.parse("geo:?q=%.8f,%.8f".format(Locale.US, lat, long)) + ) + + context.startActivity( + Intent.createChooser( + intent, + context.resources.getString(R.string.media_info_location_open_with) + ) + ) + } + + val latLongString = listOf( + lat.round(8), + long.round(8), + ).joinToString(SEPARATOR) + + if (Geocoder.isPresent()) { + locationInfoListItem.headlineText = context.resources.getString( + R.string.media_info_location_loading_placeholder + ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + geocoder.getFromLocation(lat, long, 1, ::updateLocation) + } else { + ioScope.launch { + @Suppress("DEPRECATION") + updateLocation( + geocoder.getFromLocation(lat, long, 1) ?: listOf() + ) + } + } + locationInfoListItem.supportingText = latLongString + } else { + locationInfoListItem.headlineText = latLongString + } + + locationInfoListItem.isVisible = true + } + } + } + + private fun updateLocation(addresses: List
) { + mainScope.launch { + locationInfoListItem.headlineText = addresses.getOrNull(0)?.let { address -> + address.getAddressLine(0) ?: listOfNotNull( + listOfNotNull( + address.featureName, + address.thoroughfare, + ).takeIf { it.isNotEmpty() }?.joinToString(" "), + address.locality, + listOfNotNull( + address.postalCode, + address.subAdminArea, + ).takeIf { it.isNotEmpty() }?.joinToString(" "), + address.adminArea, + address.countryName, + ).joinToString(", ").takeIf { it.isNotBlank() } + } ?: unknownString + } + } + + class Callbacks(private val fragment: Fragment) { + private lateinit var editDescriptionMedia: Media + private lateinit var editDescriptionDescription: String + + private val editDescriptionCallback = fragment.registerForActivityResult( + ActivityResultContracts.StartIntentSenderForResult() + ) { + if (it.resultCode == Activity.RESULT_OK) { + editDescription(editDescriptionMedia, editDescriptionDescription) + } + } + + fun onEditDescription(media: Media, description: String = "") { + editDescriptionMedia = media + editDescriptionDescription = description + + val contentResolver = fragment.requireContext().contentResolver + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + editDescriptionCallback.launch( + contentResolver.createWriteRequest(media.externalContentUri) + ) + } else { + editDescription(media, description) + } + } + + private fun editDescription(media: Media, description: String) { + val contentResolver = fragment.requireContext().contentResolver + + contentResolver.openFileDescriptor( + media.externalContentUri, "rw" + )?.use { assetFileDescriptor -> + val exifInterface = ExifInterface(assetFileDescriptor.fileDescriptor) + + exifInterface.userComment = description + + runCatching { + exifInterface.saveAttributes() + }.onFailure { + Toast.makeText( + fragment.requireContext(), + R.string.media_info_write_description_failed, + Toast.LENGTH_LONG + ).show() + } + } + } + } + + companion object { + private const val SEPARATOR = " • " + + private val dateFormatter = SimpleDateFormat.getDateInstance() + private val timeFormatter = SimpleDateFormat.getTimeInstance() + } +} diff --git a/app/src/main/res/drawable/ic_camera.xml b/app/src/main/res/drawable/ic_camera.xml new file mode 100644 index 0000000..47c721f --- /dev/null +++ b/app/src/main/res/drawable/ic_camera.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_image.xml b/app/src/main/res/drawable/ic_image.xml new file mode 100644 index 0000000..4edc386 --- /dev/null +++ b/app/src/main/res/drawable/ic_image.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_info.xml b/app/src/main/res/drawable/ic_info.xml new file mode 100644 index 0000000..1574920 --- /dev/null +++ b/app/src/main/res/drawable/ic_info.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_location_on.xml b/app/src/main/res/drawable/ic_location_on.xml new file mode 100644 index 0000000..f1db4bd --- /dev/null +++ b/app/src/main/res/drawable/ic_location_on.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_person.xml b/app/src/main/res/drawable/ic_person.xml new file mode 100644 index 0000000..dde2cd2 --- /dev/null +++ b/app/src/main/res/drawable/ic_person.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_video_camera_back.xml b/app/src/main/res/drawable/ic_video_camera_back.xml new file mode 100644 index 0000000..1cc05d0 --- /dev/null +++ b/app/src/main/res/drawable/ic_video_camera_back.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/layout/fragment_media_viewer.xml b/app/src/main/res/layout/fragment_media_viewer.xml index 58fd1ac..3d357b6 100644 --- a/app/src/main/res/layout/fragment_media_viewer.xml +++ b/app/src/main/res/layout/fragment_media_viewer.xml @@ -85,6 +85,11 @@ style="@style/Theme.Glimpse.MediaViewer.BottomSheet.Button" android:src="@drawable/ic_share" /> + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/media_info_bottom_sheet_dialog.xml b/app/src/main/res/layout/media_info_bottom_sheet_dialog.xml new file mode 100644 index 0000000..2ea6d29 --- /dev/null +++ b/app/src/main/res/layout/media_info_bottom_sheet_dialog.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml new file mode 100644 index 0000000..66a6712 --- /dev/null +++ b/app/src/main/res/values/attrs.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 10fed0a..5e0492d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -67,4 +67,11 @@ Manage media permission To better manage your images and videos, allow the app to manage your media files + + + Unknown + Add a description + Failed to change media description + Loading… + View the location with diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 4658dfa..e3c8b46 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -58,4 +58,24 @@ @android:color/transparent ?attr/colorOnSurface + + + + + +