Glimpse: Media info bottom sheet dialog

Change-Id: Iadcb08b09cfc07139fc69ce69e605eb4f604c760
This commit is contained in:
Sebastiano Barezzi 2023-08-08 14:39:43 +02:00
parent b2d54069e4
commit 42961be502
No known key found for this signature in database
GPG Key ID: 763BD3AE91A7A13F
20 changed files with 798 additions and 0 deletions

View File

@ -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",

View File

@ -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")

View File

@ -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) {

View File

@ -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
}

View File

@ -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
}

View File

@ -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<TextView>(R.id.dateTextView)
private val deleteButton by getViewProperty<ImageButton>(R.id.deleteButton)
private val favoriteButton by getViewProperty<ImageButton>(R.id.favoriteButton)
private val infoButton by getViewProperty<ImageButton>(R.id.infoButton)
private val shareButton by getViewProperty<ImageButton>(R.id.shareButton)
private val timeTextView by getViewProperty<TextView>(R.id.timeTextView)
private val topSheetConstraintLayout by getViewProperty<ConstraintLayout>(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 {

View File

@ -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<MaterialDivider>(R.id.divider) }
private val headlineTextView by lazy { findViewById<TextView>(R.id.headlineTextView) }
private val leadingIconImageView by lazy { findViewById<ImageView>(R.id.leadingIconImageView) }
private val supportingTextView by lazy { findViewById<TextView>(R.id.supportingTextView) }
private val trailingSupportingTextView by lazy { findViewById<TextView>(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()
}
}
}
}

View File

@ -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<ListItem>(R.id.artistInfoListItem)!! }
private val cameraInfoListItem by lazy { findViewById<ListItem>(R.id.cameraInfoListItem)!! }
private val dateTextView by lazy { findViewById<TextView>(R.id.dateTextView)!! }
private val descriptionEditText by lazy { findViewById<EditText>(R.id.descriptionEditText)!! }
private val locationInfoListItem by lazy { findViewById<ListItem>(R.id.locationInfoListItem)!! }
private val mediaInfoListItem by lazy { findViewById<ListItem>(R.id.mediaInfoListItem)!! }
private val timeTextView by lazy { findViewById<TextView>(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<Address>) {
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()
}
}

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
SPDX-FileCopyrightText: 2023 The LineageOS Project
SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#000000"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:fillType="evenOdd"
android:pathData="M9.19,4.857C9.484,4.328 10.042,4 10.647,4H15.353C15.958,4 16.516,4.328 16.81,4.857L18,7H13H8L9.19,4.857ZM12.952,7H8L3.393,7.768C2.589,7.902 2,8.597 2,9.412V17.333C2,18.254 2.746,19 3.667,19H8.101C6.805,17.73 6,15.959 6,14C6,10.15 9.108,7.026 12.952,7ZM17.899,19C19.195,17.73 20,15.959 20,14C20,10.15 16.892,7.026 13.048,7H18L20.737,7.684C21.479,7.87 22,8.537 22,9.301V17.333C22,18.254 21.254,19 20.333,19H17.899ZM6,10C6,10.552 5.552,11 5,11C4.448,11 4,10.552 4,10C4,9.448 4.448,9 5,9C5.552,9 6,9.448 6,10ZM13,19C15.761,19 18,16.761 18,14C18,11.239 15.761,9 13,9C10.239,9 8,11.239 8,14C8,16.761 10.239,19 13,19Z" />
</vector>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
SPDX-FileCopyrightText: Material Design Authors / Google LLC
SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#000000"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M19,5v14L5,19L5,5h14m0,-2L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM14.14,11.86l-3,3.87L9,13.14 6,17h12l-3.86,-5.14z" />
</vector>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
SPDX-FileCopyrightText: Material Design Authors / Google LLC
SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#000000"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M11,7h2v2h-2zM11,11h2v6h-2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z" />
</vector>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
SPDX-FileCopyrightText: Material Design Authors / Google LLC
SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#000000"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM7,9c0,-2.76 2.24,-5 5,-5s5,2.24 5,5c0,2.88 -2.88,7.19 -5,9.88C9.92,16.21 7,11.85 7,9z" />
<path
android:fillColor="@android:color/white"
android:pathData="M12,9m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0" />
</vector>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
SPDX-FileCopyrightText: Material Design Authors / Google LLC
SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#000000"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,6c1.1,0 2,0.9 2,2s-0.9,2 -2,2 -2,-0.9 -2,-2 0.9,-2 2,-2m0,10c2.7,0 5.8,1.29 6,2L6,18c0.23,-0.72 3.31,-2 6,-2m0,-12C9.79,4 8,5.79 8,8s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z" />
</vector>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
SPDX-FileCopyrightText: Material Design Authors / Google LLC
SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#000000"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M18,10.48V6c0,-1.1 -0.9,-2 -2,-2H4C2.9,4 2,4.9 2,6v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2v-4.48l4,3.98v-11L18,10.48zM16,18H4V6h12V18zM11.62,11.5L9,15l-1.62,-2.17L5,16h10L11.62,11.5z" />
</vector>

View File

@ -85,6 +85,11 @@
style="@style/Theme.Glimpse.MediaViewer.BottomSheet.Button"
android:src="@drawable/ic_share" />
<ImageButton
android:id="@+id/infoButton"
style="@style/Theme.Glimpse.MediaViewer.BottomSheet.Button"
android:src="@drawable/ic_info" />
<ImageButton
android:id="@+id/adjustButton"
style="@style/Theme.Glimpse.MediaViewer.BottomSheet.Button"

View File

@ -0,0 +1,85 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
SPDX-FileCopyrightText: 2023 The LineageOS Project
SPDX-License-Identifier: Apache-2.0
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:backgroundTint="?attr/colorSurface"
android:elevation="0dp"
android:focusable="true"
android:foreground="?attr/selectableItemBackground">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/listItemMainContent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingVertical="12dp"
android:paddingStart="16dp"
android:paddingEnd="24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/leadingIconImageView"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?attr/colorOnSurfaceVariant" />
<TextView
android:id="@+id/headlineTextView"
style="@style/TextAppearance.Material3.BodyLarge"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:textColor="?attr/colorOnSurface"
app:layout_constraintEnd_toStartOf="@+id/trailingSupportingTextView"
app:layout_constraintStart_toEndOf="@+id/leadingIconImageView"
app:layout_constraintTop_toTopOf="parent"
app:layout_goneMarginStart="0dp" />
<TextView
android:id="@+id/supportingTextView"
style="@style/TextAppearance.Material3.BodyMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:textColor="?attr/colorOnSurfaceVariant"
app:layout_constraintEnd_toStartOf="@+id/trailingSupportingTextView"
app:layout_constraintStart_toEndOf="@+id/leadingIconImageView"
app:layout_constraintTop_toBottomOf="@+id/headlineTextView"
app:layout_goneMarginStart="0dp" />
<TextView
android:id="@+id/trailingSupportingTextView"
style="@style/TextAppearance.Material3.BodyMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAlignment="viewEnd"
android:textColor="?attr/colorOnSurfaceVariant"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_max="200dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.divider.MaterialDivider
android:id="@+id/divider"
android:layout_width="0dp"
android:layout_height="1dp"
app:dividerColor="?attr/colorSurfaceVariant"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/listItemMainContent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
SPDX-FileCopyrightText: 2023 The LineageOS Project
SPDX-License-Identifier: Apache-2.0
-->
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:cardBackgroundColor="@android:color/transparent"
app:cardCornerRadius="16dp"
app:cardElevation="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurfaceContainer"
android:backgroundTint="?attr/colorSurfaceContainerHighest"
android:padding="16dp">
<TextView
android:id="@+id/dateTextView"
style="@style/Theme.Glimpse.MediaInfoBottomSheetDialog.DateTimeText"
android:text="SAT APRIL 13, 2019"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/timeTextView"
style="@style/Theme.Glimpse.MediaInfoBottomSheetDialog.DateTimeText"
android:text="12:09 PM"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/dateTextView" />
<EditText
android:id="@+id/descriptionEditText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:hint="@string/media_info_add_description_hint"
android:importantForAutofill="no"
android:inputType="text"
android:letterSpacing="0.02"
android:maxLines="1"
android:paddingTop="8dp"
android:textColor="?attr/colorOnSecondaryContainer"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/timeTextView" />
</androidx.constraintlayout.widget.ConstraintLayout>
<org.lineageos.glimpse.ui.ListItem
android:id="@+id/artistInfoListItem"
style="@style/Theme.Glimpse.MediaInfoBottomSheetDialog.ListItem"
app:headlineText="John Doe"
app:leadingIconImage="@drawable/ic_person"
app:supportingText="MS Paint • Copyright, The LineageOS Project, 2023" />
<org.lineageos.glimpse.ui.ListItem
android:id="@+id/cameraInfoListItem"
style="@style/Theme.Glimpse.MediaInfoBottomSheetDialog.ListItem"
app:headlineText="Galaxy XX"
app:leadingIconImage="@drawable/ic_camera"
app:supportingText="1/3616s • ƒ/2.4 • 28mm" />
<org.lineageos.glimpse.ui.ListItem
android:id="@+id/mediaInfoListItem"
style="@style/Theme.Glimpse.MediaInfoBottomSheetDialog.ListItem"
app:headlineText="Image.jpg"
app:leadingIconImage="@drawable/ic_image"
app:supportingText="image/jpg • 12.0MP • 3000 x 4000" />
<org.lineageos.glimpse.ui.ListItem
android:id="@+id/locationInfoListItem"
style="@style/Theme.Glimpse.MediaInfoBottomSheetDialog.ListItem"
android:visibility="gone"
app:headlineText="Ohio, US"
app:leadingIconImage="@drawable/ic_location_on"
app:supportingText="39.961152990628904 • -82.9963749073814" />
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
SPDX-FileCopyrightText: 2023 The LineageOS Project
SPDX-License-Identifier: Apache-2.0
-->
<resources>
<declare-styleable name="ListItem">
<attr name="headlineText" format="string" />
<attr name="leadingIconImage" format="reference" />
<attr name="showDivider" format="boolean" />
<attr name="supportingText" format="string" />
<attr name="trailingSupportingText" format="string" />
</declare-styleable>
</resources>

View File

@ -67,4 +67,11 @@
<!-- Manage media permission -->
<string name="manage_media_permission_title">Manage media permission</string>
<string name="manage_media_permission_message">To better manage your images and videos, allow the app to manage your media files</string>
<!-- Media info bottom sheet dialog -->
<string name="media_info_unknown">Unknown</string>
<string name="media_info_add_description_hint">Add a description</string>
<string name="media_info_write_description_failed">Failed to change media description</string>
<string name="media_info_location_loading_placeholder">Loading…</string>
<string name="media_info_location_open_with">View the location with</string>
</resources>

View File

@ -58,4 +58,24 @@
<item name="android:backgroundTint">@android:color/transparent</item>
<item name="tint">?attr/colorOnSurface</item>
</style>
<!-- Media info bottom sheet dialog -->
<style name="Theme.Glimpse.MediaInfoBottomSheetDialog" />
<!-- Media info bottom sheet dialog date/time text -->
<style name="Theme.Glimpse.MediaInfoBottomSheetDialog.DateTimeText">
<item name="android:layout_width">0dp</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:letterSpacing">0.03</item>
<item name="android:textAllCaps">true</item>
<item name="android:textColor">?attr/colorOnSecondaryContainer</item>
<item name="android:textSize">16sp</item>
<item name="android:typeface">monospace</item>
</style>
<!-- Media info bottom sheet dialog list item -->
<style name="Theme.Glimpse.MediaInfoBottomSheetDialog.ListItem">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
</style>
</resources>