Glimpse: Media info bottom sheet dialog
Change-Id: Iadcb08b09cfc07139fc69ce69e605eb4f604c760
This commit is contained in:
parent
b2d54069e4
commit
42961be502
|
@ -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",
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue